La regola del 5: usarlo o no?


20

La regola di 3 ( la regola di 5 nel nuovo standard c ++) afferma:

Se è necessario dichiarare esplicitamente il distruttore, il costruttore della copia o l'operatore di assegnazione della copia, è necessario dichiarare esplicitamente tutti e tre.

D'altra parte, il " codice pulito " di Martin consiglia di rimuovere tutti i costruttori e i distruttori vuoti (pagina 293, G12: Clutter ):

A che serve un costruttore predefinito senza implementazione? Tutto ciò che serve è ingombrare il codice con artefatti insignificanti.

Quindi, come gestire queste due opinioni opposte? I costruttori / distruttori vuoti dovrebbero davvero essere implementati?


Il prossimo esempio dimostra esattamente cosa intendo:

#include <iostream>
#include <memory>

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    ~A(){}
    A( const A & other ) : v( new int( *other.v ) ) {}
    A& operator=( const A & other )
    {
        v.reset( new int( *other.v ) );
        return *this;
    }

    std::auto_ptr< int > v;
};
int main()
{
    const A a( 55 );
    std::cout<< "a value = " << *a.v << std::endl;
    A b(a);
    std::cout<< "b value = " << *b.v << std::endl;
    const A c(11);
    std::cout<< "c value = " << *c.v << std::endl;
    b = c;
    std::cout<< "b new value = " << *b.v << std::endl;
}

Compila bene usando g ++ 4.6.1 con:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

Il distruttore per struct Aè vuoto e non proprio necessario. Quindi, dovrebbe essere lì o dovrebbe essere rimosso?


15
Le 2 citazioni parlano di cose diverse. O mi manca completamente il tuo punto.
Benjamin Bannier,

1
@honk Nello standard di codifica del mio team, abbiamo una regola per dichiarare sempre tutti e 4 (costruttore, distruttore, costruttori di copie). Mi chiedevo se avesse davvero senso fare. Devo davvero dichiarare sempre i distruttori, anche se sono vuoti?
BЈовић,

Per quanto riguarda i descrittori vuoti pensate a questo: codesynthesis.com/~boris/blog/2012/04/04/… . Altrimenti la regola del 3 (5) ha perfettamente senso per me, non ho idea del perché si vorrebbe una regola del 4.
Benjamin Bannier,

@honk Fai attenzione alle informazioni che trovi in ​​rete. Non tutto è vero. Ad esempio, virtual ~base () = default;non compilare (con una buona ragione)
BЈовић

@VJovic, No, non è necessario dichiarare un distruttore vuoto, a meno che non sia necessario renderlo virtuale. E mentre siamo sull'argomento, non dovresti usare auto_ptrneanche.
Dima,

Risposte:


44

Per cominciare la regola dice "probabilmente", quindi non si applica sempre.

Il secondo punto che vedo qui è che se devi dichiarare uno dei tre, è perché sta facendo qualcosa di speciale come allocare memoria. In questo caso, gli altri non sarebbero vuoti poiché avrebbero dovuto gestire la stessa attività (come copiare il contenuto della memoria allocata dinamicamente nel costruttore della copia o liberare tale memoria).

Quindi, in conclusione, non dovresti dichiarare costruttori o distruttori vuoti, ma è molto probabile che se uno è necessario, anche gli altri sono necessari.

Come per il tuo esempio: in tal caso, puoi lasciare fuori il distruttore. Non fa nulla, ovviamente. L'uso di puntatori intelligenti è un esempio perfetto di dove e perché la regola del 3 non regge.

È solo una guida su dove dare una seconda occhiata al tuo codice nel caso in cui potresti aver dimenticato di implementare funzionalità importanti che altrimenti avresti perso.


Con l'uso di puntatori intelligenti, i distruttori sono vuoti nella maggior parte dei casi (direi> il 99% dei distruttori nella mia base di codice è vuoto, perché quasi tutte le classi usano il linguaggio del pimpl).
BЈовић,

Wow, sono così tanti brufoli che lo definirei puzzolente. Con molti compilatori foraggiati sarà più difficile ottimizzare (ad es. Più difficile da incorporare).
Benjamin Bannier,

@honk Cosa intendi con "molti compilatori foraggiati"? :)
BЈовић,

@VJovic: scusa, errore di battitura: 'codice forato'
Benjamin Bannier,

4

Non c'è davvero nessuna contraddizione qui. La regola del 3 parla del distruttore, del costruttore della copia e dell'operatore di assegnazione della copia. Zio Bob parla di costruttori vuoti di default.

Se hai bisogno di un distruttore, allora la tua classe probabilmente contiene puntatori alla memoria allocata dinamicamente e probabilmente vorrai avere un ctor di copia e uno operator=()che esegua una copia profonda. Questo è completamente ortogonale al fatto che tu abbia bisogno o meno di un costruttore predefinito.

Si noti inoltre che in C ++ ci sono situazioni in cui è necessario un costruttore predefinito, anche se è vuoto. Supponiamo che la tua classe abbia un costruttore non predefinito. In tal caso, il compilatore non genererà un costruttore predefinito per te. Ciò significa che gli oggetti di questa classe non possono essere archiviati in contenitori STL, poiché tali contenitori si aspettano che gli oggetti siano predefinibili.

