Metaprogrammazione in C++

Pubblicità

_Achille

Utente Èlite
Messaggi
3,067
Reazioni
725
Punteggio
131
Salve ragazzi e buona domenica.
Spulciando online mi è più volte capitato di trovare online utilizzi molto potenti dei template su C++, poco chiari ai miei occhi. Cercando allora sono arrivato alla conclusione che si tratta di metaprogrammazione, che dovrebbe essere un approccio come lo è la programmazione funzionale, dinamica ecc...
Ho quindi creato, molto faticosamente, del codice che dovrebbe risalire alla metaprogrammazione:

C++:
using namespace std;

template <class, class = void>
struct is_a_pointer : false_type { };

template <class T>
struct is_a_pointer<T, void_t<decltype(*declval<T&>())>> : true_type { };

Va detto che i template sono una delle pari più dolenti a me del C++, dato che non vedevo oltre all'uso nella programmazione generica.
È poi semplice utilizzare la struttura creata in vari modi

C++:
template <class T>
constexpr auto checkDerefOperator() noexcept -> bool
{
    return is_a_pointer<T>::value;
}
Ed è così possibile sapere se il parametro ha un operator*() con overload.
E quindi
C++:
template <class T>
constexpr void failIfNotAPointer() noexcept
{
    static_assert(checkDerefOperator<T>(), "Not a pointer!");
}

// Questo non centra praticamente nulla
template <class T>
auto dereference(T& t) noexcept -> decltype (*t)
{
    return *t;
}

Quindi in tutto ciò la metaprogrammazione permette di sapere se un argomento al template ha determinate caratteristiche. E poi? Davvero questa complessità per tale cosa? Perché non implementare una clausola where come C#? Cos'altro permette di fare la metaprogrammazione?
Lo chiedo specialmente perché vi sono vari libri che spiegano la metaprogrammazione al costo non molto contenuto. Qualcuno la ha mai affrontata? Grazie
 
Ultima modifica:
Dichiarare un parametro come "template" significa permettere a quella variabile di avere molteplici tipi. Puoi quindi scrivere una funzione unica, ma poi quando la dichiari ci metti dentro il tipo che vuoi.
Prendi per esempio una classe che implementa una lista. Senza usare template, dovresti scrivere una classe diversa per ognuno dei tipi che vuoi usare, per esempio una lista per interi, una per float, una per la classe Pippo e via dicendo. Con il template, non c’è ne è bisogno. La definizione della classe ha una variabile Template, opera su quella. Poi quando la usi spetta a te, nella dichiarazione, di sostituire Template con il tipo che vuoi. È in pratica l'equivalente del tipo (void *) del linguaggio C, a cui puoi passare il puntatore di quello che vuoi. Con una grossa differenza, con (void*) il compilatore non può fare nessun tipo di controllo del tipo, mentre con una funzione con Template può controllare se hai passato la variabile giusta come nella dichiarazione.
 
Concettualmente sono un pò come i Generics in C# ed in Java. In pratica viene risolto tutto in fase di compilazione, il che consente anche di avere un controllo sui tipi (come dice Andretti), e non di avere un puntatore a "qualcosa" in memoria.
Va detto che in C++ sono però più potenti rispetto a Java e C#.

Lascio anche un articolo che sembra ben scritto: https://monoinfinito.wordpress.com/series/introduction-to-c-template-metaprogramming/

Continuo a preferire C al caos di C++. :sisi:
 
Dichiarare un parametro come "template" significa permettere a quella variabile di avere molteplici tipi. Puoi quindi scrivere una funzione unica, ma poi quando la dichiari ci metti dentro il tipo che vuoi.
Prendi per esempio una classe che implementa una lista. Senza usare template, dovresti scrivere una classe diversa per ognuno dei tipi che vuoi usare, per esempio una lista per interi, una per float, una per la classe Pippo e via dicendo. Con il template, non c’è ne è bisogno. La definizione della classe ha una variabile Template, opera su quella. Poi quando la usi spetta a te, nella dichiarazione, di sostituire Template con il tipo che vuoi. È in pratica l'equivalente del tipo (void *) del linguaggio C, a cui puoi passare il puntatore di quello che vuoi. Con una grossa differenza, con (void*) il compilatore non può fare nessun tipo di controllo del tipo, mentre con una funzione con Template può controllare se hai passato la variabile giusta come nella dichiarazione.
Concettualmente sono un pò come i Generics in C# ed in Java. In pratica viene risolto tutto in fase di compilazione, il che consente anche di avere un controllo sui tipi (come dice Andretti), e non di avere un puntatore a "qualcosa" in memoria.
Va detto che in C++ sono però più potenti rispetto a Java e C#.

