Daniro_San
@Daniro_San
Программист

Передача функции — шаблоны или std::function?

У меня есть функция, одним из аргументов принимающая другую функцию - причем передаваться может как лямбда так и обычная функция.
Использовать указатели на функцию я не хочу, ибо они плохо смотрятся вместе с лямбдами и плюшками C++14.
Что лучше использовать -
template<typename Func> void MyFunc (const Func & otherFunc);

void MyFunc (const auto & otherFunc);
void MyFunc (const std::function<void()> & otherFunc);

void MyFunc (const void (&otherFunc)()); ?
Посмотрел аналогичные ситуации в STL - там используются шаблоны.
Но зачем тогда нужен класс std::function ?
Может быть всё таки использовать ссылку на функцию?
Как в 2016 году лучше передавать функции?
spoiler

Функция выполняет действие с строкой - принимает символ, вычленяет подстроки по обе стороны символа и вставляет результат работы передаваемой функции.
Работает это примерно так:
void Subcalc(
 std::string &line,
 const char &symbol, 
 const std::function<std::string(const std::string&, const std::string&)> 
 &mathFunc);

std::string s = " 5+5*3+1 ";

Subcalc ( s, '*', myMathFunc);    // s == "5+15+1"

Или так:
std::string s = " 5+5*3-1 ";

Subcalc ( s, '*', myStringFunc);    // s == "5+555+1"


В общем я колеблюсь между вариантами:
void Subcalc(
 std::string &line,
 const char &symbol, 
 const std::function<std::string(const std::string&, const std::string&)> 
 &mathFunc);


void Subcalc(
 std::string &line, const char &symbol, const auto &mathFunc);


template<typename Func>
void Subcalc(
 std::string &line,
 const char &symbol, 
 const Func &mathFunc);


void Subcalc(
 std::string &line,
 const char &symbol, 
 std::string (&mathFunc)(const std::string&, const std::string&));

Какой вариант лучше всех?
  • Вопрос задан
  • 4924 просмотра
Решения вопроса 3
Первый вариант:
template<typename Func> void MyFunc (const Func & otherFunc);

Гарантирует, что otherFunc будет заинлайнена в MyFunc, то есть такой вариант предпочтителен там, где требуется максимальная производительность. Но шаблонные функции не могут быть виртуальными. То есть такой вариант очень хорошо подходит для функций-утилит, и плохо подходит для методов публичного API и на границах модулей, так как там как раз инлайн не нужен, а нужны виртуальность и сокрытие реализации.

Важное замечание! Приведённый код не совсем корректен, всегда в таких случаях используйте передачу по универсальной ссылке:
template<typename Func> void MyFunc (Func&& otherFunc);

Такой вариант покрывает и константные функторы, и те, которым необходима мутабельность для работы. Если вы планируете сохранить функтор, то это ещё и позволит воспользоваться perfect forwarding -- по сути, единственный способ, корректно работающий с любым переданным функтором:
field = std::forward<Func>(otherFunc);

Второй вариант:
void MyFunc (const auto & otherFunc);
Можно использовать, если вам не нужен тип функтора. По сути он эквивалентен предыдущему.

Третий вариант:
void MyFunc (const std::function<void()> & otherFunc);

Возьмите отрицание моих предыдущих аргументов и примените сюда. Можно использовать с виртуальными функциями, больше подходит для API, но теряет в производительности. А именно, вызов otherFunc эквивалентен вызову виртуальной функции.

Четвёртый вариант:
void MyFunc (const void (&otherFunc)());
Работает только с простыми, глобально определёнными функциями. Вместо этого практически всегда стоит использовать std::function.
Ответ написан
Комментировать
@Free_ze
Пишу комментарии в комментарии, а не в ответы
std::function может работать не только с функциями, но и с функторами.
Полагаю, что в STL так сделано в угоду экономии ресурсов - указатель/ссылка все равно "легче", чем std::function.
Ответ написан
Комментировать
Nipheris
@Nipheris Куратор тега C++
template<typename Func> void MyFunc (const Func & otherFunc);

void MyFunc (const std::function<void()> & otherFunc);

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

С другой стороны, почему-то никто не упомянул такую важную разницу:
class overloaded_functor {
public:
	bool operator()(int a) const { return a > 10; }
	bool operator()(double a) const { return a > 10.0; }
	bool operator()(const std::string& a) const { return a.size() > 10; }
};

template <typename Func>
void foo(Func f) {
	// Все варианты компилятся и работают (т.к. сама шаблонная функция foo и все применения оператора вызова к f компилятся ПОСЛЕ того как мы написали foo(overloaded_functor()) );
	f(3);
	f(5.4);
	f("Blah");
}

void bar(std::function<bool(int)> f) {
	f(3);
	// Компилится, но, как и ожидаемо, конвертит double к int, чего мы в нашем случае не хотим
	f(5.4);
	// Не компилится вовсе
	f("Blah");
}
...
foo(overloaded_functor());
bar(overloaded_functor());


std::function по-определению требует указания конкретного типа функции/функтора, которую он оборачивает, поэтому std::function не может работать с перегруженным функтором. Это не так часто нужно, но бывает, например visitor часто реализуется именно таким функтором с несколькими вариантами оператора вызова.

На всякий случай отмечу, что я ни в коем случае не говорю, что std::function недостаточно мощный вариант. Наоборот, его нужно использовать в большинстве случаев, т.к. в большинстве случаев нужно функция конкретного типа (конкретной сигнатуры и конкретного возвращаемого значения). Однако разницу следует понимать, т.к. эти варианты используются в разных ситуациях.
Ну и понятное дело, обычный указатель на функцию неудобен, т.к. не позволяет захватить контекст. В Сях для этого существует паттерн userData, и всякий разработчик библиотеки, которая использует callback-и, реализует этот паттерн. В Плюсах для этого есть std::function.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
kozura
@kozura
Наверное, в STL максимально поддержан старый стандарт, поэтому там шаблоны.
std::function, более очевиднее, а удлинённость можно исправить лаконичностью -typedef.
Излишняя универсальность может стать проблемой, хотя это больше вкусовые штуки.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы