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 ++!


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


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.


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.


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.


1

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


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;
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.