Lascio anche un articolo che sembra ben scritto: https://monoinfinito.wordpress.com/series/introduction-to-c-template-metaprogramming/
Ah quindi tutto ‘sto casino per definirla semplice programmazione generica?
E perché roba del tipo
C++:
template <int value>
int fattoriale()
{
    return value * fattoriale<value - 1>();
}
template <>
int fattoriale<1>()
{
    return 1;
}
Quando basta questo una funzione constexpr per ottenere lo stesso risultato (ovvero valutazione a tempo di compilazione)?
C++:
constexpr int fattoriale(int value) noexcept
{
    if (value == 1)
        return value;

    return value * fattoriale(value - 1);
}
Per non parlare di casini vari con le strutture ecc...
Boh deve esserci qualcos'altro sotto o questa storia della metaprogrammazione o forse risale ancor prima del C++11 e quindi di constexpr...
Continuo a preferire C al caos di C++. :sisi:
Ah certamente. Poco, troppo poco type-safe ma almeno non si hanno 6734583849 cose ridondati, conseguenti al fatto di voler mantenere la compatibilità con il C ma ormai col C C++ non ha nulla a che fare. Più che altro ci devo fare le olimpiadi perciò ho pure bisogno della STL.
Forse però è meglio andare a farsi un giretto su Python :sisi:
 
Non è proprio "solo programmazione generica", non intesa come in Java o C#. In C++ ad esempio non necessiti di specificare alcun tipo dato per effettuare, ad esempio, una somma tra due tipi generici 'T'. In Java se provassi a sommare oppure a richiamare un metodo... verresti fermato, in quanto dovresti prima dire a quale classe appartiene (o qual è la superclasse).
Una delle altre differenze è che in Java viene svolto tutto a runtime, in C++ invece in fase di compilazione (causa infatti un allungamento dei tempi di compilazione).

Attenzione che c'è una differenza sostanziale anche tra constexpr ed i template. Questi ultimi vengono solo creati in fase di compilazione, ma non eseguiti. Con constexpr invece puoi ottenere direttamente il risultato in compilazione.

Forse questo può darti altre informazioni e spiegare meglio alcune cose: https://docs.microsoft.com/it-it/do...ces-between-cpp-templates-and-csharp-generics
 
Non è proprio "solo programmazione generica", non intesa come in Java o C#. In C++ ad esempio non necessiti di specificare alcun tipo dato per effettuare, ad esempio, una somma tra due tipi generici 'T'. In Java se provassi a sommare oppure a richiamare un metodo... verresti fermato, in quanto dovresti prima dire a quale classe appartiene (o qual è la superclasse).
Una delle altre differenze è che in Java viene svolto tutto a runtime, in C++ invece in fase di compilazione (causa infatti un allungamento dei tempi di compilazione).

Attenzione che c'è una differenza sostanziale anche tra constexpr ed i template. Questi ultimi vengono solo creati in fase di compilazione, ma non eseguiti. Con constexpr invece puoi ottenere direttamente il risultato in compilazione.

Forse questo può darti altre informazioni e spiegare meglio alcune cose: https://docs.microsoft.com/it-it/do...ces-between-cpp-templates-and-csharp-generics
Tutte le differenze tra generics e template le avevo già colte quando provai a costituire un metodo C# che sommava modelli ottenendo un bel “nessun operatore + tra T e U” :sisi:

Comunque io sapevo che i template venissero poi costruiti a tempo di compilazione e sinceramente visto le implementazioni del tipo fibonacci, fattoriale ecc... mi aspettavo anche una valutazione pure a tempo di compilazione...
Quindi in senso del fattoriale da me postato? Fare i fighi con i template?
 
L'analisi di ciò che sta dietro è piuttosto intricata, ma penso che guardando a basso livello si capisca bene cosa avvenga, spero di riuscire a spiegarmi bene.
Ho usato il tuo codice, ho aggiunto solo il main. La funzione l'ho invocata passando 6 come valore.


Non so quale sia il livello di conoscenza del linguaggio assembly; comunque per comodità vedi MOV EAX, n come un normale assegnamento del valore n in EAX.
Viene chiamata una funzione per ottenere il valore calcolato; la funzione è di seguito riportata (ricordo il valore passato, 6):

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028CC  /$  55            PUSH EBP                                 ; test.004028CC(guessed void)
004028CD  |.  89E5          MOV EBP,ESP
004028CF  |.  83EC 08       SUB ESP,8
004028D2  |.  E8 DDFFFFFF   CALL 004028B4
004028D7  |.  89C2          MOV EDX,EAX
004028D9  |.  89D0          MOV EAX,EDX
004028DB  |.  01C0          ADD EAX,EAX
004028DD  |.  01D0          ADD EAX,EDX
004028DF  |.  01C0          ADD EAX,EAX
004028E1  |.  C9            LEAVE
004028E2  \.  C3            RETN

