Функция передана как аргумент шаблона

Я ищу правила, связанные с передачей функций шаблонов С++ в качестве аргументов.

Это поддерживается С++, как показано на примере:

#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>();
}

Изучение этой техники сложно. Googling для функции как аргумент шаблона не приводит к большому количеству. И классический С++ Templates The Complete Guide неожиданно также не обсуждает его (по крайней мере, не из моего поиска).

У меня есть вопросы: действительно ли это С++ (или просто какое-то широко распространенное расширение).

Кроме того, существует ли способ, чтобы функтор с одной и той же сигнатурой использовался взаимозаменяемо с явными функциями во время такого вызова шаблона?

В приведенной выше программе работает не, по крайней мере, в Visual С++, потому что синтаксис, очевидно, неправильно. Было бы неплохо включить функцию для функтора и наоборот, аналогично тому, как вы можете передать указатель на функцию или функтор в алгоритм std:: sort, если вы хотите определить пользовательскую операцию сравнения.

   struct add3 {
      void operator() (int &v) {v+=3;}
   };
...

    doOperation<add3>();

Указатели на веб-ссылку или две или страницу в книге шаблонов С++ будут оценены!

Ответ 1

Да, это действительно.

Что касается того, чтобы заставить его работать с функторами, обычное решение выглядит примерно так:

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(), а не только некоторые неизвестный указатель функции.)

Ответ 2

Параметры шаблона могут быть либо параметризованы по типу (typename T), либо по значению (int X).

"Традиционный" С++ способ шаблонирования фрагмента кода - использовать функтор, то есть код находится в объекте, и таким образом объект дает уникальный тип кода.

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

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". (Lisp программисты, прекратите ухмыляться!)

Мы также можем подтвердить, что это ближе к тому, что мы хотим, потому что это:

int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);

не скомпилируется. GCC говорит: "Ошибка:" func_ptr "не может появляться в константном выражении. Другими словами, я не могу полностью расширить do_op, потому что вы не дали мне достаточно информации во время компилятора, чтобы узнать, что такое наш оператор.

Итак, если второй пример действительно полностью встраивает наш 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);

Этот пример будет работать! (Я не предполагаю, что это хороший С++, но...). Что произошло, так это 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.

Для сравнения, наш байтовый случай требует точного совпадения аргументов функции.

Ответ 3

В вашем шаблоне

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() полностью определяется типом объекта функции.

Ответ 4

Указатели функций могут передаваться как параметры шаблона, а это часть стандартного С++ , Однако в шаблоне они объявляются и используются как функции, а не как указатель на функцию. В экземпляре шаблона вместо адреса присваивается адрес функции.

Например:

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);

Несколько ответов передают экземпляр functor в качестве аргумента:

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 и один раз с параметром типа.

// 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.

Ответ 5

Причина, по которой ваш пример functor не работает, заключается в том, что вам нужен экземпляр для вызова operator().

Ответ 6

Изменить: передача оператора в качестве ссылки не работает. Для простоты понимайте его как указатель на функцию. Вы просто отправляете указатель, а не ссылку. Я думаю, вы пытаетесь написать что-то вроде этого.

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;