2015-01-09 11 views
5

Die Aufgabe besteht darin, eine einzige Argumentfunktion zu erstellen, die alle Typen neben einem (Foo) weiterleitet, den es konvertiert (in Bar).Selektive Forwarding-Funktion

(Nehmen wir an, es gibt eine Konvertierung von Foo nach Bar). Hier

ist das Nutzungsszenario:

template<typename Args...> 
void f(Args... args) 
{ 
    g(process<Args>(args)...); 
} 

(. Ich habe versucht zu extrahieren/vereinfachen es aus dem ursprünglichen Kontext here - wenn ich einen Fehler gemacht habe, bitte jemand mir sagen!)

Hier sind zwei mögliche Implementierungen:

template<typename T> 
T&& process(T&& t) { 
    return std::forward<T>(t); 
} 

Bar process(Foo x) { 
    return Bar{x}; 
} 

Und ...

template <typename T, typename U> 
T&& process(U&& u) { 
    return std::forward<T>(std::forward<U>(u)); 
} 

template <typename T> 
Bar process(Foo x) { 
    return Bar{x}; 
} 

Ich habe es auf gute Autorität (here), dass die zweite vorzuziehen ist.

Allerdings kann ich die gegebene Erklärung nicht verstehen. Ich denke, dass dies in einige der dunkelsten Ecken von C++ eintaucht.

Ich denke, ich vermisse Maschinen benötigt, um zu verstehen, was vor sich geht. Könnte jemand das im Detail erklären? Wenn es zu viel Graben ist, könnte jemand eine Ressource empfehlen, um die notwendigen Voraussetzungen zu lernen?

EDIT: Ich möchte hinzufügen, dass in meinem speziellen Fall die Funktion Signatur wird eine der typedef-s auf this page entsprechen. Das heißt, jedes Argument wird entweder PyObject* (wobei PyObject eine gewöhnliche C-Struktur ist) oder ein einfacher C-Typ wie const char*, int, float sein. Also meine Vermutung ist, dass die leichte Implementierung am besten geeignet ist (ich bin kein Fan von Über-Generalisierung). Aber ich bin wirklich daran interessiert, die richtige Einstellung zu finden, um solche Probleme zu lösen.

+2

In diesen Beispielen fehlt der Kontext, in dem die Prozessfunktion aufgerufen wird - immer mit einem expliziten Typ-Template-Argument (zumindest war das die Idee, Nicht-Lvalue-Referenzen in xvalues ​​zu verwandeln). Sie können mit Ihrer Version kommen, aber es wird immer copy ctor beim Initialisieren von Parametern der Zielfunktion aufrufen –

+0

Ich stimme zu, dass Version 1 irgendwie "schmutzig" ist, weil es eine Überladung von nicht-gestützten Funktionen über eine Vorlage ist und sie unterscheiden sich nur nach Rückgabetyp. Aber für Version 2 kann ich keinen Sinn erkennen. Sie müssten den ersten Template-Parameter ohnehin immer angeben, da der Compiler ihn in einem Verwendungskontext nicht ableiten kann. –

+0