Lasciando perdere il prologo, la prima istruzione è una CALL il cui corpo è:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028B4  /$  55            PUSH EBP
004028B5  |.  89E5          MOV EBP,ESP
004028B7  |.  83EC 08       SUB ESP,8
004028BA  |.  E8 E5FFFFFF   CALL 004028A4
004028BF  |.  89C2          MOV EDX,EAX
004028C1  |.  89D0          MOV EAX,EDX
004028C3  |.  C1E0 02       SHL EAX,2
004028C6  |.  01D0          ADD EAX,EDX
004028C8  |.  C9            LEAVE
004028C9  \.  C3            RETN

anche in questo caso, avviene una CALL:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028A4  /$  55            PUSH EBP
004028A5  |.  89E5          MOV EBP,ESP
004028A7  |.  83EC 08       SUB ESP,8
004028AA  |.  E8 DDFFFFFF   CALL 0040288C
004028AF  |.  C1E0 02       SHL EAX,2
004028B2  |.  C9            LEAVE
004028B3  \.  C3            RETN

Identica situazione, altra CALL:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
0040288C  /$  55            PUSH EBP
0040288D  |.  89E5          MOV EBP,ESP
0040288F  |.  83EC 08       SUB ESP,8
00402892  |.  E8 E9FFFFFF   CALL 00402880
00402897  |.  89C2          MOV EDX,EAX
00402899  |.  89D0          MOV EAX,EDX
0040289B  |.  01C0          ADD EAX,EAX
0040289D  |.  01D0          ADD EAX,EDX
0040289F  |.  C9            LEAVE
004028A0  \.  C3            RETN

La funzione chiamata, richiama a sua volta un'altra funzione:
Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
00402880  /$  55            PUSH EBP
00402881  |.  89E5          MOV EBP,ESP
00402883  |.  E8 A8EDFFFF   CALL 00401630
00402888  |.  01C0          ADD EAX,EAX
0040288A  |.  5D            POP EBP
0040288B  \.  C3            RETN

Che infine richiama questa:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
00401630  /$  55            PUSH EBP
00401631  |.  89E5          MOV EBP,ESP
00401633  |.  B8 01000000   MOV EAX,1
00401638  |.  5D            POP EBP
00401639  \.  C3            RETN

Questo è il caso base, ovvero:

C++:
template <>
int fattoriale<1>()
{
    return 1;
}

A seguito dell'assegnamento del valore 1 ad EAX. Questo tornando al chiamante provoca una ADD EAX, EAX, che quindi somma al valore 1 di nuovo 1.
A questo punto EAX = 2; il valore viene assegnato ad EDX e poi sommato ancora in EAX, portando il totale a 6. Questo valore viene shiftato a sinistra di 2 bit, portando il valore a 24.
Tornando alla funzione chiamante il valore viene assegnato a EDX, per poi subire un nuovo shift a sinistra di 2 bit, portando il valore a 96; a questo valore viene sommato quello di EDX, quindi per un totale di 120.
A questo punto viene riassegnato nuovamente il valore a EDX: altra somma tra EAX ed EAX, quindi 120+120 = 240 a questo risultato il valore di EDX viene sommato nuovamente 120 per un totale di 360. Ora l'ultima somma: 360 + 360 = 720, ovvero 6!.

Come noti, anche se il compilatore si occupa di predisporre tutte le somme e gli shift, il valore non è calcolato.



constexpr può essere valutata a runtime oppure in compilazione. Se richiami la funzione normalmente, viene generato questo codice:

C++:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028A0  /$  55            PUSH EBP
004028A1  |.  89E5          MOV EBP,ESP
004028A3  |.  83EC 18       SUB ESP,18
004028A6  |.  837D 08 01    CMP DWORD PTR SS:[ARG.1],1
004028AA  |.  75 05         JNE SHORT 004028B1
004028AC  |.  8B45 08       MOV EAX,DWORD PTR SS:[ARG.1]
004028AF  |.  EB 12         JMP SHORT 004028C3
004028B1  |>  8B45 08       MOV EAX,DWORD PTR SS:[ARG.1]
004028B4  |.  83E8 01       SUB EAX,1
004028B7  |.  890424        MOV DWORD PTR SS:[LOCAL.6],EAX
004028BA  |.  E8 E1FFFFFF   CALL 004028A0
004028BF  |.  0FAF45 08     IMUL EAX,DWORD PTR SS:[ARG.1]
004028C3  |>  C9            LEAVE
004028C4  \.  C3            RETN

