Funktion als Vorlagenargument übergeben

224

Ich suche nach den Regeln, nach denen C ++ - Vorlagen als Argumente übergeben werden.

Dies wird von C ++ unterstützt, wie in einem Beispiel hier gezeigt:

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

Das Erlernen dieser Technik ist jedoch schwierig. Das Googeln nach "Funktion als Vorlagenargument" führt nicht zu viel. Und die klassischen C ++ - Vorlagen The Complete Guide diskutieren es überraschenderweise auch nicht (zumindest nicht aus meiner Suche).

Die Fragen, die ich habe, sind, ob dies gültiges C ++ ist (oder nur eine weit verbreitete Erweiterung).

Gibt es auch eine Möglichkeit, einen Funktor mit derselben Signatur während dieser Art des Vorlagenaufrufs austauschbar mit expliziten Funktionen zu verwenden?

Die folgende tut nicht Arbeit in dem obigen Programm, zumindest in Visual C ++ , da die Syntax offensichtlich falsch ist. Es wäre schön, eine Funktion für einen Funktor ausschalten zu können und umgekehrt, ähnlich wie Sie einen Funktionszeiger oder Funktor an den std :: sort-Algorithmus übergeben können, wenn Sie eine benutzerdefinierte Vergleichsoperation definieren möchten.

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

    doOperation<add3>();

Hinweise auf einen oder zwei Weblinks oder eine Seite im C ++ - Vorlagenbuch sind willkommen!

SPWorley
quelle
Was ist der Vorteil einer Funktion als Vorlagenargument? Würde der Rückgabetyp nicht als Vorlagentyp verwendet?
DaClown
Verwandte: Ein Lambda ohne Captures kann in einen Funktionszeiger zerfallen, und Sie können dies als Vorlagenparameter in C ++ 17 übergeben. Clang kompiliert es in Ordnung, aber das aktuelle gcc (8.2) hat einen Fehler und weist es fälschlicherweise als "keine Verknüpfung" zurück, selbst mit -std=gnu++17. Kann ich das Ergebnis eines Capture-freien Lambda-Constexpr-Konvertierungsoperators in C ++ 17 als nicht typisiertes Argument für eine Funktionszeigervorlage verwenden? .
Peter Cordes

Antworten:

123

Ja, es ist gültig.

Die übliche Lösung ist, dass es auch mit Funktoren funktioniert:

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

was nun als entweder bezeichnet werden kann:

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

Sehen Sie es live

Das Problem dabei ist, dass, wenn es für den Compiler schwierig ist, den Aufruf zu inline add2, da der Compiler nur weiß, dass ein Funktionszeigertyp void (*)(int &)übergeben wird doOperation. (Als add3Funktor kann er jedoch leicht eingefügt werden. Hier weiß der Compiler, dass ein Objekt vom Typ add3an die Funktion übergeben wird, was bedeutet, dass die aufzurufende Funktion add3::operator()nicht nur ein unbekannter Funktionszeiger ist.)

jalf
quelle
19
Hier ist eine interessante Frage. Wenn ein Funktionsname übergeben wird, ist es NICHT so, als wäre ein Funktionszeiger beteiligt. Es ist eine explizite Funktion, die zur Kompilierungszeit angegeben wird. Der Compiler weiß also genau, was er zur Kompilierungszeit hat.
SPWorley
1
Die Verwendung von Funktoren gegenüber Funktionszeigern hat einen Vorteil. Der Funktor kann innerhalb der Klasse instanziiert werden und bietet dem Compiler somit mehr Möglichkeiten für Optimierungen (z. B. Inlining). Dem Compiler würde es schwer fallen, einen Aufruf über einen Funktionszeiger zu optimieren.
Martin York
11
Wenn die Funktion in einem Vorlagenparameter verwendet wird, zerfällt sie in einen Zeiger auf die übergebene Funktion. Es ist analog dazu, wie Arrays in Zeiger zerfallen, wenn sie als Argumente an Parameter übergeben werden. Natürlich ist der Zeigerwert zur Kompilierungszeit bekannt und muss auf eine Funktion mit externer Verknüpfung verweisen, damit der Compiler diese Informationen für Optimierungszwecke verwenden kann.
CB Bailey
5
Schneller Vorlauf bis einige Jahre später: Die Situation mit der Verwendung von Funktionen als Vorlagenargumente hat sich in C ++ 11 erheblich verbessert. Sie müssen Javaismen wie Funktorklassen nicht mehr verwenden und können beispielsweise statische Inline-Funktionen direkt als Vorlagenargumente verwenden. Im Vergleich zu Lisp-Makros der 1970er Jahre noch weit entfernt, aber C ++ 11 hat im Laufe der Jahre definitiv gute Fortschritte gemacht.
Pfalcon
5
Da es in c ++ 11 nicht besser wäre, die Funktion als rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}) zu verwenden, kann bind beispielsweise einen bind-Ausdruck übergeben, anstatt ihn zu binden.
user1810087
70

Vorlagenparameter können entweder nach Typ (Typname T) oder nach Wert (int X) parametrisiert werden.

Die "traditionelle" C ++ - Methode zum Vorlagen eines Codeteils besteht darin, einen Funktor zu verwenden - das heißt, der Code befindet sich in einem Objekt, und das Objekt gibt dem Code somit einen eindeutigen Typ.

Bei der Arbeit mit herkömmlichen Funktionen funktioniert diese Technik nicht gut, da eine Änderung des Typs keine bestimmte Funktion anzeigt, sondern nur die Signatur vieler möglicher Funktionen angibt. So:

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

