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

Я ищу правила, связанные с передачей функций шаблонов 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 ++ Templates The Complete Guide на удивление также не Не обсуждаю (по крайней мере, не из моих поисков).

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

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

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

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

    doOperation<add3>();

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


person SPWorley    schedule 23.07.2009    source источник
comment
В чем преимущество функции как аргумента шаблона? Разве возвращаемый тип не будет использоваться в качестве типа шаблона?   -  person DaClown    schedule 24.07.2009
comment
Связано: лямбда без захвата может распадаться на указатель функции, и вы можете передать это как параметр шаблона в C ++ 17. Clang компилирует его нормально, но в текущем gcc (8.2) есть ошибка, и он ошибочно отклоняет его как не имеющий связи даже с -std=gnu++17. Могу ли я использовать результат оператора преобразования C ++ 17 лямбда-выражения constexpr без захвата в качестве аргумента, не являющегося типом шаблона указателя функции?.   -  person Peter Cordes    schedule 08.08.2018


Ответы (7)


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

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

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

person jalf    schedule 23.07.2009
comment
А теперь интересный вопрос. Когда передается имя функции, это НЕ похоже на указатель функции. Это явная функция, заданная во время компиляции. Таким образом, компилятор точно знает, что он получил во время компиляции. - person SPWorley; 24.07.2009
comment
Можно ли использовать перегрузку шаблона, чтобы указатель функции и случаи функтора имели одинаковый синтаксис? Будет ли выигрыш в производительности? Особенно для функторов без состояния, которые фактически не имеют экземпляров, и поэтому мы хотим избежать любого случая, когда компилятор может прекратить встраивание и оптимизацию. - person SPWorley; 24.07.2009
comment
Преимущество использования функторов перед указателями на функции. Функтор может быть создан внутри класса и, таким образом, предоставляет компилятору больше возможностей для оптимизации (например, встраивания). Компилятору будет сложно оптимизировать вызов по указателю на функцию. - person Martin York; 24.07.2009
comment
Когда функция используется в параметре шаблона, она «распадается» на указатель на переданную функцию. Это аналогично тому, как массивы распадаются на указатели при передаче в качестве аргументов параметрам. Конечно, значение указателя известно во время компиляции и должно указывать на функцию с внешней связью, чтобы компилятор мог использовать эту информацию в целях оптимизации. - person CB Bailey; 24.07.2009
comment
Мой комментарий выше относится к параметрам шаблона, принимающим параметр указателя функции, не являющийся типом. В этом случае создание экземпляра шаблона может быть встроено, поскольку компилятор знает значение указателя во время компиляции. В примере jalf шаблон принимает параметр типа, а тип параметра шаблона и значение аргумента функции вместе определяют вызываемую функцию, объект функции может быть лучше оптимизирован. - person CB Bailey; 24.07.2009
comment
@Arno: В целях оптимизации я бы выбрал функтор, независимо от того, не имеет ли он состояния. - person jalf; 24.07.2009
comment
@Charles, так что на самом деле такой шаблон действительно передает функцию POINTER? Но это действительно странно ... например, вы не можете создать шаблон, который принимает постоянный указатель экземпляра класса. А если это указатель, то он непрозрачен для компилятора и никогда не может быть встроен. - person SPWorley; 24.07.2009
comment
Да, шаблон в вашем примере специализирован для указателя на функцию. Но значение этого указателя известно во время компиляции, поэтому в принципе он все еще может быть встроен. Я не знаю, действительно ли компиляторы встраивают его. - person jalf; 24.07.2009
comment
Перенесемся через несколько лет, и ситуация с использованием функций в качестве аргументов шаблона значительно улучшилась в C ++ 11. Вы больше не обязаны использовать яваизмы, такие как классы функторов, и можете напрямую использовать, например, статические встроенные функции в качестве аргументов шаблона. По-прежнему далеко от макросов Lisp 1970-х годов, но C ++ 11 определенно добился значительного прогресса за эти годы. - person pfalcon; 13.04.2013
comment
@SPWorley это НЕ указатель, за исключением C ++, функции эквивалентны своим указателям, а их указатели эквивалентны указателям своих указателей. Это делает все выражения, включающие функции, эквивалентными. MyFunc == &MyFunc == &&&&MyFunc == ***&MyFunc и т. Д. - person v.oddou; 01.09.2014
comment
поскольку С ++ 11 не лучше ли было бы взять функцию как ссылку rvalue (template <typename F> void doOperation(F&& f) {/**/}), поэтому, например, bind может передавать выражение привязки вместо его привязки? - person user1810087; 08.01.2015
comment
@pfalcon - я, конечно, хотел бы, чтобы вы немного расширили свой комментарий выше, можете использовать, например, статические встроенные функции в качестве аргументов шаблона напрямую. Вы можете объяснить или указать на пример или документ? Хотелось бы узнать больше. - person davidbak; 06.04.2016
comment
Я узнал об этом случайно, когда читал статью cppreference о nullptr. en.cppreference.com/w/cpp/language/nullptr. Это короткий пример передачи указателя функции в шаблон. - person ; 07.03.2017
comment
@jalf Мне очень жаль, что я обновляю старую тему, но как насчет static функторов? Скажем, void operator() (int &v) внутри struct add3 - это static функция. Могу я написать; doOperation(add3::operator()); и быть уверенным, что компилятор сможет без проблем встроить этот статический функтор? - person PatrykB; 24.04.2017
comment
Разве doOperation(add3()); уже не вызывает operator() функтор? Конечно, это не то, что вам нужно? - person Nibor; 16.08.2019

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

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

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.

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

person Community    schedule 28.01.2010
comment
См. stackoverflow.com/questions/13674935/ для последующего вопроса в прямом ответе на наблюдение здесь, что int c = do_op(4,5,func_ptr); явно не встроен. - person Dan Nissenbaum; 03.12.2012
comment
См. Здесь пример того, как это встроено: stackoverflow.com/questions/4860762/ Похоже, что в наши дни компиляторы становятся довольно умными. - person BigSandwich; 16.08.2013

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

person Kietz    schedule 01.03.2014

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

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

person CB Bailey    schedule 23.07.2009

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

person Nikolai Fetissov    schedule 23.07.2009

Пришел сюда с дополнительным требованием, что также должны меняться типы параметров / возвращаемых значений. Следуя Бену Супнику, это будет для некоторого типа 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);
person Sam Ginrich    schedule 18.08.2020

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

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;
person AraK    schedule 23.07.2009