in pratica è una funzione ricorsiva (richiama sè stessa sino a che EAX=1, che è il caso base).

Con questo codice invece:
C++:
#include <iostream>


constexpr int fattoriale(int value) noexcept
{
    if (value == 1)
        return value;

    return value * fattoriale(value - 1);
}


int main() {
constexpr int n = fattoriale(6);
   
std::cout << n << "\n";
return 0;
}

la valutazione avviene in compilazione.

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
00401630  /$  8D4C24 04     LEA ECX,[ARG.1]
00401634  |.  83E4 F0       AND ESP,FFFFFFF0                         ; DQWORD (16.-byte) stack alignment
00401637  |.  FF71 FC       PUSH DWORD PTR DS:[ECX-4]
0040163A  |.  55            PUSH EBP
0040163B  |.  89E5          MOV EBP,ESP
0040163D  |.  51            PUSH ECX
0040163E  |.  83EC 24       SUB ESP,24
00401641  |.  E8 1A020000   CALL 00401860
00401646  |.  C745 F4 D0020 MOV DWORD PTR SS:[LOCAL.4],2D0
0040164D  |.  C70424 D00200 MOV DWORD PTR SS:[LOCAL.11],2D0          ; /Arg1 => 2D0
00401654  |.  B9 006AF06F   MOV ECX,6FF06A00                         ; |
00401659  |.  E8 96000000   CALL <JMP.&libstdc++-6._ZNSolsEi>        ; \libstdc++-6._ZNSolsEi

come noti, guardando le istruzioni agli indirizzi 0x00401646 e successivo, c'è una costante (un valore immediato), ovvero 0x2D0 (720 in decimale).

Spero di non aver confuso ulteriormente le cose... :)
 
L’esempio del fattoriale non conta molto in quanto puoi calcolare il fattoriale solo di una variabile di tipo numerico. I Template hanno senso per scrivere una funzione generica che possa essere valida per un vasto genere di tipi diversi. Non per nulla una delle librerie più famose ed efficienti per programmare COM in C++ è proprio la ATL (active Template library) che è mille molto meglio di quell'aborto di MFC.
 
L’esempio del fattoriale non conta molto in quanto puoi calcolare il fattoriale solo di una variabile di tipo numerico. I Template hanno senso per scrivere una funzione generica che possa essere valida per un vasto genere di tipi diversi. Non per nulla una delle librerie più famose ed efficienti per programmare COM in C++ è proprio la ATL (active Template library) che è mille molto meglio di quell'aborto di MFC.
Certamente avevo capito l'aspetto generico, ma qui si parla di un altro modo di usarli come il codice postato da me sopra.
L'analisi di ciò che sta dietro è piuttosto intricata, ma penso che guardando a basso livello si capisca bene cosa avvenga, spero di riuscire a spiegarmi bene.
Ho usato il tuo codice, ho aggiunto solo il main. La funzione l'ho invocata passando 6 come valore.


Non so quale sia il livello di conoscenza del linguaggio assembly; comunque per comodità vedi MOV EAX, n come un normale assegnamento del valore n in EAX.
Viene chiamata una funzione per ottenere il valore calcolato; la funzione è di seguito riportata (ricordo il valore passato, 6):

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028CC  /$  55            PUSH EBP                                 ; test.004028CC(guessed void)
004028CD  |.  89E5          MOV EBP,ESP
004028CF  |.  83EC 08       SUB ESP,8
004028D2  |.  E8 DDFFFFFF   CALL 004028B4
004028D7  |.  89C2          MOV EDX,EAX
004028D9  |.  89D0          MOV EAX,EDX
004028DB  |.  01C0          ADD EAX,EAX
004028DD  |.  01D0          ADD EAX,EDX
004028DF  |.  01C0          ADD EAX,EAX
004028E1  |.  C9            LEAVE
004028E2  \.  C3            RETN

Lasciando perdere il prologo, la prima istruzione è una CALL il cui corpo è:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028B4  /$  55            PUSH EBP
004028B5  |.  89E5          MOV EBP,ESP
004028B7  |.  83EC 08       SUB ESP,8
004028BA  |.  E8 E5FFFFFF   CALL 004028A4
004028BF  |.  89C2          MOV EDX,EAX
004028C1  |.  89D0          MOV EAX,EDX
004028C3  |.  C1E0 02       SHL EAX,2
004028C6  |.  01D0          ADD EAX,EDX
004028C8  |.  C9            LEAVE
004028C9  \.  C3            RETN

