Nota: il seguente è il codice C ++ 03, ma prevediamo un passaggio a C ++ 11 nei prossimi due anni, quindi dobbiamo tenerlo presente.
Sto scrivendo una linea guida (per i neofiti, tra gli altri) su come scrivere un'interfaccia astratta in C ++. Ho letto entrambi gli articoli di Sutter sull'argomento, ho cercato su Internet esempi e risposte e ho fatto alcuni test.
Questo codice NON deve essere compilato!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Tutti i comportamenti sopra citati trovano l'origine del loro problema nell'affettare : l'interfaccia astratta (o la classe non foglia nella gerarchia) non dovrebbe essere costruibile né copiabile / assegnabile, ANCHE se la classe derivata può esserlo.
0a soluzione: l'interfaccia di base
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Questa soluzione è semplice e in qualche modo ingenua: non riesce a superare tutti i nostri vincoli: può essere costruita per impostazione predefinita, costruita per copia e assegnata per copia (non sono nemmeno sicuro di spostare costruttori e assegnazione, ma ho ancora 2 anni per capire fuori).
- Non possiamo dichiarare il distruttore puro virtuale perché dobbiamo tenerlo in linea e alcuni dei nostri compilatori non digeriranno i metodi virtuali puri con il corpo vuoto in linea.
- Sì, l'unico punto di questa classe è rendere gli attuatori praticamente distruttibili, il che è un caso raro.
- Anche se avessimo un metodo puro virtuale aggiuntivo (che è la maggior parte dei casi), questa classe sarebbe comunque assegnabile alla copia.
Quindi no ...
1a soluzione: boost :: non copiabile
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Questa soluzione è la migliore, perché è semplice, chiara e C ++ (senza macro)
Il problema è che non funziona ancora per quella specifica interfaccia perché VirtuallyConstructible può ancora essere costruito di default .
- Non possiamo dichiarare il distruttore puro virtuale perché dobbiamo tenerlo in linea e alcuni dei nostri compilatori non lo digeriranno.
- Sì, l'unico punto di questa classe è rendere gli attuatori praticamente distruttibili, il che è un caso raro.
Un altro problema è che le classi che implementano l'interfaccia non copiabile devono quindi dichiarare / definire esplicitamente il costruttore della copia e l'operatore di assegnazione se devono avere quei metodi (e nel nostro codice, abbiamo classi di valore a cui il nostro cliente può ancora accedere tramite interfacce).
Questo va contro la Regola di Zero, che è dove vogliamo andare: se l'implementazione di default è ok, dovremmo essere in grado di usarla.
2a soluzione: rendili protetti!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Questo modello segue i vincoli tecnici che avevamo (almeno nel codice utente): MyInterface non può essere costruito in modo predefinito, non può essere costruito in copia e non può essere assegnato alla copia.
Inoltre, non impone alcun vincolo artificiale all'implementazione delle classi , che sono quindi libere di seguire la Regola dello Zero, o addirittura dichiarare alcuni costruttori / operatori come "= default" in C ++ 11/14 senza problemi.
Ora, questo è abbastanza dettagliato, e un'alternativa sarebbe usare una macro, qualcosa del tipo:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
Il protetto deve rimanere all'esterno della macro (perché non ha ambito).
Correttamente "namespace" (ovvero, con il prefisso del nome dell'azienda o del prodotto), la macro dovrebbe essere innocua.
E il vantaggio è che il codice viene preso in considerazione in una fonte, anziché essere incollato in tutte le interfacce. Se il costruttore di mosse e l'assegnazione di mosse dovessero essere esplicitamente disabilitati allo stesso modo in futuro, si tratterebbe di un leggero cambiamento nel codice.
Conclusione
- Sono paranoico per volere che il codice sia protetto contro lo slicing nelle interfacce? (Credo di no, ma non si sa mai ...)
- Qual è la migliore soluzione tra quelle sopra?
- C'è un'altra soluzione migliore?
Ricorda che questo è uno schema che servirà da guida per i neofiti (tra gli altri), quindi una soluzione come: "Ogni caso dovrebbe avere la sua implementazione" non è una soluzione praticabile.
Bounty e risultati
Ho assegnato la grazia a coredump a causa del tempo impiegato per rispondere alle domande e della pertinenza delle risposte.
La mia soluzione al problema probabilmente andrà a qualcosa del genere:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... con la seguente macro:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Questa è una soluzione praticabile per il mio problema per i seguenti motivi:
- Questa classe non può essere istanziata (i costruttori sono protetti)
- Questa classe può essere praticamente distrutta
- Questa classe può essere ereditata senza imporre vincoli indebiti sull'ereditarietà delle classi (ad esempio, la classe ereditaria potrebbe essere di default copiabile)
- L'uso della macro significa che la "dichiarazione" dell'interfaccia è facilmente riconoscibile (e ricercabile), e il suo codice è preso in considerazione in un unico punto, facilitando la modifica (un nome opportunamente prefissato rimuoverà gli scontri indesiderabili)
Si noti che le altre risposte hanno fornito preziose informazioni. Grazie a tutti voi che ci avete provato.
Nota che credo di poter ancora dare un altro premio a questa domanda, e apprezzo abbastanza le risposte illuminanti che dovrei vederne una, vorrei aprire un premio solo per assegnarlo a quella risposta.
virtual ~VirtuallyDestructible() = 0
l'eredità virtuale delle classi di interfaccia (solo con membri astratti). Potresti omettere che Virtualmente Distruttibile, probabilmente.
virtual void bar() = 0;
per esempio? Ciò impedirebbe l'installazione della tua interfaccia.