Entspricht nicht dem Funktorfall. In diesem Beispiel wird do_op für alle Funktionszeiger instanziiert, deren Signatur int X (int, int) ist. Der Compiler müsste ziemlich aggressiv sein, um diesen Fall vollständig zu integrieren. (Ich würde es jedoch nicht ausschließen, da die Compileroptimierung ziemlich weit fortgeschritten ist.)

Eine Möglichkeit zu erkennen, dass dieser Code nicht ganz das tut, was wir wollen, ist:

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

ist immer noch legal, und dies wird eindeutig nicht inline. Um ein vollständiges Inlining zu erhalten, müssen wir eine Vorlage nach Wert erstellen, damit die Funktion in der Vorlage vollständig verfügbar ist.

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

In diesem Fall wird jede instanziierte Version von do_op mit einer bestimmten Funktion instanziiert, die bereits verfügbar ist. Wir erwarten daher, dass der Code für do_op sehr nach "return a + b" aussieht. (Lisp-Programmierer, hör auf zu grinsen!)

Wir können auch bestätigen, dass dies näher an dem liegt, was wir wollen, weil dies:

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

wird nicht kompiliert. GCC sagt: "Fehler: 'func_ptr' kann nicht in einem konstanten Ausdruck erscheinen. Mit anderen Worten, ich kann do_op nicht vollständig erweitern, weil Sie mir zum Zeitpunkt des Compilers nicht genügend Informationen gegeben haben, um zu wissen, was unsere Operation ist.

Also, wenn das zweite Beispiel unsere Operation wirklich vollständig einfügt und das erste nicht, was nützt die Vorlage? Was macht es? Die Antwort lautet: Typ Zwang. Dieses Riff im ersten Beispiel wird funktionieren:

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

Dieses Beispiel wird funktionieren! (Ich behaupte nicht, dass es gutes C ++ ist, aber ...) Was passiert ist, ist, dass do_op um die Signaturen der verschiedenen Funktionen herum erstellt wurde und jede separate Instanziierung einen anderen Typ von Zwangscode schreibt. Der instanziierte Code für do_op mit fadd sieht also ungefähr so ​​aus:

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.

Im Vergleich dazu erfordert unser By-Value-Fall eine genaue Übereinstimmung mit den Funktionsargumenten.

Ben Supnik
quelle
2
Unter stackoverflow.com/questions/13674935/… finden Sie eine Folgefrage als direkte Antwort auf die Beobachtung hier, int c = do_op(4,5,func_ptr);die "eindeutig nicht inline" wird.
Dan Nissenbaum
Ein Beispiel hierfür finden Sie hier: stackoverflow.com/questions/4860762/… Scheint, dass Compiler heutzutage ziemlich schlau werden.
BigSandwich
15

Funktionszeiger können als Vorlagenparameter übergeben werden. Dies ist Teil von Standard-C ++ . In der Vorlage werden sie jedoch deklariert und als Funktionen und nicht als Zeiger auf Funktion verwendet. Bei Vorlage Instanziierung passiert man die Adresse der Funktion und nicht nur den Namen.

Beispielsweise:

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

Wenn Sie einen Funktionstyp als Vorlagenargument übergeben möchten:

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

Mehrere Antworten übergeben eine Funktorinstanz als 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);

Mit einem Vorlagenargument können Sie diesem einheitlichen Erscheinungsbild am nächsten kommen, do_opindem Sie es zweimal definieren - einmal mit einem Nicht-Typparameter und einmal mit einem Typparameter.

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

Ehrlich gesagt hatte ich wirklich erwartet, dass dies nicht kompiliert wird, aber es funktionierte für mich mit gcc-4.8 und Visual Studio 2013.

Kietz
quelle
9

In Ihrer Vorlage

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

Der Parameter Tist ein nicht typisierter Vorlagenparameter. Dies bedeutet, dass sich das Verhalten der Vorlagenfunktion mit dem Wert des Parameters ändert (der zur Kompilierungszeit festgelegt werden muss, welche Funktionszeigerkonstanten sind).

Wenn Sie etwas möchten, das sowohl mit Funktionsobjekten als auch mit Funktionsparametern funktioniert, benötigen Sie eine typisierte Vorlage. Wenn Sie dies tun, müssen Sie der Funktion zur Laufzeit auch eine Objektinstanz (entweder eine Funktionsobjektinstanz oder einen Funktionszeiger) bereitstellen.

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

Es gibt einige geringfügige Leistungsüberlegungen. Diese neue Version ist möglicherweise weniger effizient mit Funktionszeigerargumenten, da der bestimmte Funktionszeiger nur zur Laufzeit dereferenziert und aufgerufen wird, während Ihre Funktionszeigervorlage basierend auf dem bestimmten verwendeten Funktionszeiger optimiert werden kann (möglicherweise der Funktionsaufruf inline). Funktionsobjekte können mit der typisierten Vorlage oft sehr effizient erweitert werden, da das Besondere operator()jedoch vollständig vom Typ des Funktionsobjekts abhängt.

CB Bailey
quelle
1

Der Grund, warum Ihr Funktionsbeispiel nicht funktioniert, ist, dass Sie eine Instanz benötigen, um das aufzurufen operator().

Nikolai Fetissov
quelle
0

Bearbeiten: Das Übergeben des Operators als Referenz funktioniert nicht. Verstehen Sie es der Einfachheit halber als Funktionszeiger. Sie senden nur den Zeiger, keine Referenz. Ich denke, Sie versuchen so etwas zu schreiben.

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
quelle