anche in questo caso, avviene una CALL:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028A4  /$  55            PUSH EBP
004028A5  |.  89E5          MOV EBP,ESP
004028A7  |.  83EC 08       SUB ESP,8
004028AA  |.  E8 DDFFFFFF   CALL 0040288C
004028AF  |.  C1E0 02       SHL EAX,2
004028B2  |.  C9            LEAVE
004028B3  \.  C3            RETN

Identica situazione, altra CALL:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
0040288C  /$  55            PUSH EBP
0040288D  |.  89E5          MOV EBP,ESP
0040288F  |.  83EC 08       SUB ESP,8
00402892  |.  E8 E9FFFFFF   CALL 00402880
00402897  |.  89C2          MOV EDX,EAX
00402899  |.  89D0          MOV EAX,EDX
0040289B  |.  01C0          ADD EAX,EAX
0040289D  |.  01D0          ADD EAX,EDX
0040289F  |.  C9            LEAVE
004028A0  \.  C3            RETN

La funzione chiamata, richiama a sua volta un'altra funzione:
Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
00402880  /$  55            PUSH EBP
00402881  |.  89E5          MOV EBP,ESP
00402883  |.  E8 A8EDFFFF   CALL 00401630
00402888  |.  01C0          ADD EAX,EAX
0040288A  |.  5D            POP EBP
0040288B  \.  C3            RETN

Che infine richiama questa:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
00401630  /$  55            PUSH EBP
00401631  |.  89E5          MOV EBP,ESP
00401633  |.  B8 01000000   MOV EAX,1
00401638  |.  5D            POP EBP
00401639  \.  C3            RETN

Questo è il caso base, ovvero:

C++:
template <>
int fattoriale<1>()
{
    return 1;
}

A seguito dell'assegnamento del valore 1 ad EAX. Questo tornando al chiamante provoca una ADD EAX, EAX, che quindi somma al valore 1 di nuovo 1.
A questo punto EAX = 2; il valore viene assegnato ad EDX e poi sommato ancora in EAX, portando il totale a 6. Questo valore viene shiftato a sinistra di 2 bit, portando il valore a 24.
Tornando alla funzione chiamante il valore viene assegnato a EDX, per poi subire un nuovo shift a sinistra di 2 bit, portando il valore a 96; a questo valore viene sommato quello di EDX, quindi per un totale di 120.
A questo punto viene riassegnato nuovamente il valore a EDX: altra somma tra EAX ed EAX, quindi 120+120 = 240 a questo risultato il valore di EDX viene sommato nuovamente 120 per un totale di 360. Ora l'ultima somma: 360 + 360 = 720, ovvero 6!.

Come noti, anche se il compilatore si occupa di predisporre tutte le somme e gli shift, il valore non è calcolato.



constexpr può essere valutata a runtime oppure in compilazione. Se richiami la funzione normalmente, viene generato questo codice:

C++:
CPU Disasm
Address   Hex dump          Command                                  Comments
004028A0  /$  55            PUSH EBP
004028A1  |.  89E5          MOV EBP,ESP
004028A3  |.  83EC 18       SUB ESP,18
004028A6  |.  837D 08 01    CMP DWORD PTR SS:[ARG.1],1
004028AA  |.  75 05         JNE SHORT 004028B1
004028AC  |.  8B45 08       MOV EAX,DWORD PTR SS:[ARG.1]
004028AF  |.  EB 12         JMP SHORT 004028C3
004028B1  |>  8B45 08       MOV EAX,DWORD PTR SS:[ARG.1]
004028B4  |.  83E8 01       SUB EAX,1
004028B7  |.  890424        MOV DWORD PTR SS:[LOCAL.6],EAX
004028BA  |.  E8 E1FFFFFF   CALL 004028A0
004028BF  |.  0FAF45 08     IMUL EAX,DWORD PTR SS:[ARG.1]
004028C3  |>  C9            LEAVE
004028C4  \.  C3            RETN

in pratica è una funzione ricorsiva (richiama sè stessa sino a che EAX=1, che è il caso base).

Con questo codice invece:
C++:
#include <iostream>


constexpr int fattoriale(int value) noexcept
{
    if (value == 1)
        return value;

    return value * fattoriale(value - 1);
}


int main() {
constexpr int n = fattoriale(6);

std::cout << n << "\n";
return 0;
}