D'altra parte, se non hai intenzione di mettere mai gli oggetti della tua classe in contenitori STL, un costruttore predefinito vuoto è sicuramente un disordine inutile.


2

Qui il tuo potenziale (*) equivalente a quello predefinito di un costruttore / assegnazione / distruttore ha uno scopo: documentare il fatto che hai avuto riguardo al problema e determinare che il comportamento predefinito era corretto. A proposito, in C ++ 11, le cose non si sono stabilizzate abbastanza per sapere se =defaultpuò servire a tale scopo.

(Esiste un altro potenziale scopo: fornire una definizione fuori linea anziché quella in linea predefinita, meglio documentare esplicitamente se si hanno motivi per farlo).

(*) Potenziale perché non ricordo un caso di vita reale in cui la regola del tre non si applicava, se dovevo fare qualcosa in uno, dovevo fare qualcosa negli altri.


Modifica dopo aver aggiunto un esempio. il tuo esempio usando auto_ptr è interessante. Stai utilizzando un puntatore intelligente, ma non uno adatto al lavoro. Preferirei scriverne uno che è - soprattutto se la situazione si presenta spesso - piuttosto che fare quello che hai fatto. (Se non sbaglio, né lo standard né il boost ne forniscono uno).


L'esempio dimostra il mio punto. Il distruttore non è davvero necessario, ma la regola del 3 dice che dovrebbe essere lì.
BЈовић,

1

La regola di 5 è un'estensione cautelativa della regola di 3 che è un comportamento cautelativo contro un possibile uso improprio dell'oggetto.

Se hai bisogno di un distruttore, significa che hai fatto un po 'di "gestione delle risorse" diversa da quella predefinita (costruisci e distruggi solo i valori ).

Poiché copia, assegna, sposta e trasferisci in base ai valori di copia predefiniti , se non tieni solo i valori , devi definire cosa fare.

Detto questo, C ++ elimina la copia se si definisce lo spostamento ed elimina lo spostamento se si definisce la copia. Nella maggior parte dei casi è necessario definire se si desidera emulare un valore (quindi copiare cl clone la risorsa e spostare non ha senso) o un gestore risorse (e quindi spostare la risorsa, dove la copia non ha senso: la regola di 3 diventa la regola dell'altro 3 )

I casi in cui devi definire sia copia che sposta (regola del 5) sono piuttosto rari: in genere hai un "grande valore" che deve essere copiato se dato a oggetti distinti, ma può essere spostato se preso da un oggetto temporaneo (evitando un clone quindi distruggere ). È il caso dei contenitori STL o dei contenitori aritmetici.

Un caso può essere una matrice: devono supportare la copia perché sono valori, ( a=b; c=b; a*=2; b*=3;non devono influenzarsi a vicenda) ma possono essere ottimizzati supportando anche lo spostamento ( a = 3*b+4*cha un +che richiede due temporanei e genera un temporaneo: evitare cloni ed eliminazioni può essere utile)


1

Preferisco una diversa formulazione della regola del tre, che sembra più ragionevole, che è "se la tua classe ha bisogno di un distruttore (diverso da un distruttore virtuale vuoto) probabilmente ha anche bisogno di un costruttore di copie e di un operatore di assegnazione".

Specificarlo come relazione unidirezionale dal distruttore rende alcune cose più chiare:

  1. Non si applica nei casi in cui si fornisce un costruttore di copie non predefinito o un operatore di assegnazione solo come ottimizzazione.

  2. Il motivo della regola è che il costruttore di copie predefinito o l'operatore di assegnazione possono rovinare la gestione manuale delle risorse. Se gestisci manualmente le risorse, è probabile che ti sia reso conto che avrai bisogno di un distruttore per liberarle.


-3

C'è un altro punto non ancora menzionato nella discussione: un distruttore dovrebbe essere sempre virtuale.

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

Il costruttore deve essere dichiarato virtuale nella classe base per renderlo virtuale anche in tutte le classi derivate. Quindi, anche se la tua classe base non ha bisogno di un distruttore, finisci per dichiarare e implementare un distruttore vuoto.

Se inserisci tutti gli avvisi su (-Wall -Wextra -Weffc ++) g ++ ti avvertirà di questo. Ritengo sia buona norma dichiarare sempre un distruttore virtuale in qualsiasi classe, perché non si sa mai se la tua classe diventerà infine una classe base. Se il distruttore virtuale non è necessario, non danneggia. In tal caso, si risparmia tempo per trovare l'errore.


1
Ma non voglio il costruttore virtuale. Se lo faccio, quindi ogni chiamata a qualsiasi metodo utilizzerebbe l'invio virtuale. btw prendi nota che non esiste qualcosa come "costruttore virtuale" in c ++. Inoltre, ho compilato l'esempio come un livello di avviso molto elevato.
BЈовић,

IIRC, la regola che gcc usa per i suoi avvertimenti, e la regola che seguo comunque comunque, è che dovrebbe esserci un distruttore virtuale se ci sono altri metodi virtuali nella classe.
Jules il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.