it-swarm.com.de

Funktion als Template-Argument übergeben

Ich suche nach den Regeln, die das Übergeben von C++ - Vorlagen als Argumente beinhalten.

Dies wird von C++ unterstützt, wie ein Beispiel hier zeigt:

#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. Nach "Funktion als Template-Argument" googeln führt nicht zu viel. Und das klassische C++ Templates The Complete Guide diskutiert es überraschenderweise auch nicht (zumindest nicht aus meiner Suche).

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

Gibt es eine Möglichkeit, zuzulassen, dass ein Funktor mit derselben Signatur mit expliziten Funktionen während eines solchen Vorlagenaufrufs austauschbar verwendet wird?

Das Folgende funktioniert im obigen Programm nicht , 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 einen Funktor an den Algorithmus std :: sort übergeben können, wenn Sie eine benutzerdefinierte Vergleichsoperation definieren möchten.

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

    doOperation<add3>();

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

215
SPWorley

Ja, das ist gültig.

Die übliche Lösung, damit es auch mit Funktoren funktioniert, sieht stattdessen so aus:

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

die kann jetzt aufgerufen werden als entweder:

_doOperation(add2);
doOperation(add3());
_

Das Problem dabei ist, dass, wenn es für den Compiler schwierig ist, den Aufruf von _add2_ zu integrieren, der Compiler nur weiß, dass ein Funktionszeigertyp void (*)(int &) an doOperation. (Aber _add3_ als Funktor kann leicht eingefügt werden. Hier weiß der Compiler, dass ein Objekt vom Typ _add3_ an die Funktion übergeben wird, was bedeutet, dass die aufzurufende Funktion add3::operator() und nicht irgendein unbekannter Funktionszeiger.)

120
jalf

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

Die "traditionelle" C++ - Methode zum Schablonieren eines Codeteils besteht in der Verwendung eines Funktors - 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 nicht auf eine bestimmte - Funktion hinweist, sondern nur die Signatur vieler möglicher Funktionen angibt. Damit:

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 Funktorkoffer. 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 inline zu setzen. (Ich würde es jedoch nicht ausschließen, da die Compiler-Optimierung ziemlich weit fortgeschritten ist.)

Ein Weg zu sagen, dass dieser Code nicht genau 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 eingebunden. Um ein vollständiges Inlining zu erhalten, müssen Sie 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 bereits verfügbaren Funktion instanziiert. Wir erwarten daher, dass der Code für do_op ungefähr so ​​aussieht wie "return a + b". (LISP-Programmierer, hör auf zu grinsen!)

Wir können auch bestätigen, dass dies näher an dem ist, 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 zur Compilerzeit 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: Typenzwang. 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 schlage nicht vor, dass es gut C++ ist, aber ...) Was passiert ist, ist, dass do_op um das Signaturen der verschiedenen Funktionen herum vorgedacht wurde, und jede separate Instanziierung wird einen anderen Typ-Zwangscode schreiben. 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 der Funktionsargumente.

64
Ben Supnik

Funktionszeiger können als Template-Parameter übergeben werden, und dies ist Teil des Standard-C++ . In der Vorlage werden sie jedoch als Funktionen deklariert und verwendet, anstatt als Zeiger auf eine Funktion. Bei der Template Instanziierung wird nicht nur der Name, sondern die Adresse der Funktion übergeben.

Zum Beispiel:

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 Funktortyp 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 Funktionsinstanz 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 Template-Argument können Sie dieses einheitliche Erscheinungsbild am ehesten erreichen, indem Sie do_op zweimal - einmal mit einem nicht typisierten Parameter und einmal mit einem typisierten Parameter.

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

15
Kietz

In Ihrer Vorlage

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

Der Parameter T ist ein nicht typisierter Vorlagenparameter. Dies bedeutet, dass sich das Verhalten der Template-Funktion mit dem Wert des Parameters ändert (der zur Kompilierungszeit festgelegt werden muss, welche Funktionszeiger-Konstanten vorliegen).

Wenn Sie etwas möchten, das sowohl mit Funktionsobjekten als auch mit Funktionsparametern funktioniert, benötigen Sie eine typisierte Vorlage. Wenn Sie dies jedoch 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 Leistungsaspekte. Diese neue Version ist bei Funktionszeigerargumenten möglicherweise weniger effizient, da der bestimmte Funktionszeiger nur zur Laufzeit dereferenziert und aufgerufen wird, während Ihre Funktionszeigerschablone basierend auf dem verwendeten Funktionszeiger optimiert werden kann (möglicherweise der Funktionsaufruf inline). Funktionsobjekte können mit der typisierten Vorlage häufig sehr effizient erweitert werden, da das jeweilige operator() vollständig durch den Typ des Funktionsobjekts bestimmt wird.

8
CB Bailey

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

1

Bearbeiten: Das Übergeben des Operators als Referenz funktioniert nicht. Verstehen Sie es der Einfachheit halber als einen Funktionszeiger. Sie senden nur den Zeiger, keine Referenz. Ich denke du versuchst 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;
0
AraK