la valutazione avviene in compilazione.

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
00401630  /$  8D4C24 04     LEA ECX,[ARG.1]
00401634  |.  83E4 F0       AND ESP,FFFFFFF0                         ; DQWORD (16.-byte) stack alignment
00401637  |.  FF71 FC       PUSH DWORD PTR DS:[ECX-4]
0040163A  |.  55            PUSH EBP
0040163B  |.  89E5          MOV EBP,ESP
0040163D  |.  51            PUSH ECX
0040163E  |.  83EC 24       SUB ESP,24
00401641  |.  E8 1A020000   CALL 00401860
00401646  |.  C745 F4 D0020 MOV DWORD PTR SS:[LOCAL.4],2D0
0040164D  |.  C70424 D00200 MOV DWORD PTR SS:[LOCAL.11],2D0          ; /Arg1 => 2D0
00401654  |.  B9 006AF06F   MOV ECX,6FF06A00                         ; |
00401659  |.  E8 96000000   CALL <JMP.&libstdc++-6._ZNSolsEi>        ; \libstdc++-6._ZNSolsEi

come noti, guardando le istruzioni agli indirizzi 0x00401646 e successivo, c'è una costante (un valore immediato), ovvero 0x2D0 (720 in decimale).

Spero di non aver confuso ulteriormente le cose... :)
Assolutamente no! Aiuta a capire cosa sta sotto, soprattutto in questo caso dove molta roba è mascherata. Sfortunatamente le mie conoscenze sono nel CIL, ma dal codice in Assembly si capisce quindi che in questo caso c'è una ricorsione, basata però su funzioni diverse ognuna con locazioni in memoria diverse.
Provando però un modo diverso di affrontare la cosa:
C++:
template <int v>
struct Fattoriale { enum : long { value = v * Fattoriale<v - 1>::value }; };

template <>
struct Fattoriale<1> { enum : long { value = 1 }; };
E eseguendo questo main
C++:
int main()
{
    using namespace std;
    using namespace chrono;

    time_point<high_resolution_clock> begin, end;

    begin = high_resolution_clock::now();
    fattoriale<1111>();
    end = high_resolution_clock::now();
    cout << duration_cast<duration<double>>(end - begin).count() << endl << endl;

    begin = high_resolution_clock::now();
    Fattoriale<333>::value;
    end = high_resolution_clock::now();
    cout << duration_cast<duration<double>>(end - begin).count() << endl << endl;

    begin = high_resolution_clock::now();
    Fattoriale<444>::value;
    end = high_resolution_clock::now();
    cout << duration_cast<duration<double>>(end - begin).count();

    cin.get();
}
Si nota che mentre la valutazione della funzione richieda tempo in runtime, quella della struttura non richieda alcun tempo in esecuzione ma solo tempo in compilazione.
Ora confrontando la funzione constexpr alla struttura si ottiene praticamente lo stesso risultato, senza utilizzo dei template e generazione di n strutture. Quindi perché preferire la struttura template alla ricorsione?
C++:
template <unsigned long v>
struct Fattoriale { enum : unsigned long { value = v * Fattoriale<v - 1>::value }; };

template <>
struct Fattoriale<1> { enum : unsigned long { value = 1 }; };

constexpr auto fattoriale(unsigned long v) noexcept -> unsigned long
{
    if (v == 1)
        return 1;
    return v * fattoriale(v - 1);
}

int main()
{
    using namespace std;
    using namespace chrono;
    using namespace meta;

    time_point<high_resolution_clock> begin, end;

    begin = high_resolution_clock::now();
    constexpr long n = fattoriale(400);
    end = high_resolution_clock::now();
    cout << duration_cast<duration<double>>(end - begin).count() << endl << endl;

    begin = high_resolution_clock::now();
    Fattoriale<400>::value;
    end = high_resolution_clock::now();
    cout << duration_cast<duration<double>>(end - begin).count();

    cin.get();
}
 
Da notare comunque che il discorso qui si fa complesso: entrano in gioco tanti fattori, come ad esempio il compilatore utilizzato ed i flag utilizzati (io sto usando MinGw).
Nel caso del template le chiamate li sopra non sono ricorsive, ma una funzione chiama la successiva. Inoltre in maniera intelligente sono state fatte più istruzioni con le somme (probabilmente in questo modo ha evitato dei loop, oltre ad operazioni più complesse).

Considera il codice del template che hai incollato sopra.
C++:
#include <iostream>

template <int value>
int fattoriale()
{
    return value * fattoriale<value - 1>();
}

template <>
int fattoriale<1>()
{
    return 1;
}

int main() {

int n = fattoriale<6>();
std::cout << n << "\n";

return 0;

}

