Sovrascrivere le funzioni virtuali pure ‘utilizzando’ un metodo ereditato separatamente

Solo un piccolo fastidio perché posso aggirare il problema avvolgendo la funzione derivata invece di usare la parola chiave ‘using’, ma perché non funziona (il compilatore mi dice che ‘get_elem’ è ancora puro virtuale in ‘Bar’ class).

class Elem {}; class DerivedElem : public Elem {}; class Foo { public: virtual Elem& get_elem() = 0; }; class Goo { protected: DerivedElem elem; public: DerivedElem& get_elem() { return elem; } }; class Bar : public Foo, public Goo { public: using Goo::get_elem; }; int main(void) { Bar bar; } 

Saluti,

Tom

Se Goo è un “mixin” progettato per implementare l’interfaccia Foo in un modo particolare (potrebbero esserci altri mixin con altre implementazioni), allora Goo può derivare da Foo (invece di Bar farlo).

Se Goo non è progettato per implementare l’interfaccia Foo, sarebbe un errore terribile trattare Bar come se avesse implementato quella pura funzione virtuale, quando infatti sembra che abbia una funzione della stessa firma. Se vuoi le interfacce implicite e la digitazione “duck” in C ++ puoi farlo, ma devi farlo con i template. A torto oa ragione, le funzioni virtuali pure sono per interfacce esplicitamente dichiarate e la funzione get_elem di Goo non è dichiarata esplicitamente per implementare Foo::get_elem . Quindi non è così.

Immagino che questo non spieghi perché in linea di massima il linguaggio non possa definire using Goo::get_elem for Foo; , o alcune di tali dichiarazioni in Bar, per evitare che Bar abbia bisogno di contenere un gran numero di hotplan che avvolge la chiamata.

Puoi forse fare qualcosa con i modelli per consentire a Goo di supportarlo in una certa misura, senza veramente conoscere Foo:

 template  class Goo : public T { protected: DerivedElem elem; public: DerivedElem& get_elem() { return elem; } }; class Bar : public Goo {}; class Baz : public Goo {}; 

Dove Fuu è un’altra interfaccia che ha una funzione get_elem . Ovviamente è quindi responsabilità dell’autore di Bar assicurarsi che Goo realmente il contratto di Foo , e lo stesso per Baz controlla il contratto di Fuu .

A proposito, questa forma di covarianza è un po ‘dubbia. Guardando a Foo, qualcuno potrebbe aspettarsi che l’espressione bar.get_elem() = Elem() sia valida, e non lo è, quindi LSP è violato. I riferimenti sono divertenti come quello. ((Foo &)bar).get_elem() = Elem() è valido ma in generale non funziona! Assegna solo all’object secondario Elem e, in tal caso, fa ((Foo &)bar).get_elem() = DerivedElem() . L’assegnazione polimorfica è fondamentalmente una seccatura.

Nel tuo esempio, Foo e Goo sono classi separate. In Bar, il metodo get_elem di Goo non è affatto uguale a quello di Foo, anche se la loro firma corrisponde.

Usando using Goo::get_elem , devi semplicemente dire al compilatore di risolvere la chiamata non qualificata a get_elem () a quella di Goo.

Hai incontrato uno dei tanti strani angoli del C ++. In questo caso, C ++ non considera due funzioni virtuali ereditate da classi diverse come la stessa funzione, anche se hanno lo stesso nome e la stessa firma.

Ci sono alcuni buoni motivi per cui il C ++ agisce in questo modo. Ad esempio, è frequente il caso che quelle due funzioni in realtà non siano le stesse, nonostante abbiano lo stesso nome e la stessa firma del tipo. Il significato semantico delle due funzioni è diverso.

Ecco un esempio:

 namespace vendor1 { class Circle { public: virtual ::std::size_t size() const { return sizeof(*this); } }; } // namespace vendor1 namespace vendor2 { class Shape { public: virtual double size() const = 0; }; class Circle : public Shape { public: virtual double size() const { return radius_ * radius_ * M_PI; } }; } // namespace vendor2 

E poi provi questo:

 namespace my_namespace { class Circle : public ::vendor1::Circle, public ::vendor2::Circle { // Oops, there is no good definition for size }; 

Quindi devi ricorrere a questo:

 namespace my_namespace { class Vendor1Circle : public ::vendor1::Circle { public: virtual ::std::size_t data_structure_size() const { return size(); } }; class Vendor2Circle : public ::vendor2::Circle { public: virtual double area() const { return size(); } }; class Circle : public Vendor1Circle, public Vendor2Circle { // Now size is still ambiguous and should stay that way // And in my opinion the compiler should issue a warning if you try // to redefine it }; 

Quindi, C ++ ha buone ragioni per trattare le funzioni virtuali con la stessa firma di tipo (il tipo di ritorno non fa parte della firma del tipo) e il nome da due basi diverse come funzioni diverse.

Per quanto riguarda l’ using va … Tutta una direttiva using dice “Aggiungi i nomi da questo altro spazio dei nomi a questo spazio dei nomi come se fossero stati dichiarati qui”. Questo è un concetto nullo per quanto riguarda le funzioni virtuali. Suggerisce semplicemente che qualsiasi ambiguità nell’uso di un nome dovrebbe essere risolta in un modo diverso. Dichiara solo un nome, non definisce il nome. Per poter sostituire una funzione virtuale è necessaria una nuova definizione.

OTOH, se inserisci una semplice ridefinizione del thunk in linea come questa:

 class Bar : public Foo, public Goo { public: virtual DerivedElem& get_elem() { return Goo::get_elem(); } }; 

un buon compilatore dovrebbe vederlo e sapere di non preoccuparsi nemmeno di creare la funzione, e invece basta armeggiare le voci della tabella virtuale per fare la cosa giusta. Potrebbe essere necessario emettere effettivamente un codice per questo e avere il simbolo disponibile nel caso in cui il suo indirizzo sia preso, ma dovrebbe comunque essere in grado di blandire semplicemente il tavolo virtuale facendolo scomparire completamente quando chiamato attraverso Foo * .