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


224

Я шукаю правила, пов’язані з передачею функцій шаблонів 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>();
}

Однак дізнатися про цю техніку важко. Гуглінг для "функції як аргументу шаблону" не призводить до великого. І класичні шаблони C ++ Повне керівництво дивно також не обговорює це (принаймні, не з мого пошуку).

У мене є питання, чи дійсно це C ++ (або просто якесь широко підтримуване розширення).

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

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

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

    doOperation<add3>();

Вказівки на веб-посилання-два чи сторінку в книзі шаблонів C ++ будуть вдячні!


Яка перевага функції як аргумента шаблону? Чи не буде тип повернення використаний тип шаблону?
DaClown

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

Відповіді:


123

Так, це дійсно.

Що стосується того, щоб він працював і з функторами, звичайним рішенням є щось подібне:

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. Ви більше не зобов’язані використовувати яваїзми, такі як класи функторів, і можете використовувати, наприклад, статичні вбудовані функції як аргументи шаблону безпосередньо. Досі далеко плакати порівняно з макросами Ліспа 1970-х, але C ++ 11 безумовно має хороший прогрес протягом багатьох років.
pfalcon

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

70

Параметри шаблону можуть бути параметризовані за типом (ім'я типу 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 буде схожий на "повернути a + b". (Слухайте програмістів Lisp, перестаньте димувати!)

Ми також можемо підтвердити, що це ближче до того, що ми хочемо, оскільки це:

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

не вдасться компілювати. GCC каже: "error: '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);"явно не стає чітким".
Dan Nissenbaum

Ось див. Приклад цього накресленого: 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.