(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:

  1. È codice C valido?
  2. È codice C++ valido?

Se le risposte non sono state:

  1. Dipende.
  2. 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:

  1. Dipende, di nuovo.
  2. 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.

  1. Il C supporta l’overload?

La risposta, di nuovo, è:

  1. Dipende.

Innanzitutto, clang ha __attribute__((overloadable)). GCC, purtroppo, non ha questo fantastico attributo.

Ma eravamo -pedantic giusto?

  1. L’ISO C supporta l’overload?

Ovviamente…

  1. 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:

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 tag cpp14, 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.