@PiotrS., Ich habe bearbeitet, um einen Kontext bereitzustellen (ich denke, es entspricht meinem tatsächlichen Anwendungsfall). Dank Ihrer unschätzbaren Hilfe in den letzten Tagen habe ich jetzt eine funktionierende Lösung ([hier] (http://stackoverflow.com/q/27866483/435129)). Dies ist die letzte Komponente, an der ich kaue, weil ich nicht gerne Code einbeziehe, den ich nicht verstehe. –

Antwort

2

Ich spüre ein geringes Missverständnis in Ihrem Verständnis des Anwendungsfalls Sie konfrontiert sind.

Zunächst einmal ist dies eine Funktionsvorlage:

struct A 
{ 
    template <typename... Args> 
    void f(Args... args) 
    { 
    } 
}; 

Und dies ist keine Funktion Vorlage:

template <typename... Args> 
struct A 
{ 
    void f(Args... args) 
    { 
    } 
}; 

Im ersteren Definition (mit einer Funktion Vorlage) die Art Argumente Abzug stattfinden. Im letzteren Fall gibt es keinen Typabzug.

Sie verwenden keine Funktionsvorlage. Sie verwenden eine Nicht-Template-Member-Funktion aus einer Klassenvorlage, und für diese bestimmte Member-Funktion ist ihre Signatur festgelegt.

Durch Ihre trap Klasse definiert, wie unten:

template <typename T, T t> 
struct trap; 

template <typename R, typename... Args, R(Base::*t)(Args...)> 
struct trap<R(Base::*)(Args...), t> 
{  
    static R call(Args... args); 
}; 

und unter Hinweis auf seine Funktionselement wie unten:

&trap<decltype(&Base::target), &Base::target>::call; 

Sie mit einem Zeiger auf einen statischen Nicht-Template call Funktion am Ende mit eine feste Signatur, identisch mit der Signatur der target-Funktion.

Nun dient diese call Funktion als ein intermediate Invoker. Sie werden die call Funktion anrufen, und diese Funktion ruft die target Member-Funktion, seine eigenen Argumente zu übergeben target ‚s Parameter zu initialisieren, sagen:

template <typename R, typename... Args, R(Base::*t)(Args...)> 
struct trap<R(Base::*)(Args...), t> 
{  
    static R call(Args... args) 
    { 
     return (get_base()->*t)(args...); 
    } 
}; 

die target Funktion Angenommen, die trap Klassenvorlage verwendet zu instanziiert ist wie folgt definiert:

struct Base 
{ 
    int target(Noisy& a, Noisy b); 
}; 

Durch Instanziierung die trap Klasse, die Sie mit der folgenden call Funktion am Ende:

// what the compiler *sees* 
static int call(Noisy& a, Noisy b) 
{ 
    return get_base()->target(a, b); 
} 

Glücklicherweise adurch Bezugnahme geben wird, wird es nur durch dieselbe Art von Referenz in der target ‚s-Parameter weitergeleitet und gebunden. Leider ist dies nicht gilt für die b Objekt - egal ob die Noisy Klasse beweglich ist oder nicht, werden Sie mehrere Kopien der b Instanz zu machen, denn das ist ein Wert übergeben wird:

  • der erste: wenn die call-Funktion selbst aus einem externen Kontext aufgerufen wird.

  • der zweite: Kopieren b Instanz beim Aufruf der target Funktion aus dem Körper von call.

DEMO 1

Dies ist etwas ineffizient: Sie mindestens eine Kopie-Konstruktoraufruf gespeichert haben könnte, sie in einen Zug-Konstruktoraufruf drehen, wenn Sie nur die b Instanz in ein xValue verwandeln konnte :

Jetzt würde es stattdessen einen Move Constructor für den zweiten Parameter aufrufen.

So weit so gut, aber das wurde manuell gemacht (std::move hinzugefügt in dem Wissen, dass es sicher ist, die Bewegung Semantik anzuwenden).Nun ist die Frage, wie könnte die gleiche Funktionalität angewendet werden, wenn auf einem Parameter Pack Betrieb:

return get_base()->target(std::move(args)...); // WRONG! 

Sie nicht std::move Aufruf an jeden und jedes Argument innerhalb des Parameters Pack anwenden können. Dies würde wahrscheinlich Compiler-Fehler verursachen, wenn sie auf alle Argumente gleichermaßen angewendet werden.

DEMO 2

Zum Glück, obwohl Args... ist kein Forwarding-Referenz, die std::forward Helferfunktion stattdessen verwendet werden können. Das heißt, je nachdem, was der <T> Typ ist in std::forward<T> (eine L-Wert-Referenz oder eine Nicht-L-Wert-Referenz) die std::forward verhält sich anders:

  • für lvalue Referenzen (beispielsweise wenn T ist Noisy&): Der Wert Die Kategorie des Ausdrucks bleibt ein lvalue (dh Noisy&).

  • für Nicht-L-Wert-Referenzen (z.B. wenn TNoisy&& oder ein ebenes Noisy ist): der Wert der Kategorie des Ausdrucks ein xValue wird (d.h. Noisy&&).

Nachdem das gesagt ist, durch Definieren der target Funktion wie unten:

static R call(Args... args) 
{ 
    return (get_base()->*t)(std::forward<Args>(args)...); 
} 

Sie am Ende mit:

static int call(Noisy& a, Noisy b) 
{ 
    // what the compiler *sees* 
    return get_base()->target(std::forward<Noisy&>(a), std::forward<Noisy>(b)); 
} 

den Wert Kategorie des Ausdrucks drehen b in eine xValue Beteiligung von b, die Noisy&& ist. Dadurch kann der Compiler den Verschiebungskonstruktor auswählen, um den zweiten Parameter der target-Funktion zu initialisieren, wobei a intakt bleibt.

DEMO 3(vergleiche die Ausgabe mit DEMO 1)

Im Grunde ist das, was die für std::forward ist. Normalerweise wird std::forward mit einer Weiterleitungsreferenz verwendet, wobei T den Typ hält, der nach den Regeln des Typenabzugs für Weiterleitungsreferenzen abgeleitet wird. Beachten Sie, dass es immer von Ihnen verlangt, den <T> Teil explizit zu übergeben, da es ein anderes Verhalten abhängig von diesem Typ anwendet (unabhängig von der Wertkategorie seines Arguments). Ohne das Template-Argument <T> des expliziten Typs würde std::forward immer Lvalue-Referenzen für Argumente ableiten, auf die durch ihre Namen verwiesen wird (wie beim Erweitern des Parameter-Packs).

Nun wollten Sie zusätzlich konvertieren Sie einige der Argumente von einem Typ zu einem anderen, während alle anderen weiterleiten.Wenn Sie nicht mit std::forward ing Argumente aus dem Parameterpaket über den Trick ist es egal, und es ist in Ordnung, eine Kopie-Konstruktor immer anrufen, dann Ihre Version ist OK:

template <typename T>   // transparent function 
T&& process(T&& t) { 
    return std::forward<T>(t); 
} 

Bar process(Foo x) {   // overload for specific type of arguments 
    return Bar{x}; 
} 

//... 
get_base()->target(process(args)...); 

DEMO 4

jedoch wenn Sie die Kopie dieses Noisy Argument in der Demo vermeiden wollen, müssen Sie irgendwie std::forward Anruf mit dem process Anruf, und Durchlauf über die Args Typen kombinieren, so dass std::forward richtiges Verhalten (t gelten könnte in xvalues ​​gehen oder nichts tun). Ich habe Ihnen nur ein einfaches Beispiel dafür gegeben, wie dies implementiert werden könnte:

template <typename T, typename U> 
T&& process(U&& u) { 
    return std::forward<T>(std::forward<U>(u)); 
} 

template <typename T> 
Bar process(Foo x) { 
    return Bar{x}; 
} 

//... 
get_base()->target(process<Args>(args)...); 

Aber das ist nur eine der Optionen. Es kann vereinfacht werden, neu geschrieben oder neu angeordnet werden, so dass std::forward aufgerufen wird, bevor Sie die process Funktion (Ihre Version) nennen:

get_base()->target(process(std::forward<Args>(args))...); 

DEMO 5

Und es (die Ausgabe mit DEMO 4 vergleichen) wird auch gut funktionieren (das heißt, mit Ihrer Version). Der Punkt ist, die zusätzliche std::forward ist nur, um Ihren Code ein wenig zu optimieren, und die provided idea war nur eine der möglichen Implementierungen dieser Funktionalität (wie Sie sehen können, bringt es den gleichen Effekt).

0

Wäre nicht der erste Teil der Version 2 ausreichend? nur:

template <typename T, typename U> 
T&& process(U&& u) { 
    return std::forward<T>(std::forward<U>(u)); 
} 

einen Anwendungsfall mit einem bestehenden Wandlungs- (Konstruktor für "Bar" von "Foo") gegeben, wie:

struct Foo { 
    int x; 
}; 
struct Bar { 
    int y; 
    Bar(Foo f) { 
     y = f.x; 
    } 
}; 
int main() { 

    auto b = process<Bar>(Foo()); // b will become a "Bar" 
    auto i = process<int>(1.5f); 
} 

Sie gezwungen sind, die ersten Template-Parameter (Typ angeben zu konvertieren) sowieso, weil der Compiler es nicht ableiten kann. Es weiß also, welchen Typ Sie erwarten und baut ein temporäres Objekt vom Typ "Bar", weil es einen Konstruktor gibt.

+0

aufgerufen. Die Frage war zunächst schlecht formuliert. Die Frage wurde geklärt und Ihre Antwort ist (leider) falsch. –