Variadisk mall
Inom datorprogrammering är variadiska mallar mallar som tar ett varierande antal argument.
Variadiska mallar stöds av C++ (sedan C++11 -standarden) och programmeringsspråket D .
C++
Den variatiska mallfunktionen i C++ designades av Douglas Gregor och Jaakko Järvi och standardiserades senare i C++11. Före C++11 kunde mallar (klasser och funktioner) bara ta ett fast antal argument, som måste specificeras när en mall först deklarerades. C++11 tillåter malldefinitioner att ta ett godtyckligt antal argument av vilken typ som helst.
mall < typnamn ... Värden > klass tupel ; // tar noll eller fler argument
Ovanstående mallklasstuppel tar
valfritt antal typnamn som mallparametrar. Här instansieras en instans av ovanstående mallklass med tre typargument:
tuple < int , std :: vektor < int > , std :: map < std :: string , std :: vektor < int >>> some_instance_name ;
Antalet argument kan vara noll, så tuplea <> some_instance_name ;
kommer också att fungera.
Om den variatiska mallen endast ska tillåta ett positivt antal argument, kan denna definition användas:
mall < typnamn Först typnamn ... Rest > klass tupel ; _ // tar ett eller flera argument
Variadiska mallar kan också tillämpas på funktioner, vilket inte bara tillhandahåller ett typsäkert tillägg till variadicfunktioner (som printf), utan tillåter också en funktion som anropas med printf-liknande syntax för att bearbeta icke-triviala objekt.
mall < typnamn ... Params > void my_printf ( const std :: string & str_format , Params ... parametrar );
Ellipsoperatorn (... ) har två roller. När det förekommer till vänster om namnet på en parameter, deklarerar det ett parameterpaket. Med hjälp av parameterpaketet kan användaren binda noll eller fler argument till de variatiska mallparametrarna. Parameterpaket kan också användas för parametrar som inte är av typen. Däremot, när ellipsoperatorn förekommer till höger om ett mall- eller funktionsanropsargument, packar den upp parameterpaketen i separata argument, som args... i
huvuddelen av printf
nedan. I praktiken gör användningen av en ellipsoperator i koden att hela uttrycket som föregår ellipsen upprepas för varje efterföljande argument som packas upp från argumentpaketet, med uttrycken separerade med kommatecken.
Användningen av variadiska mallar är ofta rekursiv. De variatiska parametrarna i sig är inte lätt tillgängliga för implementering av en funktion eller klass. Därför skulle den typiska mekanismen för att definiera något som en C++11 variadic printf-
ersättning vara som följer:
// base case void my_printf ( const char * s ) { while ( * s ) { if ( * s == ' % ' ) { if ( * ( s + 1 ) != ' % ' ) ++ s ; else throw std :: runtime_error ( "ogiltig formatsträng: saknade argument" ); } std :: cout << * s ++ ; } } // rekursiv mall < typnamn T , typnamn ... Args > void my_printf ( const char * s , T value , Args ... args ) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) != '%' ) { // låtsas tolka formatet: fungerar endast på strängar med två tecken ( %d, %f, etc ); misslyckas med %5.4f s += 2 ; // skriv ut värdet std :: cout << värde ; // anropas även när *s är 0 men gör ingenting i så fall (och ignorerar extra argument) my_printf ( s , args ...); återvända ; } ++ s ; } std :: cout << * s ++ ; } }
Detta är en rekursiv mall. Lägg märke till att den variadiska mallversionen av my_printf
anropar sig själv, eller (i händelse av att args...
är tom) anropar basfallet.
Det finns ingen enkel mekanism för att iterera över värdena för den variadiska mallen. Det finns dock flera sätt att översätta argumentpaketet till ett enda argument som kan utvärderas separat för varje parameter. Vanligtvis kommer detta att förlita sig på funktionsöverbelastning , eller - om funktionen helt enkelt kan välja ett argument i taget - med hjälp av en dum expansionsmarkör:
mall < typnamn ... Args > inline void pass ( Args && ...) {}
som kan användas enligt följande:
mall < typnamn ... Args > inline void expand ( Args && ... args ) { pass ( some_function ( args )...); } expand ( 42 , "svar" , sant );
som kommer att expandera till något som:
pass ( some_function ( arg1 ), some_function ( arg2 ), some_function ( arg3 ) /* etc... */ );
Användningen av denna "pass"-funktion är nödvändig, eftersom expansionen av argumentpaketet fortsätter genom att separera funktionsanropsargumenten med kommatecken, som inte är likvärdiga med kommaoperatorn. Därför, some_function(args)...;
kommer aldrig att fungera. Dessutom kommer lösningen ovan bara att fungera när returtypen för some_function
inte är ogiltig
. Dessutom some_function-
anropen att exekveras i en ospecificerad ordning, eftersom ordningen för utvärdering av funktionsargument är odefinierad. För att undvika den ospecificerade ordningen kan initieringslistor med parenteser användas, vilket garanterar en strikt utvärderingsordning från vänster till höger. En initialiseringslista kräver en returtyp utan tomrum
, men kommaoperatorn kan användas för att ge 1
för varje expansionselement.
struct pass { mall < typnamn ... T > pass ( T ...) {} }; pass {( some_function ( args ), 1 )...};
Istället för att köra en funktion kan ett lambda-uttryck specificeras och exekveras på plats, vilket tillåter exekvering av godtyckliga sekvenser av satser på plats.
pass{([&](){ std::cout << args << std::endl; }(), 1)...};
Men i det här exemplet är en lambdafunktion inte nödvändig. Ett mer vanligt uttryck kan användas istället:
pass{(std::cout << args << std::endl, 1)...};
Ett annat sätt är att använda överbelastning med "termineringsversioner" av funktioner. Detta är mer universellt, men kräver lite mer kod och mer ansträngning för att skapa. En funktion tar emot ett argument av någon typ och argumentpaketet, medan den andra inte tar emot någotdera. (Om båda hade samma lista med initiala parametrar, skulle anropet vara tvetydigt - ett variadisk parameterpaket kan inte ensamt göra ett anrop disambiguera.) Till exempel:
void func () {} // uppsägningsversion mall < typnamn Arg1 , typnamn ... Args > void func ( const Arg1 & arg1 , const Args && ... args ) { process ( arg1 ); func ( args ...); // notera: arg1 visas inte här! }
Om args...
innehåller minst ett argument, kommer det att omdirigera till den andra versionen — ett parameterpaket kan vara tomt, i vilket fall det helt enkelt omdirigerar till avslutningsversionen, vilket inte gör någonting.
Variadiska mallar kan också användas i en undantagsspecifikation, en basklasslista eller initieringslistan för en konstruktor. Till exempel kan en klass ange följande:
mall < typnamn ... Basklasser > klass Klassnamn : offentliga Basklasser ... { offentliga : Klassnamn ( Basklasser && ... basklasser ) : Basklasser ( basklasser )... {} };
Uppackningsoperatorn kommer att replikera typerna för basklasserna för ClassName
, så att denna klass kommer att härledas från var och en av de typer som skickas in. Dessutom måste konstruktorn ta en referens till varje basklass för att initiera basklasserna för Klassnamn
.
När det gäller funktionsmallar kan de variatiska parametrarna vidarebefordras. I kombination med universella referenser (se ovan) möjliggör detta perfekt vidarebefordran:
mall < typnamn TypeToConstruct > struct SharedPtrAllocator { mall < typnamn ... Args > std :: shared_ptr < TypeToConstruct > construct_with_shared_ptr ( Args && ... params ) { return std :: shared_ptr < TypeToConstruct > ( new TypeToConstruct ( std :: forward < Args > ( params )...)); } };
Detta packar upp argumentlistan i konstruktorn för TypeToConstruct. Std ::forward<Args>(params)
-syntaxen vidarebefordrar argument perfekt som deras rätta typer, även med avseende på rvalue-ness, till konstruktorn. Uppackningsoperatören kommer att sprida vidarebefordransyntaxen till varje parameter. Denna speciella fabriksfunktion lindar automatiskt det tilldelade minnet i en std::shared_ptr
för en viss säkerhet när det gäller minnesläckor.
Dessutom kan antalet argument i ett mallparameterpaket bestämmas enligt följande:
mall < typnamn ... Args > struct SomeStruct { static const int size = sizeof ...( Args ); };
Uttrycket SomeStruct<Type1, Type2>::size
kommer att ge 2, medan SomeStruct<>::size
ger 0.
D
Definition
Definitionen av variadisk mall i D liknar deras C++ motsvarighet:
mall VariadicTemplate ( Args ...) { /* Body */ }
På samma sätt kan alla argument föregå argumentlistan:
mall VariadicTemplate ( T , strängvärde , aliassymbol , Args ...) { / * Body * / }
Grundläggande användning
Variadiska argument påminner mycket om konstant array i deras användning. De kan upprepas, nås av ett index, har en length-
egenskap och kan delas upp . Operationer tolkas vid kompilering, vilket innebär att operander inte kan vara ett körtidsvärde (som funktionsparametrar).
Allt som är känt vid kompilering kan skickas som ett variadisk argument. Det gör variatiska argument som liknar mallaliasargument , men mer kraftfulla, eftersom de också accepterar grundläggande typer (char, short, int...).
Här är ett exempel som skriver ut strängrepresentationen av de variatiska parametrarna. StringOf
och StringOf2
ger lika resultat.
0
0
0
statisk int s_int ; struct Dummy {} void main () { pragma ( msg , StringOf !( "Hello world" , uint , Dummy , 42 , s_int )); pragma ( msg , StringOf2 !( "Hej värld" , uint , Dummy , 42 , s_int )); } mall StringOf ( Args ...) { enum StringOf = Args [ ]. stringof ~ StringOf !( Args [ 1. .$]); } mall StringOf () { enum StringOf = "" ; } mall StringOf2 ( Args ...) { static if ( Args . length == ) enum StringOf2 = "" ; annars enum StringOf2 = Args [ ]. stringof ~ StringOf2 !( Args [ 1. .$]); }
Utgångar:
"Hej värld"uintDummy42s_int "Hej värld"uintDummy42s_int
AliasSeq
Variadiska mallar används ofta för att skapa en sekvens av alias, som heter AliasSeq . Definitionen av en AliasSeq är faktiskt väldigt enkel:
alias AliasSeq ( Args ...) = Args ;
Den här strukturen tillåter en att manipulera en lista med olika argument som automatiskt expanderar. Argumenten måste antingen vara symboler eller värden kända vid kompileringstillfället. Detta inkluderar värden, typer, funktioner eller till och med icke-specialiserade mallar. Detta tillåter alla operationer du kan förvänta dig:
0
0
0
0
import std . meta ; void main () { // Obs: AliasSeq kan inte modifieras och ett alias kan inte rebound, så vi måste definiera nya namn för våra modifieringar. aliasnummer = AliasSeq !( 1 , 2 , 3 , 4 , 5 , 6 ) ; // Slicing alias lastHalf = nummer [$ / 2 .. $]; static assert ( lastHalf == AliasSeq !( 4 , 5 , 6 )); // AliasSeq automatisk expansion alias siffror = AliasSeq !( , siffror , 7 , 8 , 9 ); statisk påstående ( siffror == AliasSeq !( , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 )); // std.meta tillhandahåller mallar för att fungera med AliasSeq, såsom anySatisfy, allSatisfy, staticMap och Filter. alias evenNumbers = Filter !( isEven , siffror ); static assert ( evenNumbers == AliasSeq !( , 2 , 4 , 6 , 8 )); } mall isEven ( int number ) { enum isEven = ( == ( nummer % 2 )); }
Se även
För artiklar om andra variatiska konstruktioner än mallar