Передача функций в С++
Предположим, я хочу написать функцию, которая вызывает нулевую функцию 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 c++17 гарантированное исключение предотвратит ложную копию.
В шутку вы могли бы сделать:
template<typename F>
void call100(F&& f) {
for (int i = 0; i < 99; ++i)
f();
std::forward<F>(f)();
}
но это зависит от людей, у которых &&
перегружает свой operator()
, чего никто не делает.
Ответ 3
Я не думаю, что есть однозначный ответ:
-
Первый копирует все, что вы передаете, что может быть дорого для захвата лямбд, но в остальном обеспечивает большую гибкость:
Pros
- Const объекты разрешены
- Разрешены изменяемые объекты (скопированы)
- Копия может быть исключена (?)
Cons
- Копирует все, что вы даете
- Вы не можете вызвать это с существующим объектом, таким как изменяемая лямбда, не копируя это в
-
Второй нельзя использовать для константных объектов. С другой стороны, он ничего не копирует и допускает изменяемые объекты:
Pros
- Допустимы изменяемые объекты
- Ничего не копирует
Cons
- Не разрешает const объекты
-
Третий не может быть использован для изменчивых лямбд, так что это небольшая модификация второго.
Pros
- Const объекты разрешены
- Ничего не копирует
Cons
- Не может быть вызвано с изменяемыми объектами
-
Четвертый не может быть вызван с константными объектами, если вы не скопируете их, что становится довольно неловко с лямбдами. Вы также не можете использовать его с уже существующим изменяемым лямбда-объектом, не копируя его или не удаляясь от него (теряя его в процессе), что аналогично ограничению 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 c++17 гарантированное исключение предотвратит ложную копию.
В шутку вы могли бы сделать:
template<typename F>
void call100(F&& f) {
for (int i = 0; i < 99; ++i)
f();
std::forward<F>(f)();
}
но это зависит от людей, у которых &&
перегружает свой operator()
, чего никто не делает.