Bentornato in C++ - C++ moderno

Dalla sua creazione, C++ è diventato uno dei linguaggi di programmazione più diffusi al mondo. I programmi in C++ ben scritti sono veloci ed efficienti. Il linguaggio è più flessibile rispetto ad altri linguaggi: può funzionare ai livelli più elevati di astrazione e giù al livello del processore. C++ fornisce librerie standard altamente ottimizzate. Consente l'accesso alle funzionalità hardware di basso livello, per ottimizzare la velocità e ridurre al minimo i requisiti di memoria. C++ può creare quasi qualsiasi tipo di programma: giochi, driver di dispositivo, HPC, cloud, desktop, embedded e app per dispositivi mobili e molto altro ancora. Anche librerie e compilatori per altri linguaggi di programmazione vengono scritti in C++.

Uno dei requisiti originali di C++ era la compatibilità con le versioni precedenti del linguaggio C. Di conseguenza, C++ ha sempre consentito la programmazione in stile C, con puntatori non elaborati, matrici, stringhe di caratteri con terminazione Null e altre funzionalità. Possono abilitare prestazioni elevate, ma possono anche generare bug e complessità. L'evoluzione di C++ ha sottolineato le funzionalità che riducono notevolmente la necessità di usare idiomi in stile C. Le vecchie strutture di programmazione C sono ancora lì quando sono necessarie. Tuttavia, nel codice C++ moderno è necessario che siano meno e meno. Il codice C++ moderno è più semplice, più sicuro, più elegante e sempre più veloce.

Le sezioni seguenti offrono una panoramica delle funzionalità principali di C++. Se non diversamente specificato, le funzionalità elencate di seguito sono disponibili in C++11 e versioni successive. Nel compilatore Microsoft C++ è possibile impostare l'opzione /std del compilatore per specificare la versione dello standard da usare per il progetto.

Risorse e puntatori intelligenti

Una delle classi principali di bug nella programmazione in stile C è la perdita di memoria. Le perdite sono spesso causate da un errore di chiamata delete per la memoria allocata con new. C++ moderno sottolinea il principio dell'acquisizione delle risorse è l'inizializzazione (RAII). L'idea è semplice. Le risorse (memoria heap, handle di file, socket e così via) devono essere di proprietà di un oggetto . L'oggetto crea o riceve la risorsa appena allocata nel costruttore e la elimina nel distruttore. Il principio di RAII garantisce che tutte le risorse vengano restituite correttamente al sistema operativo quando l'oggetto proprietario esce dall'ambito.

Per supportare facilmente l'adozione dei principi RAII, la libreria standard C++ offre tre tipi di puntatore intelligente: std::unique_ptr, std::shared_ptre std::weak_ptr. Un puntatore intelligente gestisce l'allocazione e l'eliminazione della memoria di cui è proprietario. Nell'esempio seguente viene illustrata una classe con un membro di matrice allocato nell'heap nella chiamata a make_unique(). Le chiamate a new e delete vengono incapsulate dalla unique_ptr classe . Quando un widget oggetto esce dall'ambito, verrà richiamato il distruttore unique_ptr e rilascia la memoria allocata per la matrice.

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // lifetime automatically tied to enclosing scope
                        // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

Quando possibile, usare un puntatore intelligente per gestire la memoria dell'heap. Se è necessario utilizzare gli new operatori e delete in modo esplicito, seguire il principio di RAII. Per altre informazioni, vedere Durata degli oggetti e gestione delle risorse (RAII).

std::string e std::string_view

Le stringhe in stile C sono un'altra fonte principale di bug. std::string Usando e std::wstring, è possibile eliminare praticamente tutti gli errori associati alle stringhe di tipo C. È anche possibile sfruttare i vantaggi delle funzioni membro per la ricerca, l'aggiunta, la pre-attesa e così via. Entrambi sono altamente ottimizzati per la velocità. Quando si passa una stringa a una funzione che richiede solo l'accesso in sola lettura, in C++17 è possibile usare std::string_view per ottenere prestazioni ancora migliori.

std::vector e altri contenitori della libreria standard

Tutti i contenitori della libreria standard seguono il principio di RAII. Forniscono iteratori per l'attraversamento sicuro degli elementi. E sono altamente ottimizzati per le prestazioni e sono stati testati accuratamente per la correttezza. Usando questi contenitori, si eliminano i potenziali bug o inefficienze che potrebbero essere introdotti nelle strutture di dati personalizzate. Anziché matrici non elaborate, usare vector come contenitore sequenziale in C++.

vector<string> apples;
apples.push_back("Granny Smith");

Usare map (non unordered_map) come contenitore associativo predefinito. Usare set, multimape multiset per degenerare e più casi.

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

Quando è necessaria l'ottimizzazione delle prestazioni, è consigliabile usare:

  • Contenitori associativi non ordinati, unordered_mapad esempio . Questi hanno un sovraccarico per elemento inferiore e una ricerca in tempo costante, ma possono essere più difficili da usare correttamente ed in modo efficiente.
  • Ordinamento di vector. Per altre informazioni, vedere Algoritmi.

Non usare matrici di tipo C. Per le API meno recenti che richiedono l'accesso diretto ai dati, usare metodi di accesso, ad esempio f(vec.data(), vec.size()); . Per altre informazioni sui contenitori, vedere Contenitori della libreria standard C++.

Algoritmi di libreria standard

Prima di presupporre che sia necessario scrivere un algoritmo personalizzato per il programma, esaminare prima di tutto gli algoritmi della libreria standard C++. La libreria standard contiene un assortimento sempre crescente di algoritmi per molte operazioni comuni, ad esempio la ricerca, l'ordinamento, il filtro e la casualità. La libreria matematica è estesa. In C++17 e versioni successive vengono fornite versioni parallele di molti algoritmi.

