Condividi tramite


Funzioni impostate come predefinite ed eliminate in modo esplicito

In C++11 le funzioni impostate come predefinite e le funzioni eliminate offrono il controllo esplicito sulla eventuale generazione automatica delle funzioni membro speciali. Le funzioni eliminate offrono anche un linguaggio semplice per impedire l'esecuzione di promozioni di tipi problematici in argomenti a funzioni di tutti i tipi, funzioni membro speciali e funzioni membro normali e funzioni non membro, che altrimenti causerebbero una chiamata di funzione indesiderata.

Vantaggi delle funzioni impostate come predefinite e delle funzioni eliminate

In C++, il compilatore genera automaticamente il costruttore predefinito, il costruttore di copia, l'operatore di assegnazione di copia e il distruttore per un tipo se non dichiara il proprio. Queste funzioni sono note come funzioni membro speciali e sono ciò che rendono semplici i tipi definiti dall'utente in C++ si comportano come le strutture in C. Ciò significa che è possibile creare, copiare ed eliminarli senza ulteriore sforzo di scrittura del codice. In C++11 è stata introdotta la semantica di spostamento nel linguaggio e aggiunge il costruttore di spostamento e l'operatore di assegnazione di spostamento all'elenco delle funzioni membro speciali che il compilatore è in grado di generare automaticamente.

Ciò risulta utile per i tipi semplici, ma i tipi complessi stessi definiscono spesso una o più funzioni membro speciali e questo può impedire la generazione automatica di altre funzioni membro speciali. In pratica:

  • Se un costruttore viene dichiarato in modo esplicito, non viene generato automaticamente alcun costruttore predefinito.

  • Se un distruttore virtuale viene dichiarato in modo esplicito, non viene generato automaticamente alcun distruttore predefinito.

  • Se un costruttore di spostamento o un operatore di assegnazione di spostamento viene dichiarato in modo esplicito:

    • Non viene generato automaticamente alcun costruttore di copia.

    • Non viene generato automaticamente alcun operatore di assegnazione di copia.

  • Se un costruttore di copia, un operatore di assegnazione di copia, un costruttore di spostamento, un operatore di assegnazione spostamento o un distruttore è dichiarato in modo esplicito:

    • Non viene generato automaticamente alcun costruttore di spostamento.

    • Non viene generato automaticamente alcun operatore di assegnazione di spostamento.

Nota

Lo standard C++11 specifica inoltre le regole aggiuntive seguenti:

  • Se un costruttore di copia o distruttore è dichiarato in modo esplicito, la generazione automatica dell'operatore di assegnazione di copia è deprecata.
  • Se un costruttore di assegnazione di copia o un distruttore è dichiarato in modo esplicito, la generazione automatica del costruttore di copia è deprecata.

In entrambi i casi, Visual Studio continua a generare automaticamente le funzioni necessarie in modo implicito e non genera un avviso per impostazione predefinita. Poiché Visual Studio 2022 versione 17.7, È possibile abilitare C5267 per generare un avviso.

Le conseguenze di tali regole possono inoltre comportare la creazione di gerarchie di oggetti. Ad esempio, se per qualsiasi motivo una classe base non riesce ad avere un costruttore predefinito chiamabile da una classe derivata, ovvero un public costruttore o protected che non accetta parametri, una classe che deriva da essa non può generare automaticamente il proprio costruttore predefinito.

Queste regole possono complicare l'implementazione di ciò che deve essere semplice, tipi definiti dall'utente e idiomi C++ comuni, ad esempio rendendo un tipo definito dall'utente non copiabile dichiarando il costruttore di copia e l'operatore di assegnazione di copia privatamente e non definendoli.

struct noncopyable
{
  noncopyable() {};

private:
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

Prima di C++11, questo frammento di codice era la forma idiotica di tipi non copiabili. Presenta tuttavia alcuni problemi:

  • Il costruttore di copia deve essere dichiarato privatamente per nasconderlo, ma poiché è dichiarato affatto, viene impedita la generazione automatica del costruttore predefinito. È necessario definire esplicitamente il costruttore predefinito, se lo si vuole usare, anche se non esegue alcuna operazione.

  • Anche se il costruttore predefinito definito in modo esplicito non esegue alcuna operazione, il compilatore lo considera nontriviale. È meno efficiente di un costruttore predefinito generato automaticamente e impedisce a noncopyable di essere effettivamente un tipo POD.

  • Anche se il costruttore di copia e l'operatore di assegnazione di copia sono invisibili al codice esterno, le funzioni membro e gli elementi Friend di noncopyable sono comunque in grado di visualizzarli e chiamarli. Se vengono dichiarati ma non definiti, la chiamata genera un errore del linker.

