Szablon wariadyczny
W programowaniu komputerowym szablony variadic to szablony , które przyjmują zmienną liczbę argumentów.
Szablony Variadic są obsługiwane przez C++ (od standardu C++11 ) oraz język programowania D.
C++
Wariantowa funkcja szablonu C++ została zaprojektowana przez Douglasa Gregora i Jaakko Järviego, a później została znormalizowana w C++11. Przed C++11 szablony (klasy i funkcje) mogły przyjmować tylko ustaloną liczbę argumentów, które musiały być określone podczas pierwszej deklaracji szablonu. C++11 pozwala definicjom szablonów przyjmować dowolną liczbę argumentów dowolnego typu.
szablon < nazwa typu ... Wartości > krotka klasy ; // pobiera zero lub więcej argumentów
Powyższa krotka klasy szablonu
przyjmie dowolną liczbę nazw typów jako parametry szablonu. Tutaj instancja powyższej klasy szablonu jest tworzona z trzema argumentami typu:
krotka < int , std :: vector < int > , std :: map < std :: string , std :: vector < int >>> nazwa_instancji ;
Liczba argumentów może wynosić zero, więc krotka <> nazwa_instancji ;
też zadziała.
Jeśli szablon wariadyczny powinien zezwalać tylko na dodatnią liczbę argumentów, można zastosować tę definicję:
szablon < nazwa typu Najpierw nazwa typu ... Reszta > krotka klasy ; // przyjmuje jeden lub więcej argumentów
Szablony variadic mogą również odnosić się do funkcji, zapewniając w ten sposób nie tylko bezpieczny dodatek do funkcji variadic (takich jak printf), ale także umożliwiając funkcji wywoływanej ze składnią podobną do printf do przetwarzania nietrywialnych obiektów.
szablon < nazwa typu ... Parametry > void my_printf ( const std :: string & str_format , Params ... parametry );
wielokropka ( ...) ma dwie role. Gdy występuje po lewej stronie nazwy parametru, oznacza to pakiet parametrów. Korzystając z pakietu parametrów, użytkownik może powiązać zero lub więcej argumentów z parametrami szablonu variadic. Pakiety parametrów mogą być również używane dla parametrów innych niż typ. Z drugiej strony, gdy operator wielokropka pojawia się po prawej stronie szablonu lub argumentu wywołania funkcji, rozpakowuje pakiety parametrów do oddzielnych argumentów, takich jak argumenty ...
w treści printf
poniżej. W praktyce użycie operatora wielokropka w kodzie powoduje, że całe wyrażenie poprzedzające wielokropek jest powtarzane dla każdego kolejnego argumentu wypakowanego z pakietu argumentów, z wyrażeniami oddzielonymi przecinkami.
Stosowanie szablonów wariadycznych jest często rekurencyjne. Same parametry zmiennoprzecinkowe nie są łatwo dostępne dla implementacji funkcji lub klasy. Dlatego typowy mechanizm definiowania czegoś w rodzaju zamiany variadic printf w C++ 11
wyglądałby następująco:
// przypadek podstawowy void my_printf ( const char * s ) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) != '%' ) ++ s ; w przeciwnym razie wyrzuć std :: runtime_error (
"nieprawidłowy łańcuch formatu: brak argumentów" ); } std :: cout << * s ++ ; } } // szablon rekurencyjny < nazwa typu T , nazwa typu ... Args > void my_printf ( const char * s , T value , Args ... args ) { while ( *
s ) { if ( * s == '%' ) { if ( * ( s + 1 ) != '%' ) { // udawaj, że przetwarzasz format: działa tylko na ciągach znaków składających się z 2 znaków ( %d, % f itd.); kończy się niepowodzeniem z %5.4f s += 2 ; // wypisz wartość std :: cout << wartość ;
// wywoływana nawet wtedy, gdy *s wynosi 0, ale w takim przypadku nic nie robi (i ignoruje dodatkowe argumenty) my_printf ( s , args ...); powrót ; } ++ s ; } std :: cout << * s ++ ; } }
To jest szablon rekurencyjny. Zauważ, że zmienna wersja szablonu my_printf
wywołuje samą siebie lub (w przypadku, gdy args...
jest pusta) wywołuje przypadek podstawowy.
Nie ma prostego mechanizmu do iteracji po wartościach szablonu variadic. Istnieje jednak kilka sposobów przetłumaczenia pakietu argumentów na pojedynczy argument, który można ocenić osobno dla każdego parametru. Zwykle polega to na przeciążaniu funkcji lub — jeśli funkcja może po prostu wybrać jeden argument naraz — użyciu głupiego znacznika rozszerzenia:
szablon < nazwa typu ... Args > inline void pass ( Args && ...) {}
które można wykorzystać w następujący sposób:
szablon < nazwa typu ... Args > inline void expand ( Args && ... args ) { pass ( some_function ( args )...); } rozwiń ( 42 , "odpowiedź" , prawda );
który rozwinie się do czegoś takiego:
pass ( jakaś_funkcja ( arg1 ), jakaś_funkcja ( arg2 ), jakaś_funkcja ( arg3 ) /* itd... */ );
Użycie tej funkcji „pass” jest konieczne, ponieważ rozwinięcie pakietu argumentów odbywa się poprzez oddzielenie argumentów wywołania funkcji przecinkami, które nie są równoważne operatorowi przecinka. Dlatego jakaś_funkcja(argumenty)...;
nigdy nie zadziała. Co więcej, powyższe rozwiązanie będzie działać tylko wtedy, gdy typem zwracanym przez some_function
nie jest void
. Ponadto funkcja some_function
wywołania będą wykonywane w nieokreślonej kolejności, ponieważ kolejność obliczania argumentów funkcji jest niezdefiniowana. Aby uniknąć nieokreślonej kolejności, można użyć list inicjalizacyjnych ujętych w nawiasy klamrowe, które gwarantują ścisłą kolejność oceny od lewej do prawej. Lista inicjalizacyjna wymaga zwracanego typu innego niż void
, ale można użyć operatora przecinka, aby uzyskać 1
dla każdego elementu rozszerzenia.
struct pass { szablon < nazwa typu ... T > pass ( T ...) {} }; przekazać {( jakaś_funkcja ( argumenty ), 1 )...};
Zamiast wykonywania funkcji można określić wyrażenie lambda i wykonać je w miejscu, co pozwala na wykonanie dowolnej sekwencji instrukcji w miejscu.
pass{([&](){ std::cout << argumenty << std::endl; }(), 1)...};
Jednak w tym konkretnym przykładzie funkcja lambda nie jest konieczna. Zamiast tego można użyć bardziej zwykłego wyrażenia:
pass{(std::cout << argumenty << std::endl, 1)...};
Innym sposobem jest użycie przeciążania z „wersjami zakończenia” funkcji. Jest to bardziej uniwersalne, ale wymaga trochę więcej kodu i więcej wysiłku w tworzeniu. Jedna funkcja otrzymuje jeden argument pewnego typu i pakiet argumentów, podczas gdy druga nie otrzymuje żadnego. (Gdyby oba miały tę samą listę parametrów początkowych, wywołanie byłoby niejednoznaczne — sam pakiet parametrów wariadycznych nie może ujednoznacznić wywołania). Na przykład:
void func () {} // szablon wersji zakończenia < nazwa typu Arg1 , nazwa typu ... Args > void func ( const Arg1 & arg1 , const Args && ... args ) { process ( arg1 ); funkcja ( argumenty ...); // uwaga: arg1 nie pojawia się tutaj! }
Jeśli args...
zawiera co najmniej jeden argument, przekieruje do drugiej wersji — pakiet parametrów może być pusty, w takim przypadku po prostu przekieruje do wersji zakończenia, która nic nie da.
Szablony Variadic mogą być również używane w specyfikacji wyjątków, liście klas podstawowych lub liście inicjalizacyjnej konstruktora. Na przykład klasa może określić następujące elementy:
szablon < nazwa typu ... BaseClasses > class ClassName : public BaseClasses ... { public : ClassName ( BaseClasses && ... base_classes ) : BaseClasses ( base_classes )... {} };
Operator unpack zreplikuje typy dla klas podstawowych klasy ClassName
, tak że ta klasa będzie pochodzić z każdego przekazanego typu. Ponadto konstruktor musi przyjąć odwołanie do każdej klasy podstawowej, aby zainicjować klasy podstawowe klasy nazwa klasy
.
W odniesieniu do szablonów funkcji można przekazywać parametry zmienne. W połączeniu z uniwersalnymi referencjami (patrz wyżej) pozwala to na perfekcyjne przekierowanie:
szablon < nazwa typu TypeToConstruct > struct SharedPtrAllocator { szablon < nazwa typu ... Args > std :: shared_ptr < TypeToConstruct > konstrukcja z_shared_ptr ( Args && ... parametry ) { return std :: shared_ptr < TypeToConstruct > ( nowy
TypeToConstruct ( std :: forward < Args > ( params )...)); } };
Spowoduje to rozpakowanie listy argumentów do konstruktora TypeToConstruct. Składnia std::forward<Args>(params)
doskonale przekazuje argumenty jako ich właściwe typy, nawet jeśli chodzi o rwartość, do konstruktora. Operator unpack propaguje składnię przekazywania do każdego parametru. Ta konkretna funkcja fabryczna automatycznie zawija przydzieloną pamięć w std::shared_ptr
dla pewnego stopnia bezpieczeństwa w odniesieniu do wycieków pamięci.
Dodatkowo liczbę argumentów w pakiecie parametrów szablonu można określić w następujący sposób:
szablon < nazwa typu ... Args > struct SomeStruct { static const int size = sizeof ...( Args ); };
Wyrażenie SomeStruct<Type1, Type2>::size
da 2, podczas gdy SomeStruct<>::size
da 0.
D
Definicja
Definicja szablonów variadic w D jest podobna do ich odpowiednika w C++:
szablon VariadicTemplate ( Args ...) { /* Treść */ }
Podobnie, każdy argument może poprzedzać listę argumentów:
szablon VariadicTemplate ( T , wartość ciągu znaków , symbol aliasu , argumenty ...) { /* Body */ }
Podstawowe użycie
Argumenty zmienne są bardzo podobne do stałych tablic w ich użyciu. Mogą być iterowane, dostępne przez indeks, mają length
i mogą być dzielone . Operacje są interpretowane w czasie kompilacji, co oznacza, że operandy nie mogą być wartościami w czasie wykonywania (takimi jak parametry funkcji).
Wszystko, co jest znane w czasie kompilacji, można przekazać jako argumenty wariadyczne. Sprawia, że argumenty variadic są podobne do argumentów aliasów szablonu , ale są potężniejsze, ponieważ akceptują również podstawowe typy (char, short, int...).
Oto przykład, który drukuje reprezentację łańcuchową parametrów wariadycznych. StringOf
i StringOf2
dają takie same wyniki.
static int s_int ; struct Dummy {} void main () { pragma ( msg , StringOf ! ( "Witaj świecie" , uint , Dummy , 42 , s_int )); pragma ( msg , StringOf2 !( "Witaj świecie" , uint , Dummy , 42 , s_int ));
0
} szablon StringOf ( Argumenty ...) { enum StringOf = Args [ ]. stringof ~ StringOf !( Args [ 1. .$]); } szablon StringOf () { enum StringOf = "" ; } szablon StringOf2 ( Args ...) { static if ( Args . length 0
0
== ) wyliczenie StringOf2 = "" ; w przeciwnym razie wylicz StringOf2 = Args [ ]. stringof ~ StringOf2 !( Args [ 1. .$]); }
Wyjścia:
"Witaj świecie"uintDummy42s_int "Witaj świecie"uintDummy42s_int
AliasSeq
Szablony Variadic są często używane do tworzenia sekwencji aliasów o nazwie AliasSeq . Definicja AliasSeq jest w rzeczywistości bardzo prosta:
alias AliasSeq ( Args ...) = Args ;
Ta struktura pozwala manipulować listą zmiennych argumentów, które będą się automatycznie rozszerzać. Argumenty muszą być symbolami lub wartościami znanymi w czasie kompilacji. Obejmuje to wartości, typy, funkcje, a nawet niewyspecjalizowane szablony. Pozwala to na dowolną operację, jakiej można się spodziewać:
import standard . meta ; void main () { // Uwaga: AliasSeq nie może być modyfikowany, a alias nie może być ponownie powiązany, więc będziemy musieli zdefiniować nowe nazwy dla naszych modyfikacji. numery aliasów = AliasSeq !( 1 , 2 , 3 , 4 , 5 , 6 ); // Krojenie aliasu lastHalf = liczby [$ / 2 .. $];
0
0 static assert ( lastHalf == AliasSeq !( 4 , 5 , 6 )); // AliasSeq automatyczne rozszerzenie alias digits = AliasSeq !( , numery , 7 , 8 , 9 ); static assert ( cyfry == AliasSeq !( , 1 , 2 , 3 , 4 , 5 ,
0
6 , 7 , 8 , 9 )); // std.meta udostępnia szablony do pracy z AliasSeq, takie jak anySatisfy, allSatisfy, staticMap i Filter. alias parzyste = Filtr !( isEven , cyfry ); static assert ( parzysteNumbers == AliasSeq !( , 2 , 4 , 6 , 8 )); } szablon isEven ( int
0
liczba ) { enum isEven = ( == ( liczba % 2 )); }
Zobacz też
Artykuły dotyczące konstrukcji wariadycznych innych niż szablony