Защищенные методы vs защищенные поля
ООП - вещь занятная и многогранная. Почему-то пришла в голову аналогия с математикой: есть начальные заданные правила(принципы и аксиомы), благодаря которым выводятся весьма нетривиальные следствия. Сегодня поговорим о довольно базовом следствии, которое однако далеко не у всех в голове есть.
Есть у вас класс и вы хотите дать его наследникам и только им возможность изменять поля класса. И у нас для этого есть два подхода: объявить поля protected и дать возможность наследникам изменять их напрямую или ввести protected методы, которые определяют полный набор изменений этих полей. Какой вариант более предпочтительный и почему?
Сразу раскрою карты: лучше определять защищенный интерфейс вместо прямого доступа к полям. Для этого есть несколько причин:
💥 Как только член класса становится более "доступен", чем private, вы даете гарантии другим классам о том, как этот член будет себя вести. Точнее никаких гарантий вы не даете. Поскольку поле совершенно неконтролируемо, размещение его "в дикой природе" открывает вашему классу и классам, которые наследуют от вашего класса или взаимодействуют с ним, прекрасный вид на саванну, а точнее море ошибок. Нет никакого способа узнать, когда меняется поле, нет никакого способа контролировать, кто или что его меняет.
💥 Если вы хотите хоть что-то похожее на безопасное приложения, вам нужны всякого рода проверки . Иногда они могут занимать несколько строчек кода. И если ваш код зависит от какого-то состояния класса, то при каждом использовании защищенного поля, вам нужно проверять, все ли с ним в порядке, все ли валидно. Насколько же проще вынести все такие проверки в защищенный метод и закрыть у себя в голове этот гештальт.
💥Когда вы определяете защищенный интерфейс, вы автоматически ограничиваете наследников в использовании ваших приватных членов, которые могли бы быть защищенными. А ограничения в программировании - это хорошо! Чем меньше вы позволяете сделать непотребств со своим классом или использовать его вне предполагаемых сценариях использования - тем лучше!
💥 Это даже тестирование упрощает. Есть четкие сценарии поведения, которые можно проверить на корректность. Очень легко с такими исходными данными писать тесты. А легкость тестирования мотивирует его в принципе делать, а не класть на него лысину(ставь лайк, если понял game of words).
💥 Количество наследников у класса может быть много и все будут завязаны на самостоятельном управлении protected членами. А если в будущем обработка этих полей изменится, то необходимы будут изменения во всех наследниках. Защищенный интерфейс делает все такие изменения локализованными в родительском классе, то есть в одном месте. Это снижает стоимость внесения изменений.
Инкапсуляция - гениальная вещь. Придерживаясь этого принципа, вы защищайте свои данные непреднамеренного изменения и позволяете изменениям не распространяться за пределы одного класса.
Protect your secrets. Stay cool.
#OOP #goodpractice #design
ООП - вещь занятная и многогранная. Почему-то пришла в голову аналогия с математикой: есть начальные заданные правила(принципы и аксиомы), благодаря которым выводятся весьма нетривиальные следствия. Сегодня поговорим о довольно базовом следствии, которое однако далеко не у всех в голове есть.
Есть у вас класс и вы хотите дать его наследникам и только им возможность изменять поля класса. И у нас для этого есть два подхода: объявить поля protected и дать возможность наследникам изменять их напрямую или ввести protected методы, которые определяют полный набор изменений этих полей. Какой вариант более предпочтительный и почему?
Сразу раскрою карты: лучше определять защищенный интерфейс вместо прямого доступа к полям. Для этого есть несколько причин:
💥 Как только член класса становится более "доступен", чем private, вы даете гарантии другим классам о том, как этот член будет себя вести. Точнее никаких гарантий вы не даете. Поскольку поле совершенно неконтролируемо, размещение его "в дикой природе" открывает вашему классу и классам, которые наследуют от вашего класса или взаимодействуют с ним, прекрасный вид на саванну, а точнее море ошибок. Нет никакого способа узнать, когда меняется поле, нет никакого способа контролировать, кто или что его меняет.
💥 Если вы хотите хоть что-то похожее на безопасное приложения, вам нужны всякого рода проверки . Иногда они могут занимать несколько строчек кода. И если ваш код зависит от какого-то состояния класса, то при каждом использовании защищенного поля, вам нужно проверять, все ли с ним в порядке, все ли валидно. Насколько же проще вынести все такие проверки в защищенный метод и закрыть у себя в голове этот гештальт.
💥Когда вы определяете защищенный интерфейс, вы автоматически ограничиваете наследников в использовании ваших приватных членов, которые могли бы быть защищенными. А ограничения в программировании - это хорошо! Чем меньше вы позволяете сделать непотребств со своим классом или использовать его вне предполагаемых сценариях использования - тем лучше!
💥 Это даже тестирование упрощает. Есть четкие сценарии поведения, которые можно проверить на корректность. Очень легко с такими исходными данными писать тесты. А легкость тестирования мотивирует его в принципе делать, а не класть на него лысину(ставь лайк, если понял game of words).
💥 Количество наследников у класса может быть много и все будут завязаны на самостоятельном управлении protected членами. А если в будущем обработка этих полей изменится, то необходимы будут изменения во всех наследниках. Защищенный интерфейс делает все такие изменения локализованными в родительском классе, то есть в одном месте. Это снижает стоимость внесения изменений.
Инкапсуляция - гениальная вещь. Придерживаясь этого принципа, вы защищайте свои данные непреднамеренного изменения и позволяете изменениям не распространяться за пределы одного класса.
Protect your secrets. Stay cool.
#OOP #goodpractice #design
👍13🔥7❤2
Порядок вызовов конструкторов и деструкторов дочерних классов
#новичкам
Сегодня такой, довольно попсовый пост. Но, как говорится, это база и это нужно знать.
Вот есть у вас какая-то иерархия классов.
Что мы увидим в консоли, если запустим этот код? Вот это:
Мы имеем 2 невиртуальные ветки наследования в класса MostDerived. В самих классах могут быть виртуальные функции, это роли не играет. В этом случае правила конструирования объекта такое: переходим в самую левую ветку и вызываем поочереди констукторы базовых классов сверху вниз. Как только дошли до MostDerived, переходим вправо в следующую ветку и также вызываем конструкторы сверху вниз. И только после этого конструируем MostDerived.
В общем случае, для n веток, можно представить, что наш наследник - корень дерева с n ветками. Так вот для такого графа конструкторы вызываются, как при обходе в глубину слева-направо.
А вот деструкторами все легко, если вы запомнили порядок вызовов конструкторов. Деструкторы выполняются в обратном порядке вызовов конструкторов соответствующих классов. Вы можете заметить, что вывод консоли из примера полностью симметричен относительно момента, когда объект уже создан.
Казалось бы, тривиальное знание для программиста. Но очень важно осознавать эти вещи для того, чтобы понять более сложные концепции или отвечать на более сложные вопросы. Например: "Зачем нужен виртуальный деструктор?", "В какой момент инициализируется указатель на виртуальную таблицу?", "Какой конкретно метод вызовется, если позвать виртуальный метод из констуруктора базового класса?". Без четкого понимания базы создания объектов, на эти вопросы конечно можно заучить ответы, но понимания никакого не будет. А программирование - это только про понимание, как и любая другая техническая дисциплина.
Поэтому
Understand essence of basic concepts. Stay cool.
#OOP #cppcore
#новичкам
Сегодня такой, довольно попсовый пост. Но, как говорится, это база и это нужно знать.
Вот есть у вас какая-то иерархия классов.
struct Base1 {
Base1() {std::cout << "Base1" << std::endl;}
~Base1() {std::cout << "~Base1" << std::endl;}
};
struct Derived1 : Base1 {
Derived1() {std::cout << "Derived1" << std::endl;}
~Derived1() {std::cout << "~Derived1" << std::endl;}
};
struct Base2 {
Base2() {std::cout << "Base2" << std::endl;}
~Base2() {std::cout << "~Base2" << std::endl;}
};
struct Derived2 : Base2 {
Derived2() {std::cout << "Derived2" << std::endl;}
~Derived2() {std::cout << "~Derived2" << std::endl;}
};
struct MostDerived: Derived2, Derived1 {
MostDerived() {std::cout << "MostDerived" << std::endl;}
~MostDerived() {std::cout << "~MostDerived" << std::endl;}
};
MostDerived{};
Что мы увидим в консоли, если запустим этот код? Вот это:
Base2
Derived2
Base1
Derived1
MostDerived
~MostDerived
~Derived1
~Base1
~Derived2
~Base2
Мы имеем 2 невиртуальные ветки наследования в класса MostDerived. В самих классах могут быть виртуальные функции, это роли не играет. В этом случае правила конструирования объекта такое: переходим в самую левую ветку и вызываем поочереди констукторы базовых классов сверху вниз. Как только дошли до MostDerived, переходим вправо в следующую ветку и также вызываем конструкторы сверху вниз. И только после этого конструируем MostDerived.
В общем случае, для n веток, можно представить, что наш наследник - корень дерева с n ветками. Так вот для такого графа конструкторы вызываются, как при обходе в глубину слева-направо.
А вот деструкторами все легко, если вы запомнили порядок вызовов конструкторов. Деструкторы выполняются в обратном порядке вызовов конструкторов соответствующих классов. Вы можете заметить, что вывод консоли из примера полностью симметричен относительно момента, когда объект уже создан.
Казалось бы, тривиальное знание для программиста. Но очень важно осознавать эти вещи для того, чтобы понять более сложные концепции или отвечать на более сложные вопросы. Например: "Зачем нужен виртуальный деструктор?", "В какой момент инициализируется указатель на виртуальную таблицу?", "Какой конкретно метод вызовется, если позвать виртуальный метод из констуруктора базового класса?". Без четкого понимания базы создания объектов, на эти вопросы конечно можно заучить ответы, но понимания никакого не будет. А программирование - это только про понимание, как и любая другая техническая дисциплина.
Поэтому
Understand essence of basic concepts. Stay cool.
#OOP #cppcore
👍26🔥13❤3😁3
Инкапсуляция и структуры
#новичкам
Всем мы знаем, что раскрывать детали реализации класса - это плохо, этого надо избегать и если вы помыслите об обратном, то придет серенький волчок и укусит за бочок.
Однако, как и со многими такими догматами случается, не всегда нужно следовать этой концепции. Давайте посмотрим на следующий код:
Выглядит солидно. Сеттеры, геттеры, все дела. Но вот души в этом коде нет. Он какой-то.. Бесполезный чтоли.
Методы класса не делают ничего такого, что пользователь класса не смог бы сделать своими руками. Скорее руками это все сделать будет даже короче.
Этим грешат новички: поначитаются модных концепций(или не очень модных) и давай скрывать все члены.
Brah... Don't do this...
Здесь нет никаких инвариантов, которые нужно было бы сохранять. Интерфейс класса и так позволяет все, шо хош делать с объектом.
Если ваш класс используется просто как хранилище данных, то это ближе не к классу, а к сишной структуре. Сделайте вы уже поля открытыми и замените ключевое слово class на более подходящее struct.
Делов-то. Зато прошлым примером можно хорошо отчитываться за количество написанных строчек кода=)
Show your inner world to others. Stay cool.
#cppcore #goodpractice #OOP
#новичкам
Всем мы знаем, что раскрывать детали реализации класса - это плохо, этого надо избегать и если вы помыслите об обратном, то придет серенький волчок и укусит за бочок.
Однако, как и со многими такими догматами случается, не всегда нужно следовать этой концепции. Давайте посмотрим на следующий код:
class MyClass
{
int m_foo;
int m_bar;
public:
int addAll() const;
int getFoo() const;
void setFoo(int foo);
int getBar() const;
void setBar(int bar);
};
int MyClass::addAll() const
{
return m_foo + m_bar;
}
int MyClass::getFoo() const
{
return m_foo;
}
void MyClass::setFoo(int foo)
{
m_foo = foo;
}
int MyClass::getBar() const
{
return m_bar;
}
void MyClass::setBar(int bar)
{
m_bar = bar;
}
Выглядит солидно. Сеттеры, геттеры, все дела. Но вот души в этом коде нет. Он какой-то.. Бесполезный чтоли.
Методы класса не делают ничего такого, что пользователь класса не смог бы сделать своими руками. Скорее руками это все сделать будет даже короче.
Этим грешат новички: поначитаются модных концепций(или не очень модных) и давай скрывать все члены.
Brah... Don't do this...
Здесь нет никаких инвариантов, которые нужно было бы сохранять. Интерфейс класса и так позволяет все, шо хош делать с объектом.
Если ваш класс используется просто как хранилище данных, то это ближе не к классу, а к сишной структуре. Сделайте вы уже поля открытыми и замените ключевое слово class на более подходящее struct.
struct MyClass
{
int m_foo;
int m_bar;
}
Делов-то. Зато прошлым примером можно хорошо отчитываться за количество написанных строчек кода=)
Show your inner world to others. Stay cool.
#cppcore #goodpractice #OOP
❤29👍23🔥7💯3😁1
🔞 Если принимаете сырой указатель, как параметр - не забудьте проверить его на nullptr. Меньше будете голову ломать при будущем дебаге.
🔞 compareData зачем-то принимает параметры по значению. В смысле, понятно зачем: чтобы было double free, а вам было интереснее ревьюить. Лучше все-таки принимать по константной ссылке аргументы.
🔞 Поле buffer публичное как раз для того, чтобы к нему доступ имела compareData. Мы все-таки за инкапсуляцию и мир во всем мире. Поле надо сделать приватным, а compareData другом. А еще лучше убрать compareData и определить нативный оператор сравнения.
Но если очень хочется оставить compareData, то лучше все равно определить оператор, не делать compareData другом класса, и пусть она использует нативный оператор внутри себя без нарушения инкапсуляции.
🔞 Data имеет конструктор от одного аргумента и лучше бы его сделать explicit. В таком случае, чтобы запретить неявные преобразования.
🔞 Многие отметили, что класс стоит пометить как final. Видимо это какой-то кодстайл, чтобы запретить другим классам наследоваться от этого. Причина, как я понял, это отсутствие виртуального деструктора. Поэтому если кто-то хочет отнаследоваться от класса, то он явно видит, что пока классом нельзя пользоваться полиморфно, убирает final и добавляет виртуальный деструктор.
Все же от Data можно безопасно наследоваться, если не использовать потом наследников полиморфно. А при добавлении виртуального метода можно и самому понять, что надо еще и деструктор соотвествующим сделать.
Мне final кажется оверкиллом для одиночных классов в небиблиотечном коде , но тут кто как привык.
Фух, на этом вроде все.
Вот что вышло по итогу исправлений:
Пишите, если что забыл.
Critique your solution. Stay cool.
#cppcore #OOP #goodpractice #design
🔞 compareData зачем-то принимает параметры по значению. В смысле, понятно зачем: чтобы было double free, а вам было интереснее ревьюить. Лучше все-таки принимать по константной ссылке аргументы.
🔞 Поле buffer публичное как раз для того, чтобы к нему доступ имела compareData. Мы все-таки за инкапсуляцию и мир во всем мире. Поле надо сделать приватным, а compareData другом. А еще лучше убрать compareData и определить нативный оператор сравнения.
Но если очень хочется оставить compareData, то лучше все равно определить оператор, не делать compareData другом класса, и пусть она использует нативный оператор внутри себя без нарушения инкапсуляции.
🔞 Data имеет конструктор от одного аргумента и лучше бы его сделать explicit. В таком случае, чтобы запретить неявные преобразования.
🔞 Многие отметили, что класс стоит пометить как final. Видимо это какой-то кодстайл, чтобы запретить другим классам наследоваться от этого. Причина, как я понял, это отсутствие виртуального деструктора. Поэтому если кто-то хочет отнаследоваться от класса, то он явно видит, что пока классом нельзя пользоваться полиморфно, убирает final и добавляет виртуальный деструктор.
Все же от Data можно безопасно наследоваться, если не использовать потом наследников полиморфно. А при добавлении виртуального метода можно и самому понять, что надо еще и деструктор соотвествующим сделать.
Мне final кажется оверкиллом для одиночных классов в небиблиотечном коде , но тут кто как привык.
Фух, на этом вроде все.
Вот что вышло по итогу исправлений:
class Data final {
private:
std::unique_ptr<char[]> buffer;
size_t buf_size = 0;
public:
Data(const char* input, size_t size)
: buf_size(size) {
if (input == nullptr && size != 0) {
throw std::invalid_argument("Input cannot be null for non-zero size");
}
if (size > 0) {
buffer = std::make_unique<char[]>(size);
std::copy(input, input + size, buffer.get());
}
}
template <size_t N>
explicit Data(const char (&str)[N])
: Data(str, N) {
}
// Копирующий конструктор
Data(const Data& other)
: Data{other.buffer.get(), other.buf_size} {
}
// Универсальное присваивание
Data& operator=(Data other) {
swap(*this, other);
return *this;
}
// Перемещающий конструктор
Data(Data&& other) noexcept {
swap(*this, other);
}
~Data() = default;
// Оператор сравнения
bool operator==(const Data& rhs) const noexcept {
return std::string_view{buffer.get(), buf_size} == std:;string_view{rhs.buffer.get(), buf_size()};
}
// Вспомогательная функция для обмена
friend void swap(Data& first, Data& second) noexcept {
using std::swap;
swap(first.buffer, second.buffer);
swap(first.buf_size, second.buf_size);
}
};
[[nodiscard]] bool compareData(const Data& data1, const Data& data2) noexcept {
return data1 == data2;
}Пишите, если что забыл.
Critique your solution. Stay cool.
#cppcore #OOP #goodpractice #design
❤35🔥12👍8❤🔥1👎1
Динамический полиморфизм. ООP-style
#новичкам
Полиморфизм - это способность кода единообразно обрабатывать разные сущности. И хоть термин "полиморфизм" называют принципом ООП, это понятие в широком смысле выходит за границы этой парадигмы. Любая конструкция языка, которая позволяет единообразно управлять разными сущностями проявляет полиморфные свойства. В этом и следующих постах постараемся по верхам раскрыть сущности, реализующие полиморфизм в С++ в широком смысле.
Но раз уж заговорили об ООП, давайте для начала поговорим про полиморфизм в рамках ООП.
Если мы говорим про ООП, значит где-то рядом тусуются классы и их иерархии. Полиморфизм в объектно-ориентированном программировании - один из основных его принципов. Это свойство, позволяющее объектам разных классов обрабатываться одинаково, используя общий интерфейс. При этом поведение разное в зависимости от конкретного типа объекта. Реализации интерфейсов у всех классов разные. И решение о вызове того или иного конкретного метода принимается во время выполнения программы.
Для работы работы динамического полиморфизма нужен: базовый класс, пара наследников и виртуальные методы:
У нас есть интерфейс ITask и виртуальный метод Execute. Два других класса наследуются от ITask и переопределяют метод Execute. В задаче FileDeleteTask удаляется файл по заданному пути из файловой системы. В задаче S3FileUploadTask файл загружается в удаленное хранилище S3.
Заметим, у этих задач общий интерфейс(их можно выполнить), но они совершают разные действия.
Теперь мы можем использовать эти задачи:
У нас есть 2 продюсера, которые кладут задачи в очередь, и воркер, который выполнятся задачи из очереди.
В очереди хранятся уникальные указатели на базовый класс ITask. Это значит, что она может хранить объекты любых наследников интерфейса ITask.
Теперь самое важное: воркеру не нужно знать, какую конкретно задачу он сейчас достанет из очереди и какой конкретно продюсер ее туда положил. Единственное, что важно - общий интерфейс. Он позволяет единообразно выполнить задачи разных типов, даже не зная их исходный тип.
В этом и суть: абстрагироваться от конкретной реализации и верхнеуровнево определить, как себя должен вести объект.
Но динамический полиморфизм не ограничивается полиморфизмом подтипов. Для него вообще иерархия классов не нужна. И в следующих постах посмотрим, что еще в С++ позволяет реализовать полиморфное поведение.
Extract common traits. Stay cool.
#OOP #cppcore
#новичкам
Полиморфизм - это способность кода единообразно обрабатывать разные сущности. И хоть термин "полиморфизм" называют принципом ООП, это понятие в широком смысле выходит за границы этой парадигмы. Любая конструкция языка, которая позволяет единообразно управлять разными сущностями проявляет полиморфные свойства. В этом и следующих постах постараемся по верхам раскрыть сущности, реализующие полиморфизм в С++ в широком смысле.
Но раз уж заговорили об ООП, давайте для начала поговорим про полиморфизм в рамках ООП.
Если мы говорим про ООП, значит где-то рядом тусуются классы и их иерархии. Полиморфизм в объектно-ориентированном программировании - один из основных его принципов. Это свойство, позволяющее объектам разных классов обрабатываться одинаково, используя общий интерфейс. При этом поведение разное в зависимости от конкретного типа объекта. Реализации интерфейсов у всех классов разные. И решение о вызове того или иного конкретного метода принимается во время выполнения программы.
Для работы работы динамического полиморфизма нужен: базовый класс, пара наследников и виртуальные методы:
struct ITask {
virtual void Execute() = 0;
virtual ~ITask() = default;
};
struct FileDeleteTask : public ITask {
std::string path_;
FileDeleteTask(const std::string &path) : path_(path) {}
void Execute() override {
std::filesystem::remove(path_);
std::cout << "Deleted: " << path_ << std::endl;
}
};
struct S3FileUploadTask : public ITask {
std::string bucket_;
std::string path_;
std::shared_ptr<S3Client> client_;
S3FileUploadTask(const std::string &bucket, const std::string &path, const std::shared_ptr<S3Client> &client)
: bucket_{bucket}, path_{path}, client_{client} {}
void Execute() override {
client_->Upload(bucket_, path_);
std::cout << "Uploaded: " << bucket_ << ", pathL " << path_ << std::endl;
}
};У нас есть интерфейс ITask и виртуальный метод Execute. Два других класса наследуются от ITask и переопределяют метод Execute. В задаче FileDeleteTask удаляется файл по заданному пути из файловой системы. В задаче S3FileUploadTask файл загружается в удаленное хранилище S3.
Заметим, у этих задач общий интерфейс(их можно выполнить), но они совершают разные действия.
Теперь мы можем использовать эти задачи:
void Producer1(const std::string &bucket, const std::vector<std::string> &paths,
const std::shared_ptr<S3Client> &client, std::deque<std::unique_ptr<ITask>> &tasks) {
for (const auto &path : paths)
tasks.emplace_back(std::make_unique<S3FileUploadTask>(bucket, path, client));
}
void Producer2(const std::vector<std::string> &paths, std::deque<std::unique_ptr<ITask>> &tasks) {
for (const auto &path : paths)
tasks.emplace_back(std::make_unique<FileDeleteTask>(path));
}
void Worker(std::deque<std::unique_ptr<ITask>> &tasks) {
while (!tasks.empty()) {
auto task = std::move(tasks.front());
task.pop_front();
task->Execute();
}
}
У нас есть 2 продюсера, которые кладут задачи в очередь, и воркер, который выполнятся задачи из очереди.
В очереди хранятся уникальные указатели на базовый класс ITask. Это значит, что она может хранить объекты любых наследников интерфейса ITask.
Теперь самое важное: воркеру не нужно знать, какую конкретно задачу он сейчас достанет из очереди и какой конкретно продюсер ее туда положил. Единственное, что важно - общий интерфейс. Он позволяет единообразно выполнить задачи разных типов, даже не зная их исходный тип.
В этом и суть: абстрагироваться от конкретной реализации и верхнеуровнево определить, как себя должен вести объект.
Но динамический полиморфизм не ограничивается полиморфизмом подтипов. Для него вообще иерархия классов не нужна. И в следующих постах посмотрим, что еще в С++ позволяет реализовать полиморфное поведение.
Extract common traits. Stay cool.
#OOP #cppcore
1❤28👍10🔥8⚡1
Inline виртуальные методы
#опытным
В догонку предыдущего поста. Если можно сделать обычные методы inline, то можно и виртуальные сделать. Что это изменит?
Ну то есть такая иерархия:
Получим ли мы какой-то профит от того, что виртуальные методы теперь inline?
Посмотрим на такой код:
Конечно же метод будет встраиваться в вызывающий код. Компилятор на этапе компиляции четко знает тип, с которым он работает, и может проводить оптимизации.
Но вообще говоря, это далеко не основной кейс использования виртуальных методов. Давайте ближе к реальной задаче:
Здесь все понятно. Компилятор реально не знает, какие типы находятся внутри my_vec в функции vec_base. Поэтому все, что он может сделать - это использовать указатель на таблицу виртуальных функций и пусть рантайме уже решается, какой конкретно метод вызвать.
Теперь посмотрим такой код:
И здесь тоже не инлайнится код! Хотя компилятор же видит, что Derived - это единственный наследник.
Но на деле это может быть не так. Единица трансляции с vec_derived может не знать о наследниках уже Derived класса, которые определены отдельно. Компилятор не может достоверно доказать, что у Derived нет наследников, поэтому и не может инлайнить код. Опять vtable и оверхэд.
И здесь в игру вступает ключевое слово final и девиртуализация вызовов, о которой мы говорили тут и тут. Пометив класс как final, мы можем ожидать, что компилятор поймет, что никаких наследников у него нет. Поэтому он может встраивать вызов метода foo для Derived класса в последнем примере.
Вот код на годболте с последними примерами и final девиртуализацией. Немного упростил его, чтобы легче было ассемблер читать.
Правды ради стоит сказать, что для девиртуализации нужны очень особенные условия, которых сложно достичь, используя стандартные практики программирования и паттерны ООП.
Учитывая, что
1️⃣ виртуальные функции обычно довольно громоздкие(их код будет генерится во всех единицах трансляции, что раздувает бинарь)
2️⃣ операции над непосредственно типами наследниками не распространены
3️⃣ а для девиртуализации нужно танцевать с бубнами
от определения чуть сложных виртуальных методов внутри класса могут быть только проблемы. Поэтому например в Chromium гайдлайнах четко прописано, что запрещены инлайн определения виртуальных методов. Все уносим в цпп.
Be useful. Stay cool.
#cppcore #OOP #design
#опытным
В догонку предыдущего поста. Если можно сделать обычные методы inline, то можно и виртуальные сделать. Что это изменит?
Ну то есть такая иерархия:
class Base {
public:
virtual int foo() = 0;
protected:
int x = 2;
};
class Derived : public Base {
public:
int foo() override {
x *= 2;
return x;
}
};Получим ли мы какой-то профит от того, что виртуальные методы теперь inline?
Посмотрим на такой код:
void simple() {
Derived d;
std::cout << d.foo();
}Конечно же метод будет встраиваться в вызывающий код. Компилятор на этапе компиляции четко знает тип, с которым он работает, и может проводить оптимизации.
Но вообще говоря, это далеко не основной кейс использования виртуальных методов. Давайте ближе к реальной задаче:
void vec_base(std::vector<std::unique_ptr<Base>>& my_vec) {
for (auto &p : my_vec) {
std::cout << p->foo();
}
}
// другая TU
std::vector<std::unique_ptr<Base>> my_vec;
my_vec.push_back(new Derived());
vec_base(my_vec);Здесь все понятно. Компилятор реально не знает, какие типы находятся внутри my_vec в функции vec_base. Поэтому все, что он может сделать - это использовать указатель на таблицу виртуальных функций и пусть рантайме уже решается, какой конкретно метод вызвать.
Теперь посмотрим такой код:
void vec_derived(std::vector<std::unique_ptr<Derived>>& my_vec) {
for (auto &p : my_vec) {
std::cout << p->foo();
}
}
// другая TU
std::vector<std::unique_ptr<Derived>> my_vec;
my_vec.push_back(new Derived());
vec_base(my_vec);И здесь тоже не инлайнится код! Хотя компилятор же видит, что Derived - это единственный наследник.
Но на деле это может быть не так. Единица трансляции с vec_derived может не знать о наследниках уже Derived класса, которые определены отдельно. Компилятор не может достоверно доказать, что у Derived нет наследников, поэтому и не может инлайнить код. Опять vtable и оверхэд.
И здесь в игру вступает ключевое слово final и девиртуализация вызовов, о которой мы говорили тут и тут. Пометив класс как final, мы можем ожидать, что компилятор поймет, что никаких наследников у него нет. Поэтому он может встраивать вызов метода foo для Derived класса в последнем примере.
Вот код на годболте с последними примерами и final девиртуализацией. Немного упростил его, чтобы легче было ассемблер читать.
Правды ради стоит сказать, что для девиртуализации нужны очень особенные условия, которых сложно достичь, используя стандартные практики программирования и паттерны ООП.
Учитывая, что
1️⃣ виртуальные функции обычно довольно громоздкие(их код будет генерится во всех единицах трансляции, что раздувает бинарь)
2️⃣ операции над непосредственно типами наследниками не распространены
3️⃣ а для девиртуализации нужно танцевать с бубнами
от определения чуть сложных виртуальных методов внутри класса могут быть только проблемы. Поэтому например в Chromium гайдлайнах четко прописано, что запрещены инлайн определения виртуальных методов. Все уносим в цпп.
Be useful. Stay cool.
#cppcore #OOP #design
1❤13👍6🔥6❤🔥2