Erweiterung des Variadic Template Packs

78

Ich versuche, verschiedene Vorlagen und Funktionen zu lernen. Ich kann nicht verstehen, warum dieser Code nicht kompiliert wird:

template<typename T>
static void bar(T t) {}

template<typename... Args>
static void foo2(Args... args)
{
    (bar(args)...);
}

int main()
{
    foo2(1, 2, 3, "3");
    return 0;    
}

Wenn ich kompiliere, schlägt es mit dem Fehler fehl:

Fehler C3520: 'args': Das Parameterpaket muss in diesem Zusammenhang erweitert werden

(in Funktion foo2).

Viacheslav Dronov
quelle
1
Was wäre, wenn Argses leer wäre?
Kaffee und Code

Antworten:

129

Einer der Orte, an denen eine Pack-Erweiterung auftreten kann, befindet sich in einer Klammer-Init-Liste . Sie können dies nutzen, indem Sie die Erweiterung in die Initialisierungsliste eines Dummy-Arrays einfügen:

template<typename... Args>
static void foo2(Args &&... args)
{
    int dummy[] = { 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
}

So erläutern Sie den Inhalt des Initialisierers ausführlicher:

{ 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
  |       |       |                        |     |
  |       |       |                        |     --- pack expand the whole thing 
  |       |       |                        |   
  |       |       --perfect forwarding     --- comma operator
  |       |
  |       -- cast to void to ensure that regardless of bar()'s return type
  |          the built-in comma operator is used rather than an overloaded one
  |
  ---ensure that the array has at least one element so that we don't try to make an
     illegal 0-length array when args is empty

Demo .

Ein wichtiger Vorteil der Erweiterung {}besteht darin, dass eine Bewertung von links nach rechts gewährleistet ist.


Mit C ++ 17- fachen Ausdrücken können Sie einfach schreiben

((void) bar(std::forward<Args>(args)), ...);
TC
quelle
19
Eine Variante, die ich gesehen habe, ist, using expander = int[]; expander{...};dass die Array-Variable dann keinen Namen hat und dem Compiler klar wird, dass das Array für mich nicht erstellt oder verwendet werden muss.
Mooing Duck
4
Wofür ist der zweite 0? Der kurz nach dem Komma-Operator
Aaron McDaid
4
@AaronMcDaid Damit der Typ des gesamten Ausdrucks intdem Elementtyp des Arrays entspricht.
TC
Mir ist aufgefallen, dass man ein schreiben kann operator void ()(ja, verrückt, ich weiß). Aber nein, ich sage nicht wirklich, dass Casting voideine schlechte Idee ist. Ich habe nur Spaß daran, über verrückte Dinge nachzudenken, die passieren können, wenn versucht wird, eine Packung mit minimalen Nebenwirkungen zu erweitern
Aaron McDaid,
2
@AaronMcDaid (void)ruftoperator void() (zum Glück) nie auf .
TC
40

Parameterpakete können nur in einer genau definierten Liste von Kontexten erweitert werden, und der Operator ,ist keiner von ihnen. Mit anderen Worten, es ist nicht möglich, die Pack-Erweiterung zu verwenden, um einen Ausdruck zu generieren, der aus einer Reihe von vom Operator begrenzten Unterausdrücken besteht ,.

Als Faustregel gilt: "Durch Erweiterung kann eine Liste mit ,getrennten Mustern erstellt werden, wobei ,ein Listenbegrenzer vorhanden ist ." Der Operator erstellt ,keine Liste im Sinne der Grammatik.

Um eine Funktion für jedes Argument aufzurufen, können Sie die Rekursion verwenden (dies ist das Hauptwerkzeug in der Box des Programmierers für verschiedene Vorlagen):

template <typename T>
void bar(T t) {}

void foo2() {}

template <typename Car, typename... Cdr>
void foo2(Car car, Cdr... cdr)
{
  bar(car);
  foo2(cdr...);
}

int main()
{
  foo2 (1, 2, 3, "3");
}

Live Beispiel

Angew ist nicht mehr stolz auf SO
quelle
2
Verdammt, Sie wetten nur, dass ich auf diese zitternde Faust antworte, aber Sie sollten Ihrer Antwort wahrscheinlich eine perfekte Weiterleitung hinzufügen. Das ist auch ein "primäres Werkzeug in der Box des variadischen Vorlagenprogrammierers".
Kaffee und Code
Vielen Dank für Ihre Antwort. Ich kenne die Implementierung von Rekursionen. Ich möchte nur eine Problemumgehung finden, um Code ohne Rekursion und neue Funktion zu kompilieren.
Viacheslav Dronov
1
@ViacheslavDronov sieht aus, als ob Sie Vorlagen verwenden: Sie haben bereits eine Menge Funktionen, die vom Compiler generiert werden. Warum nicht eine zu dieser Liste hinzufügen?
Kaffee und Code
@CoffeeandCode Ich erteile Ihnen offiziell die Erlaubnis, meine Antwort zu kopieren und sie durch Hinzufügen und Erklären der perfekten Weiterleitung zu erweitern.
Angew ist nicht mehr stolz auf SO
5
Sie können die Erweiterung problemlos mit einem Dummy-Array durchführen. int dummy[] = { 0, ((void) bar(std::forward<Args>(args)),0)... };.
TC
17

SHAMELESS COPY [von der Quelle genehmigt]

Parameterpakete können nur in einer genau definierten Liste von Kontexten erweitert werden, und der Operator ,ist keiner von ihnen. Mit anderen Worten, es ist nicht möglich, die Pack-Erweiterung zu verwenden, um einen Ausdruck zu generieren, der aus einer Reihe von vom Operator begrenzten Unterausdrücken besteht ,.

Als Faustregel gilt: "Durch Erweiterung kann eine Liste mit ,getrennten Mustern erstellt werden, wobei ,ein Listenbegrenzer vorhanden ist." Der Operator erstellt ,keine Liste im Sinne der Grammatik.

Um eine Funktion für jedes Argument aufzurufen, können Sie die Rekursion verwenden (dies ist das Hauptwerkzeug in der Box des Programmierers für verschiedene Vorlagen):

#include <utility>

template<typename T>
void foo(T &&t){}

template<typename Arg0, typename Arg1, typename ... Args>
void foo(Arg0 &&arg0, Arg1 &&arg1, Args &&... args){
    foo(std::forward<Arg0>(arg0));
    foo(std::forward<Arg1>(arg1), std::forward<Args>(args)...);
}

auto main() -> int{
    foo(1, 2, 3, "3");
}

NÜTZLICHE NICHT KOPIERTE INFO

Eine andere Sache, die Sie in dieser Antwort wahrscheinlich nicht gesehen haben, ist die Verwendung des &&Bezeichners und std::forward. In C ++ kann der &&Bezeichner eines von zwei Dingen bedeuten: rWertreferenzen oder universelle Referenzen.

Ich werde nicht auf r-Wert-Referenzen eingehen, sondern auf jemanden, der mit verschiedenen Vorlagen arbeitet. universelle Referenzen sind ein Geschenk Gottes.

Perfekte Weiterleitung

Eine der Verwendungszwecke std::forwardund universellen Referenzen ist die perfekte Weiterleitung von Typen an andere Funktionen.

Wenn wir in Ihrem Beispiel eine int&an übergeben foo2, wird diese intaufgrund der Signatur der generierten foo2Funktion nach dem Abzug der Vorlage automatisch herabgestuft. Wenn Sie diese dann argan eine andere Funktion weiterleiten möchten, die sie durch Referenz ändern würde, erhalten Sie unerwünschte Ergebnisse ( Die Variable wird nicht geändert, da foo2ein Verweis auf die temporäre Variable übergeben wird , die durch Übergabe eines intan sie erstellt wurde. Um dies zu umgehen , geben wir eine Weiterleitungsfunktion an, mit der jede Art von Referenz auf eine Variable (rvalue oder lvalue) verwendet werden kann. Dann, um sicherzugehen, dass wir den genauen Typ übergeben, der in der von uns verwendeten Weiterleitungsfunktion übergeben wurde std::forward, dann und nurdann erlauben wir die Herabstufung von Typen; weil wir jetzt an dem Punkt sind, an dem es am wichtigsten ist.

Wenn nötig, lesen Sie mehr über universelle Referenzen und perfekte Weiterleitung . Scott Meyers ist eine großartige Ressource.

Kaffee und Code
quelle
6

Die C ++ 17-Lösung hierfür kommt Ihrem erwarteten Code sehr nahe:

template<typename T>
static void bar(T t) {}

template<typename... Args>
static void foo2(Args... args) {
    (bar(args), ...);
}

int main() {
    foo2(1, 2, 3, "3");
    return 0;    
}

Dadurch wird das Muster mit dem Kommaoperator zwischen den einzelnen Ausdrücken erweitert

// imaginary expanded expression
(bar(1), bar(2), bar(3), bar("3"));
Guillaume Racicot
quelle
4

Sie können es make_tuplefür die Pack-Erweiterung verwenden, da es einen Kontext einführt, in dem ,die von einer Erweiterung erzeugte Sequenz gültig ist

make_tuple( (bar(std::forward<Args>(args)), 0)... );

Nun vermute ich, dass das unbenutzte / unbenannte / temporäre Tupel von Nullen, das erzeugt wird, vom Compiler erkannt und weg optimiert werden kann.

Demo

Lorah Attkins
quelle
Dies gibt keine Garantie dafür, in welcher Reihenfolge baraufgerufen wird
Eric
1

Dies ist ein vollständiges Beispiel, basierend auf den Antworten hier.

Beispiel für die Reproduktion console.login JavaScript:

Console console;
console.log("bunch", "of", "arguments");
console.warn("or some numbers:", 1, 2, 3);
console.error("just a prank", "bro");

Dateiname zB js_console.h:

#include <iostream>
#include <utility>

class Console {
protected:
    template <typename T>
    void log_argument(T t) {
        std::cout << t << " ";
    }
public:
    template <typename... Args>
    void log(Args&&... args) {
        int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
        cout << endl;
    }

    template <typename... Args>
    void warn(Args&&... args) {
        cout << "WARNING: ";
        int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
        cout << endl;
    }

    template <typename... Args>
    void error(Args&&... args) {
        cout << "ERROR: ";
        int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
        cout << endl;
    }
};
Kungfooman
quelle