Questo è il risultato, compilando con O2:

Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004027F0  /$  8D4C24 04     LEA ECX,[ARG.1]
004027F4  |.  83E4 F0       AND ESP,FFFFFFF0                         ; DQWORD (16.-byte) stack alignment
004027F7  |.  FF71 FC       PUSH DWORD PTR DS:[ECX-4]
004027FA  |.  55            PUSH EBP
004027FB  |.  89E5          MOV EBP,ESP
004027FD  |.  51            PUSH ECX
004027FE  |.  83EC 14       SUB ESP,14
00402801  |.  E8 CAEFFFFF   CALL 004017D0
00402806  |.  B9 006AF06F   MOV ECX,6FF06A00
0040280B  |.  C70424 D00200 MOV DWORD PTR SS:[LOCAL.7],2D0           ; /Arg1 => 2D0
00402812  |.  E8 51EEFFFF   CALL <JMP.&libstdc++-6._ZNSolsEi>        ; \libstdc++-6._ZNSolsEi
00402817  |.  83EC 04       SUB ESP,4
0040281A  |.  C74424 04 644 MOV DWORD PTR SS:[LOCAL.6],OFFSET 004040
00402822  |.  890424        MOV DWORD PTR SS:[LOCAL.7],EAX
00402825  |.  E8 26EEFFFF   CALL <JMP.&libstdc++-6._ZStlsISt11char_t ; Jump to libstdc++-6._ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0040282A  |.  8B4D FC       MOV ECX,DWORD PTR SS:[LOCAL.2]
0040282D  |.  31C0          XOR EAX,EAX
0040282F  |.  C9            LEAVE
00402830  |.  8D61 FC       LEA ESP,[ECX-4]
00402833  \.  C3            RETN

Se osservi l'indirizzo 0x0040280B noterai anche qui il calcolo fatto in compilazione. Sono le "magie" dei compilatori (e dei flags).

Riporto anche l'altra versione sempre con O2:
C++:
#include <iostream>

constexpr int fattoriale(int value) noexcept
{
    if (value == 1)
        return value;

    return value * fattoriale(value - 1);
}

int main() {
constexpr int n = fattoriale(6);
  
std::cout << n << "\n";
return 0;
}

Il codice ora è il seguente:
Codice:
CPU Disasm
Address   Hex dump          Command                                  Comments
004027E0  /$  8D4C24 04     LEA ECX,[ARG.1]
004027E4  |.  83E4 F0       AND ESP,FFFFFFF0                         ; DQWORD (16.-byte) stack alignment
004027E7  |.  FF71 FC       PUSH DWORD PTR DS:[ECX-4]
004027EA  |.  55            PUSH EBP
004027EB  |.  89E5          MOV EBP,ESP
004027ED  |.  51            PUSH ECX
004027EE  |.  83EC 14       SUB ESP,14
004027F1  |.  E8 CAEFFFFF   CALL 004017C0
004027F6  |.  B9 40724000   MOV ECX,OFFSET <&libstdc++-6._ZSt4cout>
004027FB  |.  C70424 D00200 MOV DWORD PTR SS:[LOCAL.7],2D0           ; /Arg1 => 2D0
00402802  |.  E8 51EEFFFF   CALL <JMP.&libstdc++-6._ZNSolsEi>        ; \libstdc++-6._ZNSolsEi
00402807  |.  83EC 04       SUB ESP,4
0040280A  |.  C74424 04 644 MOV DWORD PTR SS:[LOCAL.6],OFFSET 004040
00402812  |.  890424        MOV DWORD PTR SS:[LOCAL.7],EAX
00402815  |.  E8 26EEFFFF   CALL <JMP.&libstdc++-6._ZStlsISt11char_t ; Jump to libstdc++-6._ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0040281A  |.  8B4D FC       MOV ECX,DWORD PTR SS:[LOCAL.2]
0040281D  |.  31C0          XOR EAX,EAX
0040281F  |.  C9            LEAVE
00402820  |.  8D61 FC       LEA ESP,[ECX-4]
00402823  \.  C3            RETN

Anche in questo caso guardando l'indirizzo 0x004027FB vedrai il risultato.

Il codice generato come noti è molto simile (quello del template presenta qualche istruzione in più). I template costituiscono comunque uno strumento potente ed inevitabilmente aggiungono astrazione.

