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


225

Я ищу правила передачи функций шаблонов 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 ++!


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

Связанный: лямбда без перехватов может распадаться на указатель на функцию, и вы можете передать это как параметр шаблона в C ++ 17. Clang компилирует это нормально, но текущая версия gcc (8.2) имеет ошибку и неправильно отвергает ее как отсутствие «связи» даже с -std=gnu++17. Могу ли я использовать результат оператора захвата лямбда-констекспра в C ++ 17 без захвата в качестве аргумента не тип шаблона указателя функции? ,
Питер Кордес

Ответы:


124

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

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

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


19
Теперь вот интересный вопрос. Когда передается имя функции, это НЕ похоже на указатель на функцию. Это явная функция, данная во время компиляции. Таким образом, компилятор точно знает, что он получил во время компиляции.
SPWorley

1
Преимущество использования функторов перед указателями на функции Функтор может быть реализован внутри класса и, таким образом, предоставляет больше возможностей компилятору для оптимизации (такой как встраивание). Компилятору будет сложно оптимизировать вызов по указателю на функцию.
Мартин Йорк,

11
Когда функция используется в параметре шаблона, она «разлагается» на указатель на переданную функцию. Это аналогично тому, как массивы распадаются на указатели при передаче в качестве аргументов параметрам. Конечно, значение указателя известно во время компиляции и должно указывать на функцию с внешней связью, чтобы компилятор мог использовать эту информацию в целях оптимизации.
CB Bailey

5
Перенесемся через несколько лет, ситуация с использованием функций в качестве аргументов шаблона значительно улучшилась в C ++ 11. Вы больше не обязаны использовать Javaisms, такие как классы функторов, и можете напрямую использовать статические встроенные функции в качестве аргументов шаблона. По сравнению с макросами Lisp 1970-х годов все еще далеко, но C ++ 11 определенно добился значительных успехов за эти годы.
pfalcon

5
поскольку c ++ 11 не лучше ли взять функцию в качестве rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}), поэтому bind, например, может передать выражение bind вместо его связывания?
user1810087

70

Параметры шаблона могут быть параметризованы по типу (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.

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


2
См. Stackoverflow.com/questions/13674935/… для последующего вопроса в прямом ответе на замечание, которое здесь int c = do_op(4,5,func_ptr);«явно не вписывается».
Дэн Ниссенбаум

Смотрите здесь для примера этого встраивания: stackoverflow.com/questions/4860762/… Кажется, что компиляторы становятся довольно умными в наши дни.
BigSandwich

15

Указатели на функции могут быть переданы в качестве параметров шаблона, и это является частью стандарта 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.


9

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

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


1

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


0

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

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;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.