Ecco alcuni esempi importanti:

  • for_each, l'algoritmo di attraversamento predefinito (insieme ai cicli basati su for intervallo).
  • transform, per la modifica non sul posto degli elementi del contenitore
  • find_if, l'algoritmo di ricerca predefinito.
  • sort, lower_bounde gli altri algoritmi di ordinamento e ricerca predefiniti.

Per scrivere un comparatore, usare strict < e usare espressioni lambda denominate quando è possibile.

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

auto anziché nomi di tipi espliciti

C++11 ha introdotto la auto parola chiave da usare nelle dichiarazioni di variabili, funzioni e modelli. auto indica al compilatore di dedurre il tipo dell'oggetto in modo che non sia necessario digitarlo in modo esplicito. auto è particolarmente utile quando il tipo dedotto è un modello annidato:

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

Cicli basati su for intervalli

L'iterazione in stile C su matrici e contenitori è soggetta a errori di indicizzazione ed è anche noiosa per il tipo. Per eliminare questi errori e rendere il codice più leggibile, usare cicli basati su for intervalli con contenitori della libreria Standard e matrici non elaborate. Per altre informazioni, vedere Istruzione basata su for intervallo.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

constexpr espressioni anziché macro

Le macro in C e C++ sono token elaborati dal preprocessore prima della compilazione. Ogni istanza di un token di macro viene sostituita con il valore o l'espressione definiti prima della compilazione del file. Le macro vengono comunemente usate nella programmazione in stile C per definire valori costanti in fase di compilazione. Tuttavia, le macro sono soggette a errori e difficili da eseguire per il debug. In C++ moderno è consigliabile preferire constexpr le variabili per le costanti in fase di compilazione:

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

Inizializzazione uniforme

In C++ moderno è possibile usare l'inizializzazione tra parentesi graffe per qualsiasi tipo. Questa forma di inizializzazione è particolarmente utile durante l'inizializzazione di matrici, vettori o altri contenitori. Nell'esempio seguente viene v2 inizializzato con tre istanze di S. v3 viene inizializzato con tre istanze di S che vengono inizializzate usando parentesi graffe. Il compilatore deduce il tipo di ogni elemento in base al tipo dichiarato di v3.

#include <vector>

struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

Per altre informazioni, vedere Inizializzazione tra parentesi graffe.

Semantica di spostamento

C++ moderno fornisce la semantica di spostamento, che consente di eliminare copie di memoria non necessarie. Nelle versioni precedenti della lingua, le copie erano inevitabili in determinate situazioni. Un'operazione di spostamento trasferisce la proprietà di una risorsa da un oggetto al successivo senza creare una copia. Alcune classi possiedono risorse come memoria heap, handle di file e così via. Quando si implementa una classe proprietaria di risorse, è possibile definire un costruttore di spostamento e un operatore di assegnazione di spostamento per tale classe. Il compilatore sceglie questi membri speciali durante la risoluzione dell'overload in situazioni in cui non è necessaria una copia. I tipi di contenitore libreria standard richiamano il costruttore di spostamento sugli oggetti se ne è definito uno. Per altre informazioni, vedere Spostare costruttori e operatori di assegnazione di spostamento (C++).

Espressioni lambda

Nella programmazione in stile C, una funzione può essere passata a un'altra funzione usando un puntatore a funzione. I puntatori a funzione sono scomodi da gestire e comprendere. La funzione a cui fanno riferimento può essere definita altrove nel codice sorgente, lontano dal punto in cui viene richiamato. Inoltre, non sono indipendenti dai tipi. Modern C++ fornisce oggetti funzione, classi che eseguono l'override dell'operatore operator() , che consentono di chiamarle come una funzione. Il modo più pratico per creare oggetti funzione è con espressioni lambda inline. Nell'esempio seguente viene illustrato come usare un'espressione lambda per passare un oggetto funzione, che la find_if funzione richiamerà su ogni elemento del vettore:

    std::vector<int> v {1,2,3,4,5};
    int x = 2;
    int y = 4;
    auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

L'espressione [=](int i) { return i > x && i < y; } lambda può essere letta come "funzione che accetta un singolo argomento di tipo int e restituisce un valore booleano che indica se l'argomento è maggiore x e minore di y". Si noti che le variabili x e y dal contesto circostante possono essere usate nell'espressione lambda. [=] Specifica che tali variabili vengono acquisite in base al valore; in altre parole, l'espressione lambda dispone di copie personalizzate di tali valori.

Eccezioni

C++ moderno enfatizza le eccezioni, non i codici di errore, come il modo migliore per segnalare e gestire le condizioni di errore. Per altre informazioni, vedere Procedure consigliate C++ moderne per le eccezioni e la gestione degli errori.

std::atomic

Usare lo struct della libreria std::atomic standard C++ e i tipi correlati per i meccanismi di comunicazione tra thread.

std::variant (C++17)

Le unioni vengono comunemente usate nella programmazione in stile C per risparmiare memoria consentendo ai membri di tipi diversi di occupare la stessa posizione di memoria. Tuttavia, le unioni non sono indipendenti dai tipi e sono soggette a errori di programmazione. C++17 introduce la std::variant classe come alternativa più affidabile e sicura alle unioni. La std::visit funzione può essere usata per accedere ai membri di un variant tipo in modo indipendente dai tipi.

Vedi anche

Riferimenti al linguaggio C++
Espressioni lambda
Libreria standard C++
Conformità del linguaggio Microsoft C/C++