Funkcja przekazana jako argument szablonu

224

Szukam reguł dotyczących przekazywania funkcji szablonów C ++ jako argumentów.

Jest to obsługiwane przez C ++, jak pokazano na przykładzie tutaj:

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

Jednak nauka tej techniki jest trudna. Googling dla „funkcja jako argument szablonu” nie prowadzi do wielu. I klasyczne szablony C ++ Kompletny przewodnik zaskakująco również go nie omawia (przynajmniej nie z mojego wyszukiwania).

Mam pytania, czy to jest poprawne C ++ (czy tylko jakieś szeroko obsługiwane rozszerzenie).

Czy jest też sposób, aby zezwolić na stosowanie funktora o tej samej sygnaturze zamiennie z funkcjami jawnymi podczas tego rodzaju wywoływania szablonu?

Poniższe nie działa w powyższym programie, przynajmniej w Visual C ++ , ponieważ składnia jest oczywiście nieprawidłowa. Byłoby miło móc wyłączyć funkcję funktora i vice versa, podobnie jak można przekazać wskaźnik funkcji lub funktor do algorytmu std :: sort, jeśli chcesz zdefiniować niestandardową operację porównania.

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

    doOperation<add3>();

Będziemy wdzięczni za wskazania na link lub dwa lub stronę w książce szablonów C ++!

SPWorley
źródło
Jakie są zalety funkcji jako argumentu szablonu? Czy typ zwrotu nie byłby używany jako typ szablonu?
DaClown,
Powiązane: lambda bez przechwytywania może rozpaść się na wskaźnik funkcji, i możesz przekazać to jako parametr szablonu w C ++ 17. Clang kompiluje to dobrze, ale obecny gcc (8.2) ma błąd i niepoprawnie odrzuca go jako „bez powiązania” nawet z -std=gnu++17. Czy mogę użyć wyniku bez przechwyconego operatora konwersji C ++ 17 lambda constexpr jako argumentu innego niż typ szablonu wskaźnika? .
Peter Cordes,

Odpowiedzi:

123

Tak, to jest poprawne.

Jeśli chodzi o to, aby działało to również z funktorami, zwykłym rozwiązaniem jest coś takiego:

template <typename F>
void doOperation(F f)
{
  int temp=0;
  f(temp);
  std::cout << "Result is " << temp << std::endl;
}

które można teraz wywołać jako:

doOperation(add2);
doOperation(add3());

Zobacz na żywo

Problem polega na tym, że jeśli kompilator utrudnia inline wywołanie add2, ponieważ kompilator wie, że void (*)(int &)przekazywany jest typ wskaźnika funkcji doOperation. (Ale add3będąc funktorem, można go łatwo wprowadzić. Tutaj kompilator wie, że obiekt typu add3jest przekazywany do funkcji, co oznacza, że ​​wywoływaną funkcją jest add3::operator(), a nie tylko nieznany wskaźnik funkcji).

jalf
źródło
19
Oto interesujące pytanie. Po przekazaniu nazwy funkcji NIE oznacza to, że w grę wchodzi wskaźnik funkcji. Jest to wyraźna funkcja podana podczas kompilacji. Kompilator wie dokładnie, co ma w czasie kompilacji.
SPWorley
1
Zaletą używania funktorów nad wskaźnikami funkcji. Funktor można zainstalować w klasie, a tym samym zapewnia on kompilatorowi większe możliwości optymalizacji (takie jak wstawianie). Kompilator byłby mocno naciskany, aby zoptymalizować wywołanie nad wskaźnikiem funkcji.
Martin York,
11
Gdy funkcja jest używana w parametrze szablonu, „rozkłada się” na wskaźnik do przekazanej funkcji. Jest to analogiczne do tego, w jaki sposób tablice rozpadają się na wskaźniki, gdy są przekazywane jako argumenty parametrów. Oczywiście wartość wskaźnika jest znana w czasie kompilacji i musi wskazywać na funkcję z zewnętrznym łączeniem, aby kompilator mógł wykorzystać te informacje do celów optymalizacji.
CB Bailey,
5
Szybko do przodu, kilka lat później, sytuacja z użyciem funkcji jako argumentów szablonu znacznie poprawiona w C ++ 11. Nie musisz już używać Javaism, takich jak klasy funktorów, i możesz na przykład bezpośrednio używać statycznych funkcji wstawianych jako argumentów szablonu. Wciąż daleko w porównaniu z makrami Lispa z lat 70., ale C ++ 11 zdecydowanie rozwijał się przez lata.
pfalcon
5
skoro c ++ 11 nie byłoby lepiej wziąć tę funkcję jako odwołanie do wartości rvalue ( template <typename F> void doOperation(F&& f) {/**/}), więc na przykład bind może przekazać wyrażenie wiązania zamiast go powiązać?
user1810087
70

Parametry szablonu można sparametryzować według typu (nazwa typu T) lub wartości (int X).

„Tradycyjnym” sposobem szablonów fragmentu kodu w C ++ jest użycie funktora - tzn. Kod znajduje się w obiekcie, a zatem obiekt nadaje unikalny typ kodowi.

Podczas pracy z tradycyjnymi funkcjami ta technika nie działa dobrze, ponieważ zmiana typu nie wskazuje konkretnej funkcji - raczej określa jedynie sygnaturę wielu możliwych funkcji. Wię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);

