Передача функций в С++

Предположим, я хочу написать функцию, которая вызывает нулевую функцию 100 раз. Какая из этих реализаций является лучшей и почему?

template<typename F>
void call100(F f) {
    for (int i = 0; i < 100; i++)
        f();
}

template<typename F>
void call100(F& f) {
    for (int i = 0; i < 100; i++)
        f();
}

template<typename F>
void call100(const F& f) {
    for (int i = 0; i < 100; i++)
        f();
}


template<typename F>
void call100(F&& f) {
    for (int i = 0; i < 100; i++)
        f();
}

Или есть лучшая реализация?

Обновление относительно 4

struct S {
    S() {}
    S(const S&) = delete;
    void operator()() const {}
};

template<typename F>
void call100(F&& f) {
    for (int i = 0; i < 100; i++)
        f();
}

int main() {
    const S s;
    call100(s);
}

Ответ 1

Я бы использовал первый (передать значение по значению).

Если вызывающий абонент обеспокоен стоимостью копирования вызываемого объекта, он может использовать std::ref(f) или std::cref(f) чтобы передать его с помощью reference_wrapper.

Делая это, вы предоставляете абоненту наибольшую гибкость.

Ответ 2

Единственная стоимость выполнения

template<typename F>
void call100(F&& f) {
  for (int i = 0; i < 100; ++i)
    f();
}

является то, что он может иметь больше версий (копий кода), если вы передаете f несколькими способами. При использовании MSVC или золотого компоновщика с ICF эти копии стоят только время компиляции, если они не различаются, и если они различаются, вы, вероятно, захотите сохранить их.

template<typename F>
void call100(F f) {
  for (int i = 0; i < 100; ++i)
    f();
}

у этого есть преимущество того, чтобы быть семантикой значения; и следование правилу принятия ценностей, если только у вас нет веских причин не делать этого, является разумным std::ref/std::cref позволяет вам вызывать его с постоянной ссылкой, а для prvalues гарантированное исключение предотвратит ложную копию.

В шутку вы могли бы сделать:

template<typename F>
void call100(F&& f) {
  for (int i = 0; i < 99; ++i)
    f();
  std::forward<F>(f)();
}

но это зависит от людей, у которых && перегружает свой operator(), чего никто не делает.

Ответ 3

Я не думаю, что есть однозначный ответ:

  1. Первый копирует все, что вы передаете, что может быть дорого для захвата лямбд, но в остальном обеспечивает большую гибкость:

    Pros

    • Const объекты разрешены
    • Разрешены изменяемые объекты (скопированы)
    • Копия может быть исключена (?)

    Cons

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

    Pros

    • Допустимы изменяемые объекты
    • Ничего не копирует

    Cons

    • Не разрешает const объекты
  3. Третий не может быть использован для изменчивых лямбд, так что это небольшая модификация второго.

    Pros

    • Const объекты разрешены
    • Ничего не копирует

    Cons

    • Не может быть вызвано с изменяемыми объектами
  4. Четвертый не может быть вызван с константными объектами, если вы не скопируете их, что становится довольно неловко с лямбдами. Вы также не можете использовать его с уже существующим изменяемым лямбда-объектом, не копируя его или не удаляясь от него (теряя его в процессе), что аналогично ограничению 1.

    Pros

    • Избежание копий явным образом путем принудительного (требующего) перемещения семантики, если копия необходима
    • Допускаются изменчивые объекты.
    • Разрешены объекты Const (кроме изменяемых лямбд)

    Cons

    • Не допускает const изменчивые лямбды без копии
    • Вы не можете вызвать это с существующим объектом, таким как изменяемая лямбда

И там у вас есть это. Здесь нет серебряной пули, и у каждой из этих версий есть свои плюсы и минусы. Я склонен склоняться к первому, являющемуся дефолтом, но с определенными типами захвата лямбд или более крупными вызовами это может стать проблемой. И вы не можете вызвать 1) с изменяемым объектом и получить ожидаемый результат. Как упоминалось в другом ответе, некоторые из них могут быть преодолены с помощью std::ref и других способов манипулирования фактическим типом T По моему опыту, они, как правило, являются источником довольно неприятных ошибок, хотя, когда T - это нечто отличное от ожидаемого, то есть изменчивость копии или что-то подобное.

Ответ 4

Единственная стоимость выполнения

template<typename F>
void call100(F&& f) {
  for (int i = 0; i < 100; ++i)
    f();
}

является то, что он может иметь больше версий (копий кода), если вы передаете f несколькими способами. При использовании MSVC или золотого компоновщика с ICF эти копии стоят только время компиляции, если они не различаются, и если они различаются, вы, вероятно, захотите сохранить их.

template<typename F>
void call100(F f) {
  for (int i = 0; i < 100; ++i)
    f();
}

у этого есть преимущество того, чтобы быть семантикой значения; и следование правилу принятия ценностей, если только у вас нет веских причин не делать этого, является разумным std::ref/std::cref позволяет вам вызывать его с постоянной ссылкой, а для prvalues гарантированное исключение предотвратит ложную копию.

В шутку вы могли бы сделать:

template<typename F>
void call100(F&& f) {
  for (int i = 0; i < 99; ++i)
    f();
  std::forward<F>(f)();
}

но это зависит от людей, у которых && перегружает свой operator(), чего никто не делает.