[БЕЗ_ЗВУКА] Итак, вы решили часть F. Давайте обсудим вместе ее решение и заодно узнаем какие-то интересные моменты. С чего стоит начать реализовывать билиотеку вывода Svg? Конечно, стоит начать с вывода цветов, потому что цвета бывают разные, Это несложно, но тем не менее давайте обсудим. Цвета должны были быть либо строчками, либо RGB, то есть красный, синий, зеленый, либо никакими. И это нужно было поддержать в самой библиотеке. Для этого, конечно, идеально подходит std::variant. И вот мы его следующим образом используем. Мы объявим, во-первых, структуру Rgb, с полями red, green и blue и скажем, что цвет — это variant из monostate, string, Rgb; monostate мы упоминали в коричневом поясе как способ иметь variant, который может быть ничем. То есть, по сути, optional над variant — это довольно странная конструкция, давайте у нас будет variant из трех вариантов: либо цвет никакой, и для этого у нас есть monostate, либо цвет string, либо цвет, цвет Rgb. Вот у нас этот тип Color объявляется именно вот так, плюс нам нужна константа NoneColor, собственно, с вариантом monostate, который обозначат несуществующий цвет, для удобства. И нужно уметь этот цвет выводить, что мы и делаем с помощью std::visit. Вот у нас есть функция RenderColor, которую мы будем много где использовать, она принимает поток, в который мы будем выводить, и, собственно, сам цвет. Здесь вызывается функция visit от lambda, которая вызывает RenderColor от конкретного значения цвета и, собственно, самого std::variant. И у нас написаны варианты перегрузки RenderColor для monostate, и в этом случае выводится none, потому что это никакой цвет, для строки, и здесь выводится, собственно, сама строка, и для цвета, здесь выводится просто Rgb и значение в круглых скобках. С цветом все понятно. Как же, собственно, реализовать саму библиотеку, где нам нужно хранить ломаные, тексты, круги и общие SVG-документы со всем вот этим вместе? Понятно, что если мы хотим хранить набор объектов, по-разному устроенных, но имеющих что-то общее, нам нужна виртуальная иерархия классов. Ну и мы напишем некоторый базовый класс Object, от него отнаследуем, например, круг, и в кругу сделаем все нужные нам методы. Это самый простой подход, давайте посмотрим, что из этого у нас получится. Нам нужна в любом случае структура точки, она нам полезна. Вот у нас есть те же самые цвета, цвета, цвета, цвета. Вот есть базовый класс Object, в нем есть, конечно же, виртуальный деструктор и виртуальный метод Render, потому что нужно уметь рендерить любые объекты. Ну и, соответственно, в рендеринге документа будем рендерить все объекты подряд, это довольно понятно. Это чисто виртуальный метод, соотвественно, Object — это абстрактный класс, вам нельзя просто взять и создать Object. В кругу, согласно самому простому подходу, мы наследуемся от Object базового и пишем все необходимые Set-методы, все, что требовалось в условии задачи: SetFillColor, SetStrokeColor, ла-ла-ла-ла-ла, SetCenter, SetRadius и метод Render, который пригодится нам в самом документе. Ну и, соответственно, есть все необходимые приватные поля, которые нужны для хранения всех этих свойств. Где-то optional нужен, где-то optional не нужен — смотрите условие задачи. Класс Document. Я здесь показываю только класс круга. Понятно, что если я захочу написать текст или ломаную, они будут выглядеть примерно так же. И в этом проблема. Но тем не менее класс Document выглядит понятно как, нам нужно уметь рендерить документ, нужно хранить в нем все объекты в виде unique_ptr на них и нужно уметь добавлять новый объект. Это, кстати, интересный момент, как мы добавляем новый объект. Я пропущу Set-методы, потому что в них совершенно ничего интересного, кроме того, что мы ради chaining возвращаем ссылку на текущий объект. В рендеринге тоже совершенно ничего интересного, просто берем и вводим XML по строчкам. И вот, как я и обещал, метод Add. Нам нужно уметь добавлять какой-то объект, при этом, кажется, удобнее всего будет уметь забирать владение этим объектом, поэтому принимаем объект по значению, здесь же создаем умный указатель и move'аем туда наш объект. И этот умный указатель push_back'аем в наш вектор объектов. Вот такой мы написали Add. И в рендеринге документа мы просто перебираем все объекты, и от каждого вызываем виртуальный метод Render. И поэтому все в итоге работает. Но есть нюанс, к сожалению. Если я захочу написать еще какой-то объект, кроме круга, мне придется скопировать все эти методы общих свойств конкретных всевозможных линий: толщины линий у нас тут есть, цвета, типы соединений. Это все есть во всех объектах, это придется скопировать. И методы, и конкретные поля. И это явно не то, чего мы хотим в С++, уж тем более в черном поясе. Нужно как-то выделить общие поля и общие методы. Понятно, что для этого нужно наследование. И простейший, простейшее умозаключение и преобразование кода приводит нас к следующей идее. Давайте мы выделим общие свойства всевозможных путей, PathProps мы их назовем, в отдельный класс, в котором будут те самые поля и те самые Set-методы. Мы даже напишем у него метод RenderAttrs, который будет рендерить эти самые свойства. Здесь вы видите реализацию этого метода. Просто fill равно тому-то, stroke равно тому-то, выделили общий код. И от этого PathProps если мы отнаследуемся в кругу, то у нас для круга станут доступны эти самые методы. Здесь, правда, получилось множественное наследование, но не нужно его пугаться. В данном случае у вас есть базовый класс Object, который нужен только для того, чтобы у вас была виртуальная иерархия классов, и есть базовый класс PathProps, от которого берутся нужные везде поля и методы. Ну и, соотвественно, класс круга получается довольно небольшим и приятным. Вот мы даже написали класс линии, он тоже маленький и приятный, и класс текста чуть побольше просто потому, что у текста больше свойств. Но везде есть наследование от PathProps, и вроде как мы можем для объектов любого типа вызывать эти самые общие методы вроде SetFillColor. Однако есть подвох. Нам нужен chaining методов, нам нужно уметь вызывать методы подряд, друг от друга через точку. Что мы возвращали в методах круга? Мы возвращали ссылку на Circle. Довольно логично. В методе поля Line возвращали ссылку на поле Line, а в метода текста возвращали ссылку на текст. Но в PathProps что мы еще можем вернуть, кроме как ссылку на сам текущий объект? Вот return *this мы возвращаем. Но это влечет за собой некоторые проблемы. Если мы возьмем пример из условия задачи и подключим этот самый заголовочный файл, текущую версию, и попробуем скомпилировать, этот самый пример, где мы создаем Polyline и вызываем SetStrokeColor, SetStrokeWidth, SetStrokeLineCap, AddPoint, Addpoint. Если мы это попробуем скомпилировать, то оно не скомпилируется с вердиктом, что у PathProps нету метода AddPoint. Оно и понятно, на самом деле, потому что когда мы вызывали эти общие методы, из этого метода, из SetStrokeLineCap вернулась ссылка на PathProps. И теперь у нас есть только ссылка на базовую часть исходной линии и мы не можем от нее вызывать метод AddPoint просто потому, что у PathProps нету метода AddPoint. Ну и все, проблема. Как можно ее решать? Нам нужно как-то, с одной стороны, выделить из того же круга, из той же Polyline общий код, с одной стороны, но, с другой стороны, нужно, чтобы эти самые методы, которые мы отнаследовали, возвращали ссылку нужного нам типа. Если мы в Circle вызываем SetFillColor, то мы ожидаем, что вернется ссылка на Circle, а если мы в Polyline вызываем SetFillColor, мы ожидаем, что вернется ссылка на Polyline. Как можно добиться такой универсальности, гибкости типа возвращаемого значения этих общих методов. Нам нужно использовать шаблонизацию. Опять же если особо не думать и не бояться того, что будут делать ваши руки, то получается следующее решение. Мы делаем шаблонный PathProps, который шаблонизирован типом владельца этих методов. Соответственно, если мы делаем PathProps для Circle, для круга, то мы будем возвращать ссылку на Circle. И мы именно так напишем этот шаблонный уже класс. Если мы пишем PathProps для Polyline, то мы должны возвращать ссылку на Polyline. Соответственно, само описание класса меняется минимально, только лишь этот Owner участвует как возвращаемое значение в этих методах. Сами методы мы пишем каким образом? Давайте спустимся до этого метода. Например, SetFillColor. Мы записываем то, что нужно, куда нужно, и теперь мы хотим вернуть ссылку на текущий объект, но текущий объект, с точки зрения PathProps — это сам этот PathProps, у которого нет нужных методов, но мы-то знаем, что, если у нас был Circle, например, у него есть базовый класс PathProps, и если мы сейчас внутри PathProps, то у нас все-таки есть некий объемлющий Circle, который от нас отнаследовался, и потом безопасно сделать static_cast к классу-потомку. В данном случае это Owner. Вот мы кастуем ссылку на this — это ссылка на PathProps — к ссылке на Owner. И так мы делаем во всех методах. И, наверное, одна из самых интересных частей: как выглядит класс Circle теперь. Класс Circle наследуется от Object и от PathProps от Circle. То есть ровно там, где мы определяем Circle, мы уже используем этот Circle. И, в принципе, это не страшно, потому что самому PathProps нужно только знать, что этот тип существует, А уж ссылка на этот тип, адрес в памяти — он для любого типа просто адрес в памяти. Поэтому это успешно работает. У вас появилась статическая типизация, статическая шаблонизация. И теперь если мы вернемся к нашему примеру и используем новую версию нашей библиотеки, то мы увидим, что все успешно компилируется и работает. Вот у меня выводится пример с svg файлом, с той самой прямой, у которой правильно установлен цвет линии, толщина линии и т.д. Остался последний штрих. В методах PathProps мы везде делали static_cast к потомку. Это неудобно, потому что если вы будете добавлять новые такие методы, вам везде нужно этот static_cast, по сути, копировать. Ну и половина этих Set методов — это копипаст. Это некрасиво. Поэтому этот общий код можно вынести спокойно и написать отдельный метод у PathProps. Ну как мы его напишем? template шаблонизируем по типу класса владельца. Метод, который будет возвращать эту самую ссылку на Owner, мы называем этот метод AsOwner. Метод AsOwner у класса PathProps от Owner не принимает ничего. Нужно ли его делать константным? Да, в общем, нет, нам никакая константность не нужна. И здесь мы просто вернем этот самый static_cast, ссылку на класс владелец. И везде вот здесь мы заиспользуем этот самый метод. Ну и давайте я покажу, что из этого получается. Вот у нас есть класс PathProps, в нем есть приватный метод AsOwner, возвращающий ссылку на Owner, он реализован, как static_cast к Owner ссылка. И во всех Set методах мы используем в return этот самый AsOwner. Вот здесь, вот здесь, вот здесь. Ну, это уже скорее не копипаст, а просто использование общего удобного метода. Вот у нас получилось отличное решение. Давайте обсудим еще небольшой относящийся к нему момент, вопрос, который мог у вас возникнуть. У нас здесь множественное наследование. И все типы наследуются и от Object, и от PathProps. И Circle, и Polyline, и Text — везде наследование от Object и от PathProps. Можно ли объединить Object и PathProps в один какой-то общий класс? Во-первых, сходу совсем это не получится, потому что PathProps здесь у нас везде шаблонизирован типом класса владельца. Но можно рискнуть, отнаследовать PathProps от Object и сказать, что это не набор каких-то общих свойств линий, вот этот класс, а это объект, обладающий такими свойствами. И отнаследовать PathProps от Object. А здесь везде просто наследоваться от PathProps, не наследуясь от Object. Ну, то есть давайте я примерно покажу, что произойдет. Мы вот это наследование уберем, а здесь отнаследуемся от Object. Я все еще не говорю, что так стоит делать. Но так можно попробовать, и так в принципе, у вас будет работать. У вас как бы здесь меньше наследований, и здесь как бы все есть. Но есть нюанс. Если вы захотите добавить еще какой-то набор общих свойств объектов, например, общие свойства анимированности этих объектов, и допишете новый AnimationProps и тоже отнаследуете его от Object и от него отнаследуетесь в каком-нибудь кругу, вот здесь, еще public что-нибудь Props. AnimationProps. То если этот AnimationProps будет отнаследован от Object, у вас будет Object дважды базовым классом от вашего типа. Ну и ничего хорошего из этого не выйдет, поэтому давайте вот так делать не будем. А оставим именно отдельный PathProps со смыслом, что есть общие свойства линий, и отдельный Object, от которого все объекты должны наследоваться. И вот у нас получилось отличное решение части f.