Мудрецы мира С++ часто напоминают нам о том как важно максимально точно выражать свои мысли в коде, делать код понятным и простым, не теряя при этом (а зачастую и выигрывая) в эффективности. Но задумайтесь как выглядит в С++ код связанный с динамическим полиморфизмом.

Сложные иерархии наследования, ручное управление памятью, провисшие ссылки, аллокация на каждый объект, тот самый виртуальный деструктор, о котором обязательно спрашивают джунов, большие накладные расходы на вызов, так как компилятор очень плох в девиртуализации, непереиспользуемые классы - класс, который написан для использования в полиморфном контексте неэффективно использовать в других контекстах, создание часто ненужных rtti нод...
Типичный код с виртуальными функциями выглядит примерно так:
class Ifoo {
public:
virtual int foo() const = 0;
virtual ~Ifoo() = default;
};
class Bar : public Ifoo {
public:
int foo() const override {
return i + 10;
}
int i;
};
К сожалению, многие просто не замечают здесь ничего особенного, нормальный же код?
Нет.
Суть нашего кода объектах, которые вызывают разные версии foo, а не в наследовании, указателях и выделениях памяти. Виртуальные функции буквально вынуждают нас закапываться в куче бойлер плейта реализации.
Подумайте как правильно скопировать такой полиморфный объект. Придётся делать новый метод типа Clone(), изощряться, ох, не этим мы хотели заниматься, неправда ли? Неужели эту проблему не замечал никто? Замечали конечно, но долгое время в стандарте С++ не было других способов работы с динамическим полиморфизмом.
В С++17 появились такие инструменты как std::any и std::variant, вкратце опишем их устройство, плюсы и минусы:
std::any - объект этого класса способен хранить любой другой копируемый объект. Собственно всё. Позволяет избежать некоторых аллокаций, но использовать это практически невозможно, нужно либо угадать что там лежит, либо просто использовать его как деструктор для любого объекта.
std::variant куда интереснее, объект этого класса всегда* хранит объект одного из типов, указанных на компиляции. Большой плюс - возможность заинлайнить деструкторы и другие нужные методы(проявляется особенно хорошо для тривиальных типов), никаких аллокаций, НО у этой штуки громадные проблемы с раздуванием кода и временем компиляции, можно неожиданно вовсе сделать страшные вещи (https://godbolt.org/z/3hxxP8PvW) к тому же ещё большие проблемы с исключениями - внезапно variant не всегда хранит значение, он может быть .valueless_by_exception! И полагаю никто всерьёз не собирается обрабатывать эту ситуацию, ну и пользоваться variant'ом неочень то удобно (через std::visit)
Резюмируя - std::any не подходит ни для чего в силу сложности взаимодействия с тем что лежит внутри, std::variant отлично подходит для тривиальных типов и когда sizeof у них близкий, а вот если объекты сложные, могут бросить исключение или у альтернатив сильно разный sizeof, то это будет крайне неэффективно, не считая конечно того что придётся менять код каждый раз, когда добавляется новая альтернатива.
И С++ до сих пор не имеет стандартной возможности работы с этим!
Но не отчаивайтесь, решение существует. Представьте сначала каким было бы идеальное решение, как бы оно выглядело в коде.
А пока вы представляете я опишу реальное. И оно очень красивое.
Что если мы пойдем дальше и разовьём идею std::any, позволив захватывать произвольные методы у объектов? Что то типа runtime концепта? Программист говорит "хочу хранить в этом объекте любые другие объекты, обладающие методом foo", или, строго говоря, удовлетворяющие списку требований.
Пример - программист хочет создать тип такой, который хранит любой executor, а executor'ом он считает всё, у чего есть метод execute принимающий std::function<void()>
В идеальном мире в коде я представляю себе это так:
template<typename T>
concept executor = requires (T value, std::function<void()> foo) {
value.execute(foo)
};
using any_executor = any_<executor>;
А теперь реальный код, который реально работает
template<typename T>
struct Execute {
static void do_invoke(const T& self, std::function<void()> foo) {
self.execute(std::move(foo));
}
};
using any_executor = aa::any_with<Execute>;
Всё! Тип создан, его можно использовать
struct real_executor {
void execute(auto f) const {
f();
}
};
int main() {
any_executor exe = real_executor{};
aa::invoke<Execute>(exe, [] { std::cout << "Hello world"; });
}
Причём понятное дело можно объявить внутри any_executor метод, который вызывает Execute и пользоваться классом как и всеми другими, максимально удобно. Мы получили ясный и понятный код, value семантику (копирование, мув и прочие плюшки), убрали наследование, управление памятью (и аллокации собственно тоже почти все убрали), real_executor можно использовать не только в полиморфном контексте, то есть увеличилось переиспользование кода, плюсы со всех сторон. Немаловажно также упомянтуь, что мы получили strong exeption guarantee, которой у variant не было.
Ну и добавить конечно же можно любое количество требований к типу (методов), можно добавить только копирование или только мув (что например избавляет от проблемы в std::function, для решения которой добавляют std::move_only_function...)
Чуть не забыл - реализация лежит тут.
Пробуйте, предлагайте, взгляните на С++ по новому! И обязательно выражайте свои мысли в коде правильно!
P.S.
Вызов функции идентичен в ассемблерном коде вызову через vtable,
А остальное использование согласно бенчмаркам примерно на уровне variant по производительности (у виртуальных функций просто нет возможностей по копированию и муву объекта, так что сравнение было с variant и std::proxy, предлагаемым в С++23).
Также в реализации нет ни одного слова virtual и не создаются rtti ноды (но можно подключить метол aa::RTTI и можно будет спросить std::type_info у any).