  • Anche se si tratta di un linguaggio comunemente accettato, la finalità non è chiara a meno che non si comprendano tutte le regole per la generazione automatica delle funzioni membro speciali.

In C++11, il linguaggio non copiabile può essere implementato in modo più semplice.

struct noncopyable
{
  noncopyable() =default;
  noncopyable(const noncopyable&) =delete;
  noncopyable& operator=(const noncopyable&) =delete;
};

Si noti il modo in cui vengono risolti i problemi relativi all'idioma pre-C++11:

  • La generazione del costruttore predefinito viene impedita dichiarando il costruttore di copia, ma è possibile ripristinarlo impostandolo in modo esplicito come predefinito.

  • Le funzioni membro speciali predefinite in modo esplicito sono ancora considerate semplici, quindi non esiste alcuna riduzione delle prestazioni e noncopyable non viene impedito di essere un vero tipo POD.

  • Il costruttore di copia e l'operatore di assegnazione di copia sono pubblici ma eliminati. Si tratta di un errore in fase di compilazione per definire o chiamare una funzione eliminata.

  • Lo scopo è chiaro a chiunque comprenda =default e =delete. Non è necessario comprendere le regole per la generazione automatica di funzioni membro speciali.

Esistono idiomi simili per rendere i tipi definiti dall'utente non rimovibili, che possono essere allocati in modo dinamico o che non possono essere allocati dinamicamente. Per ogni idioma sono disponibili implementazioni pre-C++11 che presentano problemi simili, risolti in modo analogo in C++11 mediante l'implementazione sotto forma di funzioni membro speciali impostate come predefinite ed eliminate.

Funzioni impostate come predefinite in modo esplicito

È possibile impostare per impostazione predefinita una delle funzioni membro speciali, in modo da dichiarare in modo esplicito che la funzione membro speciale usa l'implementazione predefinita, per definire la funzione membro speciale con un qualificatore di accesso non pubblico o per ripristinare una funzione membro speciale la cui generazione automatica è stata impedita da altre circostanze.

Per impostare come predefinita una funzione membro speciale, dichiararla come illustrato nell'esempio seguente:

struct widget
{
  widget()=default;

  inline widget& operator=(const widget&);
};

inline widget& widget::operator=(const widget&) =default;

Si noti che è possibile predefinito una funzione membro speciale all'esterno del corpo di una classe purché sia inlinable.

A causa dei vantaggi a livello di prestazioni offerti dalle funzioni membro speciali superflue, è consigliabile favorire le funzioni membro speciali generate automaticamente rispetto ai corpi di funzione vuoti quando si vuole ottenere il comportamento predefinito. È possibile ottenere questo risultato impostando esplicitamente come predefinita la funzione membro speciale o non dichiarandola e non dichiarando le altre funzioni membro speciali che ne impedirebbero la generazione automatica.

Funzioni eliminate

È possibile eliminare funzioni membro speciali e funzioni membro normali e funzioni non membro per impedirne la definizione o la chiamata. L'eliminazione di funzioni membro speciali consente di impedire al compilatore di generare funzioni membro speciali non desiderate. La funzione deve essere eliminata perché è dichiarata; non può essere eliminato in seguito nel modo in cui una funzione può essere dichiarata e quindi successivamente predefinita.

struct widget
{
  // deleted operator new prevents widget from being dynamically allocated.
  void* operator new(std::size_t) = delete;
};

L'eliminazione di funzioni membro normali o non membro impedisce la chiamata di una funzione non intenzionale. Questo procedimento funziona poiché le funzioni eliminate partecipano comunque alla risoluzione dell'overload e offrono una corrispondenza migliore rispetto alla funzione che può essere chiamata dopo la promozione dei tipi. La chiamata di funzione viene risolta nella funzione più specifica, ma eliminata e genera un errore del compilatore.

// deleted overload prevents call through type promotion of float to double from succeeding.
void call_with_true_double_only(float) =delete;
void call_with_true_double_only(double param) { return; }

Si noti che nell'esempio precedente la chiamata call_with_true_double_only tramite un float argomento provocherebbe un errore del compilatore, ma la chiamata call_with_true_double_only tramite un int argomento non sarebbe. Nel caso in cui int l'argomento verrà alzato di livello da int a double e chiamare correttamente la double versione della funzione, anche se ciò potrebbe non essere quello previsto. Per assicurarsi che qualsiasi chiamata a questa funzione tramite un argomento non doppio causi un errore del compilatore, è possibile dichiarare una versione del modello della funzione eliminata.

template < typename T >
void call_with_true_double_only(T) =delete; //prevent call through type promotion of any T to double from succeeding.

void call_with_true_double_only(double param) { return; } // also define for const double, double&, etc. as needed.