
SPWorley
Ich suche nach den Regeln, die das Übergeben von C++-Vorlagenfunktionen als Argumente beinhalten.
Dies wird von C++ unterstützt, wie hier ein Beispiel 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. Googeln nach “Funktion als Vorlagenargument” bringt nicht viel. Und das klassische C++ Templates The Complete Guide geht überraschenderweise auch nicht darauf ein (zumindest nicht aus meiner Suche).
Die Fragen, die ich habe, sind, ob dies gültiges C++ ist (oder nur eine weithin unterstützte Erweiterung).
Gibt es auch eine Möglichkeit, einen Funktor mit derselben Signatur während dieser Art von Vorlagenaufruf austauschbar mit expliziten Funktionen zu verwenden?
Folgendes tut es nicht Arbeit in dem oben genannten Programm, zumindest in Visual C++, weil die Syntax offensichtlich falsch ist. Es wäre schön, eine Funktion für einen Funktor austauschen 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>();
Verweise auf ein oder zwei Weblinks oder eine Seite im C++-Vorlagenbuch wären willkommen!

jalf
Ja, es ist gültig.
Damit es auch mit Funktoren funktioniert, sieht die übliche Lösung stattdessen so aus:
template <typename F>
void doOperation(F f)
{
int temp=0;
f(temp);
std::cout << "Result is " << temp << std::endl;
}
die jetzt aufgerufen werden kann als:
doOperation(add2);
doOperation(add3());
Sehen Sie es live
Das Problem dabei ist, dass es für den Compiler schwierig wird, den Aufruf zu inline add2
da der Compiler nur weiß, dass es sich um einen Funktionszeigertyp handelt void (*)(int &)
wird weitergegeben doOperation
. (Aber add3
, da es sich um einen Funktor handelt, kann leicht inliniert werden. Hier weiß der Compiler, dass ein Objekt vom Typ add3
an die Funktion übergeben wird, was bedeutet, dass die Funktion aufgerufen werden soll add3::operator()
und nicht nur ein unbekannter Funktionszeiger.)
Vorlagenparameter können entweder nach Typ (typename T) oder nach Wert (int X) parametrisiert werden.
Die “traditionelle” C++-Art, einen Codeabschnitt mit Vorlagen zu versehen, 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 traditionellen Funktionen funktioniert diese Technik nicht gut, da eine Änderung des Typs nicht auf a hinweist Spezifisch Funktion – sondern spezifiziert nur die Signatur vieler möglicher Funktionen. 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 Fall des Funktors. 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 inlinen. (Ich würde es jedoch nicht ausschließen, da die Compiler-Optimierung ziemlich weit fortgeschritten ist.)
Eine Möglichkeit zu sagen, 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 inliniert. Um vollständiges Inlining zu erhalten, müssen wir die 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 bereits verfügbaren spezifischen Funktion instanziiert. Daher erwarten wir, dass der Code für do_op sehr ähnlich aussieht wie “return a + b”. (Programmierer, hör auf zu grinsen!)
Wir können auch bestätigen, dass dies näher an dem liegt, was wir wollen, denn 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.
Wenn also das zweite Beispiel unsere Operation wirklich vollständig einbettet und das erste nicht, was nützt dann die Vorlage? Was macht es? Die Antwort lautet: Geben Sie Zwang ein. Dieses Riff im ersten Beispiel funktioniert:
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 herum erstellt wurde Unterschriften der verschiedenen Funktionen, und jede separate Instanziierung schreibt einen anderen Typ von Zwangscode. Der instanziierte Code für do_op mit fadd sieht also etwa 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 exakte Übereinstimmung der Funktionsargumente.

