Возможно ли избежать использования вызова виртуального метода в этом случае?

У меня есть тип данных, которые должны храниться в смежном массиве, который повторяется для обновления этих данных. Сложная часть заключается в том, что я хочу иметь возможность динамически изменять способ обновления любого объекта.

Это то, что я придумал до сих пор:

struct Update {
    virtual void operator()(Data & data) {}
};

struct Data {
    int a, b, c;
    Update * update;
};

struct SpecialBehavior : public Update {
    void operator()(Data & data) override { ... }
};

Затем я бы присвоил каждому объекту данных некоторый тип обновления. Затем во время обновления все данные передаются его собственному функтору обновления:

for (Data & data : all)
   data->update(data);

Это, насколько я понимаю, называется шаблоном стратегии.

Мой вопрос: есть ли способ сделать это более эффективно? Какой-то способ добиться такой же гибкости без затрат на вызов виртуального метода?

Ответы

Ответ 1

Вместо этого вы можете использовать указатель на функцию.

struct Data;

using Update = void (*)(Data &);

void DefaultUpdate(Data & data) {};

struct Data {
    int a, b, c;
    Update update = DefaultUpdate;
};

void SpecialBehavior(Data & data) { ... };
// ...
Data a;
a.update = &SpecialBehaviour;

Это позволяет избежать стоимости виртуальной функции, но все же имеет стоимость использования указателя функции (что меньше). Начиная с С++ 11, вы также можете использовать не-захватывающие lambdas (которые неявно конвертируются в указатели на функции).

a.update = [](Data & data) { ... };

В качестве альтернативы вы можете использовать перечисление и оператор switch.

enum class UpdateType {
    Default,
    Special
};

struct Data {
    int a, b, c;
    UpdateType behavior;
};

void Update(Data & data) {
    switch(data.behavior) {
        case UpdateType::Default:
            DoThis(data);
            break;
        case UpdateType::Special:
            DoThat(data);
            break;
    }
}

Ответ 2

Что такое накладные расходы на вызов виртуальной функции? Ну, реализация должна сделать две вещи:

  • Загрузите указатель vtable из объекта.
  • Загрузите указатель на функцию из таблицы vtable.

Это как раз две неизменности памяти. Вы можете избежать одного из двух, поместив указатель функции непосредственно внутри объекта (избегая поиска указателя vtable из объекта), который является подходом, представленным ответом ralismarks.

Это имеет недостаток, что он работает только для одной виртуальной функции, если вы добавите больше, вы раздуваете свои объекты указателями на функции, что приведет к более сильному давлению на ваши кеши и, следовательно, к ухудшению производительности. Пока вы просто заменяете одну виртуальную функцию, это прекрасно, добавьте еще три, и вы раздули свой объект на 24 байта.


Невозможно избежать второй косвенности памяти, если вы не убедитесь, что компилятор может получить реальный тип Update во время компиляции. И поскольку, как представляется, всецело использовать виртуальную функцию для выполнения решения во время выполнения, вам не повезло: любая попытка "удалить" эту косвенность даст худшую производительность.

(я говорю "remove" в кавычках, потому что вы, конечно же, можете избежать поиска указателя функции из памяти. Цена будет заключаться в том, что вы выполняете что-то вроде лестницы switch() или else if() для определенного идентификационного значения типа, загруженного из объект, который окажется более дорогостоящим, чем просто загрузка указателя функции из объекта. Второе решение в ralismarks answer делает это явно, а std::variant<> подход Vittorio Romeo скрывает его в шаблоне std::variant<>. Направление не действительно удалено, оно просто скрыто в еще более медленных операциях.)

Ответ 3

Если вам не нужен полиморфизм открытого набора (т.е. вы заранее знаете все типы, которые будут получены из Update), вы можете использовать вариант, например std::variant или boost::variant:

struct Update0 { void operator()(Data & data) { /* ... */ } };
struct Update1 { void operator()(Data & data) { /* ... */ } };
struct Update2 { void operator()(Data & data) { /* ... */ } };

struct Data {
    int a, b, c;
    std::variant<Update0, Update1, Update2> update;
};

for (Data & data : all)
{
    std::visit(data.update, [&data](auto& x){ x(data); });
}

Это позволит вам:

  • Избегайте стоимости вызова функции virtual.

  • Храните ваши экземпляры Data в режиме кэширования.

  • Имеют классы Update с разными интерфейсами или произвольно разными состояниями.


Если вы хотите разрешить открытый полиморфизм, но только через интерфейс operator()(Data&), вы можете использовать что-то вроде function_view, который является в основном безопасным по типу ссылкой на объект функции с определенной сигнатурой.

struct Data {
    int a, b, c;
    function_view<void(Data&)> update_function;
};

for (Data & data : all)
{
    data.update_function(data);
}