Nie jest odpowiednikiem przypadku funktora. W tym przykładzie instancja do_op jest tworzona dla wszystkich wskaźników funkcji, których sygnaturą jest int X (int, int). Kompilator musiałby być dość agresywny, aby w pełni uwzględnić tę sprawę. (Nie wykluczyłbym tego, ponieważ optymalizacja kompilatora stała się bardzo zaawansowana.)

Jednym ze sposobów stwierdzenia, że ​​ten kod nie robi dokładnie tego, czego chcemy, jest:

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

jest nadal legalne i wyraźnie nie jest to podkreślane. Aby uzyskać pełne wstawianie, musimy utworzyć szablon według wartości, aby funkcja była w pełni dostępna w szablonie.

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

W takim przypadku każda utworzona instancja do_op jest tworzona z określoną dostępną funkcją. Dlatego spodziewamy się, że kod do_op będzie przypominał „return a + b”. (Programiści Lisp, przestańcie się śmiać!)

Możemy również potwierdzić, że jest to bliższe temu, czego chcemy, ponieważ:

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

nie uda się skompilować. GCC mówi: „błąd: 'func_ptr' nie może pojawić się w wyrażeniu stałym. Innymi słowy, nie mogę w pełni rozwinąć do_op, ponieważ nie dostarczyłeś mi wystarczająco dużo informacji w czasie kompilatora, aby wiedzieć, co to jest nasza operacja.

Więc jeśli drugi przykład naprawdę w pełni uwzględnia naszą operację, a pierwszy nie, to po co szablon? Co to robi? Odpowiedź brzmi: wpisz przymus. Ten riff na pierwszym przykładzie będzie działał:

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

Ten przykład zadziała! (Nie sugeruję, że jest dobry C ++, ale ...) Co się stało jest do_op został matrycy wokół podpisy różnych funkcji, a każdy oddzielny instancji napisze inny kod rodzaj przymusu. Tak więc instancyjny kod do_op z fadd wygląda mniej więcej tak:

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.

Dla porównania, nasz przypadek wartości wymaga dokładnego dopasowania argumentów funkcji.

Ben Supnik
źródło
2
Zobacz stackoverflow.com/questions/13674935/..., aby uzyskać dalsze pytanie w bezpośredniej odpowiedzi na obserwację tutaj, która int c = do_op(4,5,func_ptr);„wyraźnie nie jest wprowadzana”.
Dan Nissenbaum,
Zobacz tutaj przykład inline: stackoverflow.com/questions/4860762/... Wygląda na to, że kompilatory stają się dość inteligentne w dzisiejszych czasach.
BigSandwich,
15

Wskaźniki funkcji mogą być przekazywane jako parametry szablonu i jest to część standardowego C ++ . Jednak w szablonie są one zadeklarowane i używane jako funkcje zamiast wskaźnika do funkcji. Podczas tworzenia szablonu podaje się adres funkcji, a nie tylko nazwę.

Na przykład:

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

Jeśli chcesz przekazać typ funktora jako argument szablonu:

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

Kilka odpowiedzi przekazuje instancję funktora jako argument:

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

Najbliżej tego jednolitego wyglądu za pomocą argumentu szablonu można zdefiniować do_opdwa razy - raz parametrem innym niż typ i raz parametrem typu.

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

Szczerze mówiąc, naprawdę nie spodziewałem się, że to się nie skompiluje, ale zadziałało to dla mnie z gcc-4.8 i Visual Studio 2013.

Kietz
źródło
9

W twoim szablonie

template <void (*T)(int &)>
void doOperation()

Ten parametr Tjest nietypowym parametrem szablonu. Oznacza to, że zachowanie funkcji szablonu zmienia się wraz z wartością parametru (który musi zostać ustalony w czasie kompilacji, jakie są stałe wskaźnika funkcji).

Jeśli chcesz czegoś, co działa zarówno z obiektami funkcji, jak i parametrami funkcji, potrzebujesz szablonu maszynowego. Jednak po wykonaniu tej czynności należy również dostarczyć instancję obiektu (instancję obiektu funkcji lub wskaźnik funkcji) do funkcji w czasie wykonywania.

template <class T>
void doOperation(T t)
{
  int temp=0;
  t(temp);
  std::cout << "Result is " << temp << std::endl;
}

Istnieje kilka drobnych uwag dotyczących wydajności. Ta nowa wersja może być mniej wydajna z argumentami wskaźnika funkcji, ponieważ konkretny wskaźnik funkcji jest tylko wyłączony i wywoływany w czasie wykonywania, podczas gdy szablon wskaźnika funkcji można zoptymalizować (ewentualnie wbudowane wywołanie funkcji) w oparciu o konkretny zastosowany wskaźnik funkcji. Obiekty funkcyjne mogą być często bardzo skutecznie rozszerzane za pomocą szablonu maszynowego, jednak o szczegółach operator()decyduje typ obiektu funkcyjnego.

CB Bailey
źródło
1

Twój przykład funktora nie działa dlatego, że potrzebujesz instancji, aby wywołać funkcję operator().

Nikołaj Fetissow
źródło
0

Edycja: Przekazywanie operatora jako odwołania nie działa. Dla uproszczenia zrozum go jako wskaźnik funkcji. Po prostu wysyłasz wskaźnik, a nie referencję. Myślę, że próbujesz napisać coś takiego.

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;
ARAK
źródło