Функция передана как аргумент шаблона
Я ищу правила передачи функций шаблонов C++ в качестве аргументов.
Это поддерживается C++, как показано на примере здесь:
#include <iostream>
void add1(int &v)
{
v+=1;
}
void add2(int &v)
{
v+=2;
}
template <void (*T)(int &)>
void doOperation()
{
int temp=0;
T(temp);
std::cout << "Result is " << temp << std::endl;
}
int main()
{
doOperation<add1>();
doOperation<add2>();
}
Однако узнать об этой технике сложно. Поиск в Google для "функции в качестве аргумента шаблона" мало что дает. И классические шаблоны C++ The Complete Guide, к удивлению, также не обсуждают это (по крайней мере, из моего поиска).
У меня есть вопросы, является ли это допустимым C++ (или просто некоторым широко поддерживаемым расширением).
Кроме того, есть ли способ позволить функтору с одинаковой сигнатурой взаимозаменяемо использоваться с явными функциями во время такого вызова шаблона?
Следующее не работает в вышеуказанной программе, по крайней мере, в Visual C++, потому что синтаксис явно неправильный. Было бы неплохо иметь возможность отключить функцию для функтора и наоборот, подобно тому, как вы можете передать указатель функции или функтор в алгоритм std::sort, если вы хотите определить пользовательскую операцию сравнения.
struct add3 {
void operator() (int &v) {v+=3;}
};
...
doOperation<add3>();
Будем признательны за указатели на одну или две веб-ссылки или страницу в книге шаблонов C++!
6 ответов
Да, это действительно.
Что касается работы с функторами, обычное решение выглядит примерно так:
template <typename F>
void doOperation(F f)
{
int temp=0;
f(temp);
std::cout << "Result is " << temp << std::endl;
}
который теперь можно назвать как:
doOperation(add2);
doOperation(add3());
Проблема в том, что если компилятору сложно встроить вызов add2
, поскольку все, что знает компилятор, это то, что тип указателя на функцию void (*)(int &)
передается doOperation
, (Но add3
будучи функтором, может быть легко встроен. Здесь компилятор знает, что объект типа add3
передается в функцию, что означает, что вызываемая функция add3::operator()
, а не просто какой-то неизвестный указатель на функцию.)
Параметры шаблона могут быть параметризованы по типу (typename T) или по значению (int X).
"Традиционный" C++ способ шаблонирования фрагмента кода заключается в использовании функтора, то есть кода в объекте, и объект, таким образом, дает коду уникальный тип.
При работе с традиционными функциями этот метод не работает должным образом, потому что изменение типа не указывает на конкретную функцию - скорее, оно определяет только сигнатуру многих возможных функций. Так:
template<typename OP>
int do_op(int a, int b, OP op)
{
return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op(4,5,add);
Не эквивалентно случаю функтора. В этом примере do_op создается для всех указателей на функции, чья сигнатура int X (int, int). Компилятор должен быть довольно агрессивным, чтобы полностью встроить этот случай. (Я бы не стал исключать это, так как оптимизация компилятора стала довольно продвинутой.)
Один из способов сказать, что этот код не совсем то, что мы хотим, это:
int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);
все еще законно, и ясно, что это не становится ограниченным. Чтобы получить полное встраивание, нам нужно шаблон по значению, чтобы функция полностью была доступна в шаблоне.
typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);
В этом случае каждая конкретная версия do_op создается с уже доступной определенной функцией. Таким образом, мы ожидаем, что код для do_op будет выглядеть как "return a + b". (Программисты на Лиспе, перестань ухмыляться!)
Мы также можем подтвердить, что это ближе к тому, что мы хотим, потому что это:
int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);
не удастся скомпилировать. GCC говорит: "ошибка:" func_ptr "не может появляться в константном выражении. Другими словами, я не могу полностью развернуть do_op, потому что вы не дали мне достаточно информации во время компиляции, чтобы знать, что такое наш оператор".
Так что, если второй пример действительно полностью вписывается в нашу операционную систему, а первый - нет, что хорошего в шаблоне? Что это делает? Ответ: тип принуждения. Этот рифф на первом примере будет работать:
template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);
Этот пример будет работать! (Я не утверждаю, что это хороший C++, но...) То, что произошло, - do_op был создан вокруг сигнатур различных функций, и каждая отдельная реализация будет писать код приведения разных типов. Таким образом, инстанцированный код для do_op с fadd выглядит примерно так:
convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.
Для сравнения, наш случайный случай требует точного соответствия аргументов функции.
Указатели на функции могут передаваться как параметры шаблона, и это является частью стандарта C++. Однако в шаблоне они объявлены и используются как функции, а не как указатель на функцию. При создании шаблона передается адрес функции, а не только имя.
Например:
int i;
void add1(int& i) { i += 1; }
template<void op(int&)>
void do_op_fn_ptr_tpl(int& i) { op(i); }
i = 0;
do_op_fn_ptr_tpl<&add1>(i);
Если вы хотите передать тип функтора в качестве аргумента шаблона:
struct add2_t {
void operator()(int& i) { i += 2; }
};
template<typename op>
void do_op_fntr_tpl(int& i) {
op o;
o(i);
}
i = 0;
do_op_fntr_tpl<add2_t>(i);
Несколько ответов передают экземпляр функтора в качестве аргумента:
template<typename op>
void do_op_fntr_arg(int& i, op o) { o(i); }
i = 0;
add2_t add2;
// This has the advantage of looking identical whether
// you pass a functor or a free function:
do_op_fntr_arg(i, add1);
do_op_fntr_arg(i, add2);
Самое близкое к этому единому виду с аргументом шаблона является определение do_op
дважды - один раз с нетиповым параметром и один раз с параметром типа.
// non-type (function pointer) template parameter
template<void op(int&)>
void do_op(int& i) { op(i); }
// type (functor class) template parameter
template<typename op>
void do_op(int& i) {
op o;
o(i);
}
i = 0;
do_op<&add1>(i); // still need address-of operator in the function pointer case.
do_op<add2_t>(i);
Честно говоря, я действительно ожидал, что это не скомпилируется, но у меня это сработало с gcc-4.8 и Visual Studio 2013.
В вашем шаблоне
template <void (*T)(int &)>
void doOperation()
Параметр T
является нетиповым параметром шаблона. Это означает, что поведение функции шаблона изменяется со значением параметра (который должен быть зафиксирован во время компиляции, каковы константы указателя на функцию).
Если вы хотите что-то, что работает как с объектами функций, так и с параметрами функций, вам нужен типизированный шаблон. Однако, когда вы делаете это, вам также необходимо предоставить экземпляр объекта (либо экземпляр объекта функции, либо указатель функции) для функции во время выполнения.
template <class T>
void doOperation(T t)
{
int temp=0;
t(temp);
std::cout << "Result is " << temp << std::endl;
}
Есть некоторые незначительные соображения производительности. Эта новая версия может быть менее эффективной с аргументами указателя на функцию, так как указатель на конкретную функцию только разыменовывается и вызывается во время выполнения, тогда как шаблон указателя на функцию можно оптимизировать (возможно, вызов функции встроен) на основе конкретного используемого указателя функции. Функциональные объекты часто могут быть очень эффективно расширены с помощью типизированного шаблона, хотя в качестве конкретного operator()
полностью определяется типом объекта функции.
Причина, по которой ваш пример функтора не работает, заключается в том, что вам нужен экземпляр для вызова operator()
,
Пришел сюда с дополнительным требованием, что также должны меняться типы параметров / возвращаемых значений. Следуя Бену Супнику, это будет для некоторого типа T.
typedef T(*binary_T_op)(T, T);
вместо того
typedef int(*binary_int_op)(int, int);
Решение здесь - поместить определение типа функции и шаблон функции в окружающий шаблон структуры.
template <typename T> struct BinOp
{
typedef T(*binary_T_op )(T, T); // signature for all valid template params
template<binary_T_op op>
T do_op(T a, T b)
{
return op(a,b);
}
};
double mulDouble(double a, double b)
{
return a * b;
}
BinOp<double> doubleBinOp;
double res = doubleBinOp.do_op<&mulDouble>(4, 5);
В качестве альтернативы BinOp может быть классом со статическим шаблоном метода do_op(...), который затем называется
double res = BinOp<double>::do_op<&mulDouble>(4, 5);
Редактировать: передача оператора в качестве ссылки не работает. Для простоты понимаем его как указатель на функцию. Вы просто отправляете указатель, а не ссылку. Я думаю, что вы пытаетесь написать что-то вроде этого.
struct Square
{
double operator()(double number) { return number * number; }
};
template <class Function>
double integrate(Function f, double a, double b, unsigned int intervals)
{
double delta = (b - a) / intervals, sum = 0.0;
while(a < b)
{
sum += f(a) * delta;
a += delta;
}
return sum;
}
,,
std::cout << "interval : " << i << tab << tab << "intgeration = "
<< integrate(Square(), 0.0, 1.0, 10) << std::endl;