
Der C ++ 17-Standard hat der Sprache eine neue Funktion hinzugefügt: Class Template Argument Deduction (CTAD) . Zusammen mit den neuen Funktionen in C ++ wurden traditionell neue Methoden zum Schießen eigener Gliedmaßen hinzugefügt. In diesem Artikel werden wir verstehen, was CTAD ist, wofür es verwendet wird, wie es das Leben vereinfacht und welche Fallstricke es enthält.
Fangen wir von weitem an
Erinnern Sie sich daran, worum es beim Abzug von Vorlagenargumenten geht und wofür. Wenn Sie sich mit C ++ - Vorlagen sicher genug fühlen, können Sie diesen Abschnitt überspringen und sofort mit dem nächsten fortfahren.
Vor C ++ 17 wurde die Ausgabe von Vorlagenparametern nur auf Funktionsvorlagen angewendet. Wenn Sie eine Funktionsvorlage instanziieren, geben Sie möglicherweise nicht explizit die Vorlagenargumente an, die aus den Typen der tatsächlichen Funktionsargumente abgeleitet werden können. Die Regeln für die Ableitung sind recht kompliziert. Sie werden im gesamten Abschnitt 17.9.2 des Standards [temp.deduct] behandelt (im Folgenden beziehe ich mich auf die frei verfügbare Version des Standardentwurfs ; in zukünftigen Versionen kann sich die Abschnittsnummerierung ändern, daher empfehle ich, nach dem in spezifizierten Mnemonikcode zu suchen eckige Klammern).
Wir werden nicht alle Feinheiten dieser Regeln im Detail analysieren, sie werden nur von Compiler-Entwicklern benötigt. Für den praktischen Gebrauch reicht es aus, sich an eine einfache Regel zu erinnern: Der Compiler kann die Argumente der Funktionsvorlage unabhängig voneinander ableiten, wenn dies anhand der verfügbaren Informationen eindeutig möglich ist. Beim Ableiten von Arten von Vorlagenparametern werden Standardtransformationen wie beim Aufrufen einer regulären Funktion angewendet ( const wird aus Literaltypen verworfen, Arrays werden auf Zeiger reduziert, Funktionsreferenzen werden auf Funktionszeiger reduziert usw.).
template <typename T> void func(T t) {
All dies vereinfacht die Verwendung von Funktionsvorlagen, ist jedoch leider nicht auf Klassenvorlagen anwendbar. Bei der Instanziierung von Klassenvorlagen mussten alle nicht standardmäßigen Vorlagenparameter explizit angegeben werden. Aufgrund dieser unangenehmen Eigenschaft wurde in der Standardbibliothek eine ganze Familie freier Funktionen mit dem Präfix make_ angezeigt : make_unique , make_shared , make_pair , make_tuple usw.
Neu in C ++ 17
Im neuen Standard werden in Analogie zu den Parametern von Funktionsvorlagen die Parameter von Klassenvorlagen aus den Argumenten der aufgerufenen Konstruktoren abgeleitet:
std::pair pr(false, 45.67);
Es ist sofort erwähnenswert, welche CTAD-Einschränkungen zum Zeitpunkt von C ++ 17 gelten (möglicherweise werden diese Einschränkungen in zukünftigen Versionen des Standards entfernt):
- CTAD funktioniert nicht mit Vorlagenaliasnamen:
template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true};
- CTAD erlaubt keine teilweise Ausgabe von Argumenten (wie dies für die reguläre Ableitung von Vorlagenargumenten funktioniert):
std::pair p{1, 5};
Außerdem kann der Compiler keine Typen von Vorlagenparametern ableiten, die nicht explizit mit den Typen von Konstruktorargumenten zusammenhängen. Das einfachste Beispiel ist ein Containerkonstruktor, der zwei Iteratoren akzeptiert:
template <typename T> struct MyVector { template <typename It> MyVector(It from, It to); }; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()};
Der Typ Es steht nicht in direktem Zusammenhang mit T , obwohl wir Entwickler genau wissen, wie man es bekommt. Um dem Compiler mitzuteilen, wie direkt nicht verwandte Typen ausgegeben werden sollen, wurde in C ++ 17 ein neues Sprachkonstrukt angezeigt - der Ableitungsleitfaden , den wir im nächsten Abschnitt behandeln werden.
Widmungsleitfäden
Für das obige Beispiel würde der Abzugsleitfaden folgendermaßen aussehen:
template <typename It> MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>;
Hier teilen wir dem Compiler mit, dass Sie für einen Konstruktor mit zwei Parametern desselben Typs den Typ von T mithilfe der Konstruktion std::iterator_traits<It>::value_type
. Beachten Sie, dass Abzugsleitfäden außerhalb der Klassendefinition liegen. Auf diese Weise können Sie das Verhalten externer Klassen anpassen, einschließlich Klassen aus der C ++ - Standardbibliothek.
Eine formale Beschreibung der Syntax von Abzugsleitfäden finden Sie in C ++ Standard 17 in Abschnitt 17.10 [temp.deduct.guide] :
[explicit] template-name (parameter-declaration-clause) -> simple-template-id;
Das explizite Schlüsselwort vor dem Abzugsleitfaden verbietet die Verwendung mit der Initialisierung der Kopierliste :
template <typename It> explicit MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()};
Der Abzugsleitfaden muss übrigens keine Vorlage sein:
template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"};
Detaillierter CTAD-Algorithmus
Formale Regeln zum Ableiten von Klassenvorlagenargumenten werden ausführlich in Abschnitt 16.3.1.8 [over.match.class.deduct] von C ++ Standard 17 beschrieben. Versuchen wir, sie herauszufinden.
Wir haben also einen Vorlagentyp C, für den CTAD angewendet wird. Um auszuwählen, welcher Konstruktor und mit welchen Parametern für C aufgerufen werden soll, werden viele Vorlagenfunktionen nach den folgenden Regeln gebildet:
- Für jeden Ci- Konstruktor wird eine Dummy- Fi- Vorlagenfunktion generiert. Fi- Vorlagenparameter sind C- Parameter, gefolgt von Ci- Vorlagenparametern (falls vorhanden), einschließlich Parametern mit Standardwerten. Die Parametertypen der Fi- Funktion entsprechen den Parametertypen des Ci- Konstruktors. Gibt eine Dummy-Funktion Fi Typ C mit Argumenten zurück, die mit den C- Vorlagenparametern übereinstimmen.
Pseudocode:
template <typename T, typename U> class C { public: template <typename V, typename W = A> C(V, W); };
- Wenn Typ C nicht definiert ist oder keine Konstruktoren angegeben sind, gelten die obigen Regeln für den hypothetischen Konstruktor C () .
- Für den C © -Konstruktor wird eine zusätzliche Dummy-Funktion generiert, für die sogar ein spezieller Name gefunden wurde: Copy Deduction Candidate .
- Für jeden Abzugsleitfaden wird auch eine Dummy-Funktion Fi mit Vorlagenparametern und Abzugsleitfadenargumenten sowie einem Rückgabewert generiert, der dem Typ rechts von -> im Abzugsleitfaden entspricht (in der formalen Definition heißt er simple-template-id ).
Pseudocode:
template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<V>>;
Ferner werden für den resultierenden Satz von Fi- Dummy-Funktionen die üblichen Regeln für die Ausgabe von Vorlagenparametern und die Überlastungsauflösung angewendet, mit einer Ausnahme: Wenn eine Dummy-Funktion mit einer Initialisierungsliste aufgerufen wird, die aus einem einzelnen Parameter vom Typ cv U besteht , wobei U Spezialisierung C oder ein von Spezialisierung C geerbter Typ ist (Nur für den Fall, ich werde klarstellen, dass cv == const flüchtig ist . Ein solcher Datensatz bedeutet, dass die Typen U , const U , flüchtig U und const flüchtig U gleich behandelt werden.) Die Regel, die dem Konstruktor C(std::initializer_list<>)
Priorität einräumt C(std::initializer_list<>)
(wird übersprungen Einzelheiten zur Liste initia Die Versionierung finden Sie in Abschnitt 16.3.1.7 [over.match.list] von C ++ Standard 17). Ein Beispiel:
std::vector v1{1, 2};
Wenn es schließlich möglich war, die einzig am besten geeignete Dummy-Funktion auszuwählen, wird der entsprechende Konstruktor oder Ableitungsleitfaden ausgewählt. Wenn es keine geeigneten oder mehrere gleich geeignete gibt, meldet der Compiler einen Fehler.
Fallstricke
CTAD wird zum Initialisieren von Objekten verwendet, und die Initialisierung ist traditionell ein sehr verwirrender Teil der C ++ - Sprache. Mit der Einführung einer einheitlichen Initialisierung in C ++ 11 haben sich die Möglichkeiten, Ihr Bein abzuschießen, nur erhöht. Jetzt können Sie den Konstruktor für ein Objekt mit runden und geschweiften Klammern aufrufen. In vielen Fällen funktionieren beide Optionen gleich, aber nicht immer:
std::vector v1{8, 15};
Bisher scheint alles ziemlich logisch zu sein: v1 und v3 rufen den Konstruktor auf, der std::initializer_list<int>
, int wird aus den Parametern abgeleitet; v4 kann keinen Konstruktor finden, der nur einen Parameter vom Typ int akzeptiert . Aber das sind immer noch Blumen, Beeren vorne:
std::vector v5{"hi", "world"};
v5 wird erwartungsgemäß vom Typ std::vector<const char*>
und mit zwei Elementen initialisiert, aber die nächste Zeile macht etwas völlig anderes. Für einen Vektor gibt es nur einen Konstruktor, der zwei Parameter desselben Typs akzeptiert:
template< class InputIt > vector( InputIt first, InputIt last, const Allocator& alloc = Allocator() );
Dank der Ableitungsanleitung für std::vector
"hi" und "world" als Iteratoren behandelt, und alle "dazwischen" liegenden Elemente werden einem Vektor vom Typ std::vector<char>
hinzugefügt. Wenn wir Glück haben und diese beiden Zeichenfolgenkonstanten hintereinander im Speicher sind, fallen drei Elemente in den Vektor: 'h', 'i', '\ x00', aber höchstwahrscheinlich führt ein solcher Code zu einer Verletzung des Speicherschutzes und zum Absturz des Programms.
Verwendete Materialien
Entwurf Standard C ++ 17
CTAD
CppCon 2018: Stephan T. Lavavej "Klassenvorlagenargumentabzug für alle"