Ti prego, smettila di dire che conosci il C/C++!
(Nota: questo non è uno sproloquio, ma sproloquierò nella prefazione. Mi dispiace.)
Prefazione
Ok, rilassati. Fai un respiro profondo. Ora, ascoltami (leggimi): non esiste nessun linguaggio denominato C/C++. C’è il C, ci sono i C++, e sono tutti linguaggi diversi.
Potresti conoscerne uno di questi, forse anche tutti, ma dire che conosci il “C/C++” non ha nessun senso.
Scriveresti mai nel tuo curriculum che conosci, ad esempio, il “C/Objective-C”? O il “Java/Kotlin”? Se la tua risposta è “certo che no, sono linguaggi diversi!”, la risposta è giusta e quindi dovresti anche smetterla di scrivere “C/C++”. Mettici una maledetta virgola per separare due linguaggi diversi.
Aspetta, percepisco qualcuno dire:
Ma nel mio caso ha senso perché…
In generale, se ha senso, ha senso anche scrivere “C, C++”. Forse nel tuo caso specifico ha davvero senso…ne parlerò nell’appendice di questo articolo. Sopportami fino ad allora, per favore.
Perché scrivere questo articolo.
Mi è capitato innumerevoli volte di vedere curricula, pagine personali e quant’altro in cui una persona dicesse di conoscere il “C/C++”. Non posso generalizzare per ognuna di queste, ma per le persone con le quali ho avuto a che fare di persona, o conoscevano decentemente il C e un po’ di C++98, oppure conoscevano il C++98 (a volte il C++11) ed erano convinti di poter scrivere codice in C con un minimo sforzo.
I primi non erano sviluppatori C++. I secondi non erano sviluppatori C.
Non è un crimine non conoscere un linguaggio o conoscerlo solo in minima parte, sia chiaro. Ma a mio avviso ognuno dovrebbe almeno sapere cosa sa e cosa non sa.
Cosa ha a che fare tutto ciò con i videogiochi?
Alcuni dei più diffusi linguaggi di programmazione nell’industria dei videogiochi sono i C++, soprattutto i motori grafici. Sapere le principali differenze e riuscire a distinguere i vari C++ può essere di grande aiuto.
Non mi metterò a parlare di quali siano queste differenze (non ora, almeno!), ma spero almeno di far luce sulla loro esistenza.
Perché continui a dire i C++
Ci arriveremo.
Il codice C è sempre codice valido in C++?
Ok, ecco un piccolo snippet:
int bar(int* arr, const int size);
int foo(const int size) {
int arr[size];
for(int i = 0; i < size; i++) arr[i] = 0;
return bar(arr, size);
}
Lasciamo perdere cosa faccia la funzione bar()
e concentriamoci su foo()
. Facciamoci quindi due domande:
- È codice C valido?
- È codice C++ valido?
Se le risposte non sono state:
- Dipende.
- Dipende.
…be’, avete sbagliato.
Diciamo che vogliamo essere pedanti, cioè vogliamo attenerci strettamente alle specifiche ISO dei linguaggi. Possiamo aggiungere -pedantic
a qualunque compilatore vogliamo (tranne MSVC, ne parleremo dopo).
Ora le risposte sono:
- Dipende, di nuovo.
- No.
Hai indovinato? Non hai indovinato e non mi credi? Ecco qui:
Usando l’ultima versione di gcc
e compilando con -pedantic -Werror
…i VLA (Variable-Length Array) non sono codice ISO C++ valido. In altre parole, se dichiariamo un array C-style in C++, la sua dimensione deve essere nota a compile time, non possiamo dichiararla tramite variabile.
Ok, ma è codice C valido, giusto? Be’….
…non è sempre così. Se siamo vincolati a usare il C90 c’è poco da fare, i VLA non sono codice C90 valido. In realtà, GCC ci permette di usarli se non siete -pedantic
, ma se stiamo usando un compilatore come MSVC non possiamo proprio farci niente, niente VLA, né in C né in C++.
Ma…cosa sono i VLA? Sono solo array di lunghezza variabile (sic!) allocati sullo stack. Hai mai sentito parlare della funzione alloca()
? A meno che tu non abbia dovuto avere a che fare con qualche progetto in ANSI C abbastanza corposo, probabilmente no, e ne hai ben donde.
alloca
funziona come malloc
, ma sullo stack! Bello, possiamo simulare i VLA nel caro vecchio C90, giusto…?
…come scusa‽‽‽
Be’, se sei abituato a MSVC (in modalità C++) o al C99, o se pensavi di poter scrivere codice in C perché conosci il C++…siamo arrivati al punto della questione.
In C90 non possiamo dichiarare variabili nella dichiarazione di un ciclo for
. E non è tutto, non possiamo dichiarare variabili da nessuna parte se non all’inizio di un blocco. Tutte le dichiarazioni devono avvenire prima di qualunque statement
Nemmeno questo snippet è valido in C90:
{
int i = 0;
foo(i);
int j = 1
bar(j);
}
Perché abbiamo dichiarato j
dopo lo statement foo(i)
.
Ora, finalmente, ecco il maledetto codice sistemato per funzionare in C90:
Confusi? Be’, è comprensibile. Si potrebbe pensare che il C90 è vecchio e deprecato…ma non è così. Nel 2015 ho dovuto lavorare con del codice in C90 scritto quello stesso anno. Certo, abbiamo migrato poco dopo al C99, ma per un po’ ci ho dovuto avere a che fare. Ed era codice che doveva girare su una normalissima e recente piattaforma x86-64. Un normalissimo PC.
NOTA: se volessimo essere davvero pedanti, in C11 i VLA sarebbero opzionali e non obbligatori. Ma non sono riuscito a trovare un singolo compilatore C11 che non li supportasse. Siamo stati fortunati.
Approfondiamo le differenze.
Bene! Scriverò sul mio CV che conosco il “C, C++” e sarò a posto!
Be’…no. Non ancora.
C
Ecco un’altra domanda.
- Il C supporta l’overload?
La risposta, di nuovo, è:
- Dipende.
Innanzitutto, clang ha __attribute__((overloadable))
. GCC, purtroppo, non ha questo fantastico attributo.
Ma eravamo -pedantic
giusto?
- L’ISO C supporta l’overload?
Ovviamente…
- Dipende.
Mai sentito parlare delle _Generic
selections? Ti suona familiare questa sintassi?
#define foo(val) _Generic((val), \
int: foo_i, \
char: foo_c, \
double: foo_d )(val)
Se la risposta è no, non conosci il C11. (E ovviamente, questo non è codice C++ valido, non importa quante flag proviamo a settare.)
Se restiamo nel territorio del C, un linguaggio che evolve molto lentamente, scrivere semplicemente che si conosce il “C”, va benissimo. Anche se non si è abituati alle nuove caratteristiche del C11, siccome l’unica nuova sintassi da imparare sarebbe quella delle _Generic
…ci si adatta abbastanza in fretta. Se tu fossi un esperto sviluppatore C99 e avessi bisogno di qualcuno per il mio progetto in C11, saresti più o meno allo stesso livello di uno sviluppatore C11 di pari esperienza.
C++
Diciamo che sei uno sviluppatore C di media esperienza e che durante l’università hai imparato cosa sono classi e oggetti e le hai usate con un compilatore C++ (magari Dev-C++!).
Quindi sai cosa sono classi e metodi. Ma da quel corso in C++ sei passato a Java, C# o qualche altro linguaggio di livello “più alto”. E visto che la OOP non ti è ignota, che conosci il C e che hai lavorato un po’ col C++ in passato, scrivi quel bel “C++” nel tuo CV.
Se qualcuno lo leggesse saprebbe immediatamente che non conosci il C++ e che forse conosci un po’ C++98 mischiato col C. O, con un po’ di fortuna, un po’ di C++11.
Ti ricori quando continuavo a scrivere i C++ invece de il C++?
Ecco una lista di qualche feature disponibile ad oggi, con il C++20, senza nessun ordine specifico (contate quante ne conoscete):
- Classi e oggetti
- Template
constexpr
consteval
- Static assertions
- Lambda
- Templated lambda
- Fold expressions
- Concepts
- Non-type template parameters
- Smart pointers
std::variant
- Structured binding
auto
(non nel senso di automatic storage)
Quanti punti hai fatto? Se meno di 8, non conosci completamente nemmeno il C++11. Inoltre, considera che queste caratteristiche sono state introdotte in diverse versioni, quindi avrai a disposizione un sottoinsieme diverso per ogni versione scelta.
Potresti pensare che sono troppo pignolo, e che puoi tranquillamente muovervi da una versione all’altra con un po’ di sforzo.
Il che è vero (soprattutto che sono pignolo), ma sarebbe vero per qualunque linguaggio. E se avessi bisogno di assumere qualcuno con una buona esperienza in C++17, la conoscenza di un sottoinsieme del C++98 non sarebbe nemmeno lontanamente sufficiente. Perché oggi, il codice C++ può essere una roba del genere:
template<typename T>
concept Meowable = requires(T a) { a.meow(); };
template<Meowable ...Args>
consteval int meow_count(Args&&... args) {
return (args.meow() + ...);
}
struct Cat { constexpr int meow() { return 1; } };
int foo() {
return meow_count(Cat{}, Cat{}, Cat{}, Cat{});
}
E questo snippet genererebbe solo quattro (4) righe di assembly e solo per la funzione foo()
, e ritornerebbe semplicemente 4, senza nessun calcolo a run-time. Sarebbe fatto tutto a compile-time. E senza il bisogno di attivare alcuna ottimizzazione. Sarebbe compile-time per standard, nel senso che ogni compilatore deve generare quel tipo di assembly.
Ecco questo snippet compilato con gcc
10.1 e con le flag -std=c++20 -fconcepts -O0 -pedantic -Werror
Questa “roba” non assomiglia nemmeno da lontano a quello che avrebbe potuto vedere un programmatore C++ degli anni ‘90. È un mostro completamente diverso, nel bene o nel male.
In realtà, quella roba lì non assomiglia nemmeno a codice C++14.
Postfazione
Spero che ora sia più chiaro quanta differenza esiste tra il C e il C++ e tra le varie versioni di C++.
Ovviamente non è necessario scrivere nel CV ogni singola maledetta versione di C++ che si conosce. Mi sembra ragionevole, ad esempio, scrivere che si conosce il C++11/14 o il C++17/20.
E non fraintendermi, va benissimo conoscere solo il C++98 perché hai frequentato un corso di OOP al primo anno di università. Se hai esperienza con altri linguaggi e come sviluppatore in generale, potresti essere un valido sviluppatore C++20 con un minimo di impegno.
Ma è anche importante sapere cosa si sa e cosa non si sa (quante volte ho usato il verbo sapere finora?). Questo perché non solo è difficile avanzare verso le versioni più recenti di C++, ma è anche parimenti difficile “regredire” verso le più vecchie.
Se mai dovessi trovarmi obbligato a usare un vecchio compilatore limitato al C++98, cercherei di utilizzare direttamente il C. Non perché il C++98 sia un brutto linguaggio, ma perché io lavoro principalmente con il C (per lavoro) e con il C++17/20 (nei miei side project). E onestamente non ho idea di come sia composta la mia “cassetta degli attrezzi” in C++98. Ci sono i non-type template parameter? Quali parti della STL sono disponibili? So che non ci sono gli smart pointer…ma posso usare boost? I delegate constructor non ci sono…giusto?
Infine, un suggerimento: se devi imparare il C++ per entrare nella scena dello sviluppo videoludico, impara un C++ moderno e tuffati direttamente nel C++17, almeno. Anche il C++20, visto che ormai è completo e il supporto nei maggiori compilatori è alle battute finali.
Un paio di motori grafici C++17-ready:
- Godot compila senza problemi in C++17.
- Anche Unreal Engine 4.
Qualcosa di open-source? Eccoti accontentato!
- OpenRCT2 richiede C++17!
- Halley è un altro motore interessanto, fatto in C++17.
- Anche
EnTT
richiede C++17. Be’, in realtà c’è il tagcpp14
, ma l’ultimo commit è del 2 Settembre 2018.
TL;DR
Ti prego, smettila di scrivere “C/C++” e specifica quale versione del C++ conosci!
Appendice: unire C e C++
Magari usi un sacco di extern "C" {}
e mischi codice C e C++ per una qualunque ragione. Programmazione embedded, vecchie librerie…
Magari sei specializzato nel combinare codice C e C++, una cosa che non tutti sanno fare…
Nel tuo caso, “C/C++” sarebbe effettivamente la cosa giusta da scrivere. Ma tutti sappiamo che non è quello che la maggior parte delle persone intende. Tristemente, a causa dell’abuso che si fa di questa notazione, sarebbe megliore scrivere qualcosa di più originale (e verboso):
Linguaggi di programmazione: C, C++14, interfaccia C/C++
Ecco fatto.