Scrissi tempo fa un articolo, per la verità poco notato, su if e switch; non è proprio in topic, ma mostravo le differenze tra compilatori e tra la compilazione con e senza flag: I costrutti condizionali: switch, un analisi a basso livello (Parte 2/2) (alla parte 1 puoi risalire dall'articolo linkato).
 
Scusate ma vi state fasciando la testa inutilmente. Innanzitutto la metaprogrammazione tramite template è il caso più banale di metaprogrammazione. C'è ben altro oltre ed è ben visibile a chi abbia mai usato LISP.

Sul fatto che lo scopo pratico sia in linea con gli obiettivi della programmazione generica siamo d'accordo. Ma a che costo? Perchè non dimentichiamo che C++ è un linguaggio di sistema e segue la filosofia "zero cost abstraction". Che è una filosofia talmente seria da aver spinto gli sviluppatori di Rust a creare un modello di memoria estremamente complesso per ottenere i loro scopi senza....dover introdurre analisi del programma a runtime!

L'introspezione e la reflection sono le due armi che gli altri linguaggi ( non di sistema ) usano per ottenere risultati simili, ok anche qualcosina in più ( sempre in LISP si può vedere questo qualcosa fin dove si spinge ).

Ma tutto ciò ha un costo, in termini di cicli di cpu, memoria e di complessità dell'ambiente di runtime ( che ti porti appresso ). C++ non può concedersi questo lusso, da cui si capisce perchè non abbia seguito la strada percorsa da Java, C# e compagnia.
 
[QUOTE="pabloski, post: 6961232, member: 21773" ... C++ non può concedersi questo lusso, da cui si capisce perchè non abbia seguito la strada percorsa da Java, C# e compagnia.[/QUOTE]
Quoto. Posso capire che utilizzare Template in C++ sia macchinoso, ma non per chi e' abituato al linguaggio C/C++ (che sono macchinosi per principio). Ma i risultati sono fantastici, a tutti i livelli.

Per chi volesse un veloce tutorial della programmazione usando Templato consiglio questo articolo (c'e anche la parte seconda per quelli che vogliano continuare) che venne premiato come migliore articolo del 2011
https://www.codeproject.com/Articles/257589/An-Idiots-Guide-to-Cplusplus-Templates-Part-1
L'inizio dice tutto: "Most C++ programmers stay away from C++ templates due to their perplexed nature."
 
Quoto. Posso capire che utilizzare Template in C++ sia macchinoso, ma non per chi e' abituato al linguaggio C/C++ (che sono macchinosi per principio). Ma i risultati sono fantastici, a tutti i livelli.

E poi quando non è macchinoso per il programmatore, diventa macchinoso per la macchina. Non esistono pasti gratis da queste parti. Purtroppo spessissimo pure programmatori, non avvezzi alle complessità dei linguaggi, pensano erroneamente che i meccanismi "facili" per il programmatore non abbiano un prezzo da pagare. Il prezzo c'è e a volte è salatissimo e i programmatori di Digia potrebbero raccontarci belle storie, visto che lavorano fianco a fianco con C++ e Qml.
 
Ma tutto ciò ha un costo, in termini di cicli di cpu, memoria e di complessità dell'ambiente di runtime ( che ti porti appresso ). C++ non può concedersi questo lusso, da cui si capisce perchè non abbia seguito la strada percorsa da Java, C# e compagnia.
Va detto che però una probabile aggiunta in C++20 (o C++23) sarà proprio le Reflection.
Posso capire che utilizzare Template in C++ sia macchinoso, ma non per chi e' abituato al linguaggio C/C++ (che sono macchinosi per principio). Ma i risultati sono fantastici, a tutti i livelli.
Ma nessuno ha detto che i template sono pericolosi, complessi ecc... Io ne faccio vasto uso quando possibile ma qui si sta parlando del perché preferire quella struttura rispetto ad una funzione che viene validata a tempo di compilazione. Alla fine si dovrebbero creare una struttura per ciascun intero!
 
... perché preferire quella struttura rispetto ad una funzione che viene validata a tempo di compilazione. Alla fine si dovrebbero creare una struttura per ciascun intero!
??? Uno dei vantaggi dei template e' che vengono validati al tempo della compilazione. Infatti quello che fa il compilatore e' creare una funzione per ogni tipo di variabile per cui lo abbiamo utilizzato, usando method overloading. In pratica e' quello che faremmo (a mano) noi, evitando le possibilta' di sbagliare (che sono alte facendo taglia e cuci).
Il concetto dei Template e' proprio come uno dei tanti altri concetti tipico della programmazione. Da' al programmatore la possibilita' di usarlo. Sta al programmatore decidere SE usarlo, a seconda del problema e del contesto. Come in TUTTI gli altri casi. Prendi la ricorsione per esempio; e' stupenda per una limitata classe di problemi, e' orribile per altri. Lo stesso con i Template.
Prova a scrivere un Windows server usando COM. Hai due possibilita': MFC e ATL, entrambe funzionano, ma un server scritto usando ATL e' MILLE volte meglio. Il codice e' piu' piccolo e quindi piu' facile da mantenere, l'eseguibile ha dimensioni minuscole, va piu' veloce, e' piu facile da farne il debugging.
Non per nulla il concetto di Template e' stato esportato anche su C# (linguaggio molto strongly typed) con l'introduzione dei "generic"
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-methods
 
Pubblicità
Pubblicità
Indietro
Top