Im aktuellen Zustand von C++ 11 (sagen gcc 4.7.2), wie sollte ich wählen zwischen variadic-Vorlage oder std::initializer_list
, wenn ich einen Konstruktor brauche, der variable Argumente annehmen kann?Wie kann ich für Konstruktoren zwischen variadic-templates und std :: initializer_list wählen?
Antwort
Eine Variadic-Vorlage ermöglicht Ihnen das Bereitstellen von Argumenten unterschiedlicher Typen, während eine std::initializer_list
mit dem Typ des Arguments templated ist. Dies bedeutet, dass der Typ aller Elemente in der Liste identisch sein muss (oder in den zugrunde liegenden Typ konvertierbar sein muss, aber keine einschränkenden Konvertierungen zulässig sind). Je nachdem, ob dies für Sie wünschenswert ist oder nicht, können Sie sich für das eine oder das andere entscheiden.
Auch eine variadische Vorlage ist in der Regel die Standardauswahl, wenn Sie benötigen perfekten Forwarding, dass in der syntaktische Form T&&
sowohl lvalue Referenzen und rvalue Referenzen binden kann, während ein ähnlicher Abzug Typen kann nicht für initializer_list
durchgeführt werden:
struct A
{
// Deduces T& for lvalue references, T for rvalue references, and binds to both
template<typename... Ts>
A(Ts&&...) { }
// This is an rvalue reference to an initializer_list. The above type deduction
// does not apply here
template<typename T>
A(initializer_list<T>&&) { }
};
Beachten Sie auch, dass ein Konstruktor eine initializer_list
akzeptieren wird standardmäßig aufgerufen werden, wenn Sie einheitliche Initialisierung Syntax (dh geschweiften Klammern) verwenden, obwohl eine andere tragfähige Konstruktor existiert. Dies kann oder kann nicht etwas, das Sie haben wollen sein:
struct A
{
A(int i) { }
};
struct B
{
B(int) { }
B(std::initializer_list<A>) { }
};
int main()
{
B b {1}; // Will invoke the constructor accepting initializer_list
}
Erweitern auf dem Bit über perfekte Weiterleitung, 'std :: initializer_list
@LucDanton: Guter Punkt. –
Ich empfehle immer variadische Vorlagen chosing und vermeiden std::initializer_list
wann immer möglich.
Dies ist, wie ich std::vector
umgesetzt würde mit C++ 11:
#include <iostream>
#include <vector>
struct exp_sequence {
template <typename... T>
exp_sequence(T&&...) {}
};
struct from_arglist_t {} from_arglist;
template <typename T>
class my_vector {
std::vector<T> data;
public:
my_vector(int n, T const& e) : data(n, e) {}
template <typename... Args>
my_vector(from_arglist_t, Args&&... args) {
data.reserve(sizeof...(Args));
exp_sequence{(data.push_back(std::forward<Args>(args)),1)...};
}
std::size_t size() { return data.size(); }
};
int main()
{
std::vector<int> v1{13, 13}; std::cout << v1.size() << '\n'; // 2
std::vector<int> v2(13, 13); std::cout << v2.size() << '\n'; // 13
my_vector<int> v3{13, 13}; std::cout << v3.size() << '\n'; // 13
my_vector<int> v4(13, 13); std::cout << v4.size() << '\n'; // 13
my_vector<int> v5(from_arglist, 13, 13); std::cout << v5.size() << '\n'; // 2
my_vector<int> v6{from_arglist, 13, 13}; std::cout << v6.size() << '\n'; // 2
}
Der Grund ist, wie in main
zeigt initializer_list
in generischem Code zu unterschiedlichem Verhalten führen kann, je nachdem, welche Art von Klammer war gewählt. Es gibt auch die Möglichkeit, Code durch Hinzufügen eines solchen Konstruktors still zu ändern.
Ein weiterer Grund sind Einzug nur Typen:
//std::vector<move_only> m1{move_only{}}; // won't compile
my_vector<move_only> m2{from_arglist, move_only{}}; // works fine
Der 'exp_sequence'-Trick ist ordentlich, aber garantiert er wirklich, die Elemente in der richtigen Reihenfolge einzufügen? AFAIR, C und C++ haben niemals versprochen, Funktionsargumente in lexikalischer Reihenfolge auszuwerten. Wenn es also im C++ 11-Standard keine spezielle Garantie für die Argumentauswertungsreihenfolge für variadic-Vorlagenkonstruktoren gibt, ist dies nicht portabel. – fgp
@fgp: Die Argumentauswertungsreihenfolge ist tatsächlich garantiert von links nach rechts zu sequenzieren.Dies gilt für jede Verwendung der Klammerinitialisierung (daher muss exp_sequence eine Klasse sein). – ipc
@ipc Was genau passiert bei 'exp_sequence {(data.push_back (std :: vorwärts
Mit einer variadische Vorlage wird die Anzahl der Argumente während der Kompilierung (und zugänglich über sizeof...
) bekannt. Bei einer std::initializer_list
ist die Anzahl der Argumente nur zur Laufzeit bekannt. Ein Teil der Entscheidung hängt also davon ab, wann Sie wissen oder wollen, wie viele Argumente Sie haben.
Der Container hat die Größe der Kompilierungszeit, da er im vollständigen Status initialisiert werden muss (Sie können nicht push_back usw.). Wenn Sie meinten, dass die Methode '' size() 'nicht mit 'constexpr' gekennzeichnet ist, war dies ein Fehler in C++ 11 und in C++ 14 behoben. So könnte man auf die Größe einer named 'initializer_list', etc. in vielerlei Hinsicht zurückgreifen, die Größe ist zur Kompilierzeit verfügbar. aber sicher, vielleicht nicht so viele Möglichkeiten wie die variadische Option! –
Ein Konstruktor _parameter_, das eine 'initializer_list' ist, hat keine Größe, die während der Kompilierung bekannt ist, da der Konstruktor von mehreren Sites mit unterschiedlich großen Initialisierern an jedem Standort aufgerufen werden kann. – KnowItAllWannabe
Yeah, toller Punkt, ich habe noch nicht komplett nachgedacht :) –
Ich bin mir nicht sicher (warum ist das ein Kommentar), aber kann nicht variadic Vorlagen verschiedene Arten behandeln, während eine Initialisiererliste alle vom gleichen Typ sein muss? –
@JoachimPileborg, absolut korrekt, obwohl Sie dann zwischen 'int ...' und 'std :: initializer_list' wählen könnten. Ich sage, wählen Sie, was auch immer sich natürlicher anfühlt. –
chris