Kietz
Funktionszeiger können als Vorlagenparameter übergeben werden, und dies ist Teil des Standard-C++
. In der Vorlage werden sie jedoch als Funktionen und nicht als Zeiger auf Funktionen deklariert und verwendet. Bei Vorlage Instanziierung Man übergibt die Adresse der Funktion und nicht nur den Namen.
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 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);
Am nächsten kommt man diesem einheitlichen Erscheinungsbild mit einem Template-Argument, indem man definiert do_op
zweimal – einmal mit einem Nicht-Typ-Parameter und einmal mit einem Typ-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, ich Ja wirklich Ich hatte erwartet, dass dies nicht kompiliert wird, aber es hat bei mir mit gcc-4.8 und Visual Studio 2013 funktioniert.
In deiner Vorlage
template <void (*T)(int &)>
void doOperation()
Der Parameter T
ist ein Vorlagenparameter, der kein Typ ist. Dies bedeutet, dass sich das Verhalten der Vorlagenfunktion mit dem Wert des Parameters ändert (der zur Kompilierzeit festgelegt werden muss, welche Funktionszeigerkonstanten sind).
Wenn Sie etwas wollen, das sowohl mit Funktionsobjekten als auch mit Funktionsparametern funktioniert, benötigen Sie eine typisierte Vorlage. Wenn Sie dies tun, müssen Sie der Funktion jedoch 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 kleinere Überlegungen zur Leistung. Diese neue Version ist möglicherweise weniger effizient mit Funktionszeiger-Argumenten, da der bestimmte Funktionszeiger nur derefferiert und zur Laufzeit aufgerufen wird, während Ihre Funktionszeiger-Vorlage basierend auf dem bestimmten verwendeten Funktionszeiger optimiert werden kann (möglicherweise der Funktionsaufruf inline). Funktionsobjekte können oft sehr effizient mit der typisierten Vorlage erweitert werden, allerdings als das Besondere operator()
wird vollständig durch den Typ des Funktionsobjekts bestimmt.
Der Grund, warum Ihr Funktor-Beispiel nicht funktioniert, ist, dass Sie eine Instanz benötigen, um die aufzurufen operator()
.
Kam hier mit der zusätzlichen Anforderung, dass auch Parameter/Rückgabetypen variieren sollten. Nach Ben Supnik wäre dies für einen Typ T
typedef T(*binary_T_op)(T, T);
anstatt
typedef int(*binary_int_op)(int, int);
Die Lösung besteht hier darin, die Funktionstypdefinition und das Funktions-Template in ein umgebendes Struct-Template zu packen.
template <typename T> struct BinOp
{
typedef T(*binary_T_op )(T, T); // signature for all valid template params
template<binary_T_op op>
T do_op(T a, T b)
{
return op(a,b);
}
};
double mulDouble(double a, double b)
{
return a * b;
}
BinOp<double> doubleBinOp;
double res = doubleBinOp.do_op<&mulDouble>(4, 5);
Alternativ könnte BinOp eine Klasse mit statischem Methoden-Template do_op(…) sein, die dann als aufgerufen wird
double res = BinOp<double>::do_op<&mulDouble>(4, 5);
BEARBEITEN
Inspiriert durch den Kommentar von 0x2207 ist hier ein Funktor, der eine beliebige Funktion mit zwei Parametern und konvertierbaren Werten übernimmt.
struct BinOp
{
template <typename R, typename S, typename T, typename U, typename V> R operator()(R (*binaryOp )(S, T), U u, V v)
{
return binaryOp(u,v);
}
};
double subD(double a, int b)
{
return a-b;
}
int subI(double a, int b)
{
return (int)(a-b);
}
int main()
{
double resD = BinOp()(&subD, 4.03, 3);
int resI = BinOp()(&subI, 4.03, 3);
std::cout << resD << std::endl;
std::cout << resI << std::endl;
return 0;
}
richtig auswertet doppelt 1.03 und int 1

Ravi
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 glaube, 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;
9880700cookie-checkAls Template-Argument übergebene Funktionyes
Was ist der Vorteil einer Funktion als Template-Argument? Würde der Rückgabetyp nicht als Vorlagentyp verwendet werden?
– DaClown
23. Juli 2009 um 21:04 Uhr
Verwandt: Ein Lambda ohne Captures kann zu einem Funktionszeiger zerfallen, und Sie können das als Template-Parameter in C++17 übergeben. Clang kompiliert es in Ordnung, aber der aktuelle gcc (8.2) hat einen Fehler und weist ihn sogar mit fälschlicherweise als “keine Verknüpfung” zurück
-std=gnu++17
. Kann ich das Ergebnis eines C++17 Captureless Lambda constexpr Konvertierungsoperators als Nicht-Typ-Argument einer Funktionszeigervorlage verwenden?– Peter Cordes
7. August 2018 um 22:41 Uhr