Utilizzo di "super" in C ++


203

Il mio stile di programmazione include il seguente linguaggio:

class Derived : public Base
{
   public :
      typedef Base super; // note that it could be hidden in
                          // protected/private section, instead

      // Etc.
} ;

Questo mi permette di usare "super" come alias di Base, ad esempio nei costruttori:

Derived(int i, int j)
   : super(i), J(j)
{
}

O anche quando si chiama il metodo dalla classe base all'interno della sua versione sostituita:

void Derived::foo()
{
   super::foo() ;

   // ... And then, do something else
}

Può anche essere incatenato (devo ancora trovare l'uso per quello, però):

class DerivedDerived : public Derived
{
   public :
      typedef Derived super; // note that it could be hidden in
                             // protected/private section, instead

      // Etc.
} ;

void DerivedDerived::bar()
{
   super::bar() ; // will call Derived::bar
   super::super::bar ; // will call Base::bar

   // ... And then, do something else
}

Ad ogni modo, trovo molto utile l'uso di "typedef super", ad esempio, quando Base è prolisso e / o modellato.

Il fatto è che super è implementato in Java, così come in C # (dove si chiama "base", a meno che non mi sbagli). Ma al C ++ manca questa parola chiave.

Quindi, le mie domande:

  • questo uso di typedef è molto comune / raro / mai visto nel codice con cui lavori?
  • è questo uso di typedef super Ok (cioè vedi dei motivi forti o meno per non usarlo)?
  • "super" dovrebbe essere una buona cosa, dovrebbe essere un po 'standardizzato in C ++, o è già abbastanza usato in un typedef?

Modifica: Roddy ha menzionato il fatto che il typedef dovrebbe essere privato. Ciò significherebbe che qualsiasi classe derivata non sarebbe in grado di usarla senza dichiararla nuovamente. Ma suppongo che impedirebbe anche il super :: super concatenamento (ma chi pianterà per questo?).

Modifica 2: Ora, alcuni mesi dopo aver usato in modo massiccio "super", sono pienamente d'accordo con il punto di vista di Roddy: "super" dovrebbe essere privato. Voterei la sua risposta due volte, ma credo di non poterlo fare.


Eccezionale! Esattamente quello che stavo cercando. Non pensare di aver mai avuto bisogno di usare questa tecnica fino ad ora. Ottima soluzione per il mio codice multipiattaforma.
AlanKley,

6
Per me supersembra Javae non è niente di male, ma ... Ma C++supporta l'eredità multipla.
ST3,

2
@ user2623967: giusto. Nel caso dell'eredità semplice, basta un "super". Ora, se hai l'ereditarietà multipla, avere "superA", "superB", ecc. È una buona soluzione: VUOI chiamare il metodo da un'implementazione o da un'altra, quindi devi dire quale implementazione desideri. L'uso di typedef simile a "super" ti consente di fornire un nome facilmente accessibile / scrivibile (invece di, diciamo, scrivendo MyFirstBase<MyString, MyStruct<MyData, MyValue>>ovunque)
paercebal

Si noti che quando si eredita da un modello, non è necessario includere gli argomenti del modello quando si fa riferimento ad esso. Ad esempio: template <class baz> struct Foo {...void bar() {...} ...}; struct Foo2: Foo<AnnoyinglyLongListOfArguments> { void bar2() { ... Foo::bar(); ...} };questo mi ha funzionato con gcc 9.1 --std = c ++ 1y (c ++ 14).
Thanasis Papoutsidakis,

1
... Um, correzione. Sembra funzionare in qualsiasi standard c ++, non solo 14.
Thanasis Papoutsidakis,

Risposte:


151

Bjarne Stroustrup menziona in Design ed Evolution di C ++ che supercome parola chiave è stata considerata dal comitato ISO C ++ Standards la prima volta che C ++ è stato standardizzato.

Dag Bruck ha proposto questa estensione, chiamando la classe base "ereditata". La proposta menzionava il problema dell'ereditarietà multipla e avrebbe contrassegnato gli usi ambigui. Persino Stroustrup ne era convinto.

Dopo la discussione, Dag Bruck (sì, la stessa persona che ha presentato la proposta) ha scritto che la proposta era attuabile, tecnicamente valida e priva di grossi difetti, e ha gestito l'eredità multipla. D'altra parte, non c'era abbastanza botto per il dollaro e il comitato dovrebbe gestire un problema più spinoso.

Michael Tiemann arrivò in ritardo, e poi mostrò che un super dattiloscritto avrebbe funzionato bene, usando la stessa tecnica di cui si chiedeva in questo post.

Quindi, no, questo probabilmente non verrà mai standardizzato.

Se non ne hai una copia, Design ed Evolution valgono il prezzo di copertina. Le copie usate possono essere acquistate per circa $ 10.


5
D&E è davvero un buon libro. Ma sembra che dovrò rileggerlo - non ricordo nessuna delle due storie.
Michael Burr,

2
Ricordo tre caratteristiche non accettate, discusse in D&E. Questo è il primo (cercare "Michael Tiemann" nell'indice per trovare la storia), la regola delle due settimane è la seconda (cercare "la regola delle due settimane" nell'indice) e la terza è stata denominata parametri (cercare " argomenti chiamati "nell'indice).
Max Lybbert,

12
C'è un grosso difetto nella typedeftecnica: non rispetta il DRY. L'unico modo per aggirare sarebbe usare brutte macro per dichiarare le classi. Quando erediti, la base potrebbe essere una lunga classe di template multiparametrica, o peggio. (ad es. multi classi) dovresti riscriverlo una seconda volta. Infine, vedo un grosso problema con le basi dei modelli che hanno argomenti sulla classe dei modelli. In questo caso, il super è un modello (e non un'istanza di un modello). Che non può essere dattiloscritto. Anche in C ++ 11 è necessario usingper questo caso.
v

105

Ho sempre usato "ereditato" piuttosto che super. (Probabilmente a causa di un background di Delphi), e lo rendo sempre privato , per evitare il problema quando "ereditato" viene erroneamente omesso da una classe ma una sottoclasse tenta di usarlo.

class MyClass : public MyBase
{
private:  // Prevents erroneous use by other classes.
  typedef MyBase inherited;
...

Il mio "modello di codice" standard per la creazione di nuove classi include il typedef, quindi ho poche opportunità di ometterlo accidentalmente.

Non penso che il suggerimento incatenato "super :: super" sia una buona idea. Se lo fai, probabilmente sei molto legato a una particolare gerarchia e cambiarlo probabilmente spezzerà male le cose.


2
Per quanto riguarda il concatenamento di super :: super, come ho già detto nella domanda, devo ancora trovare un uso interessante. Per ora, lo vedo solo come un trucco, ma vale la pena menzionarlo, se non altro per le differenze con Java (dove non è possibile concatenare "super").
paercebal,

4
Dopo pochi mesi, sono stato convertito al tuo punto di vista (e ho avuto un bug a causa di un "super" dimenticato, come hai menzionato ...). Hai perfettamente ragione nella tua risposta, incluso il concatenamento, immagino. ^ _ ^ ...
paercebal,

come lo uso ora per chiamare i metodi della classe genitore?
mLstudent33,

significa che devo dichiarare tutti i metodi della classe Base virtualcome mostrato qui: martinbroadhurst.com/typedef-super.html
mLstudent33

36

Un problema è che se si dimentica di (ri) definire super per le classi derivate, qualsiasi chiamata a super :: qualcosa verrà compilata correttamente ma probabilmente non chiamerà la funzione desiderata.

Per esempio:

class Base
{
public:  virtual void foo() { ... }
};

class Derived: public Base
{
public:
    typedef Base super;
    virtual void foo()
    {
        super::foo();   // call superclass implementation

        // do other stuff
        ...
    }
};

class DerivedAgain: public Derived
{
public:
    virtual void foo()
    {
        // Call superclass function
        super::foo();    // oops, calls Base::foo() rather than Derived::foo()

        ...
    }
};

(Come sottolineato da Martin York nei commenti a questa risposta, questo problema può essere eliminato rendendo il typedef privato piuttosto che pubblico o protetto.)


Grazie per l'osservazione. Questo effetto collaterale era sfuggito alla mia attenzione. Anche se probabilmente non verrà compilato per l'uso del costruttore, ovunque, suppongo, il bug sarebbe lì.
paercebal,

5
Tuttavia, il typedef privato impedirà l'uso incatenato, menzionato nel post originale.
AnT

1
Incontrare questo esatto bug è ciò che mi ha portato a questa domanda :(
Steve Vermeulen,

quindi utilizzare super1per Base e super2in derivato? DerivedAgain può usare entrambi?
mLstudent33,

20

FWIW Microsoft ha aggiunto un'estensione per __super nel proprio compilatore.


Alcuni sviluppatori qui hanno iniziato a spingere per usare __super. All'inizio ho fatto un passo indietro poiché ho ritenuto che fosse "sbagliato" e "non standard". TUTTAVIA, ho imparato ad amarlo.
Aardvark,

8
Sto lavorando su un'app di Windows e adoro l'estensione __super. Mi rattrista il fatto che il comitato standard lo abbia respinto a favore del trucco typedef menzionato qui, perché mentre questo trucco typedef è buono, richiede più manutenzione rispetto a una parola chiave del compilatore quando si modifica la gerarchia dell'ereditarietà e gestisce correttamente l'ereditarietà multipla (senza richiedere due typedefs come super1 e super2). In breve, sono d'accordo con l'altro commentatore che l'estensione MS è MOLTO utile, e chiunque usi esclusivamente Visual Studio dovrebbe prendere in seria considerazione di usarlo.
Brian

15

Super (o ereditato) è una cosa molto buona perché se devi inserire un altro livello di ereditarietà tra Base e Derivato, devi solo cambiare due cose: 1. la "classe Base: pippo" e 2. il typedef

Se ricordo bene, il comitato per gli standard del C ++ stava prendendo in considerazione l'idea di aggiungere una parola chiave per questo ... fino a quando Michael Tiemann ha sottolineato che questo trucco tipografico funziona.

Per quanto riguarda l'ereditarietà multipla, dal momento che è sotto il controllo del programmatore puoi fare quello che vuoi: forse super1 e super2, o qualsiasi altra cosa.


13

Ho appena trovato una soluzione alternativa. Ho un grosso problema con l'approccio typedef che mi ha morso oggi:

  • Il typedef richiede una copia esatta del nome della classe. Se qualcuno cambia il nome della classe ma non cambia il typedef, allora incontrerai dei problemi.

Quindi ho trovato una soluzione migliore usando un modello molto semplice.

template <class C>
struct MakeAlias : C
{ 
    typedef C BaseAlias;
};

Quindi ora, invece di

class Derived : public Base
{
private:
    typedef Base Super;
};

hai

class Derived : public MakeAlias<Base>
{
    // Can refer to Base as BaseAlias here
};

In questo caso, BaseAliasnon è privato e ho cercato di proteggermi dall'uso incauto selezionando un nome di tipo che dovrebbe avvisare gli altri sviluppatori.


4
publicalias è un aspetto negativo, in quanto sei aperto al bug menzionato nelle risposte di Roddy's e Kristopher (ad esempio puoi (per errore) derivare Derivedinvece di MakeAlias<Derived>)
Alexander Malakhov

3
Inoltre, non hai accesso ai costruttori di classi base negli elenchi di inizializzatori. (Ciò può essere compensato in C ++ 11 usando l'ereditarietà dei costruttori MakeAliaso il perfetto inoltro, ma richiede di fare riferimento ai MakeAlias<BaseAlias>costruttori piuttosto che riferirsi Cdirettamente ai costruttori.)
Adam H. Peterson

12

Non ricordo di averlo visto prima, ma a prima vista mi piace. Come osserva Ferruccio , non funziona bene di fronte all'MI, ma l'MI è più un'eccezione che una regola e non c'è nulla che dica che qualcosa deve essere utilizzabile ovunque per essere utile.


6
Votazione solo per la frase "non c'è nulla che dica che qualcosa deve essere utilizzabile ovunque per essere utile".
Tanktalus,

1
Concordato. È utile. Stavo solo guardando la libreria dei tratti del tipo di boost per vedere se c'era un modo per generare il typedef tramite un modello. Sfortunatamente, non sembra che tu possa farlo.
Ferruccio,

9

Ho visto questo linguaggio usato in molti codici e sono abbastanza sicuro di averlo visto da qualche parte nelle librerie di Boost. Tuttavia, per quanto ricordo il nome più comune èbase (o Base) invece di super.

Questo linguaggio è particolarmente utile se si lavora con classi di template. Ad esempio, considera la seguente classe (da a progetto reale ):

template <typename TText, typename TSpec>
class Finder<Index<TText, PizzaChili<TSpec> >, PizzaChiliFinder>
    : public Finder<Index<TText, PizzaChili<TSpec> >, Default>
{
    typedef Finder<Index<TText, PizzaChili<TSpec> >, Default> TBase;
    // …
}

Non importa i nomi divertenti. Il punto importante qui è che la catena di ereditarietà usa argomenti di tipo per ottenere il polimorfismo in fase di compilazione. Sfortunatamente, il livello di annidamento di questi modelli diventa piuttosto alto. Pertanto, le abbreviazioni sono fondamentali per la leggibilità e la manutenibilità.


Ultimamente avevo usato il "super" per lo stesso motivo. L'eredità pubblica era troppo dolorosa per gestire altrimenti ... :-) ...
paercebal,


4

questo uso di typedef è molto comune / raro / mai visto nel codice con cui lavori?

Non ho mai visto questo particolare schema nel codice C ++ con cui lavoro, ma ciò non significa che non sia disponibile.

è questo uso di typedef super Ok (cioè vedi dei motivi forti o meno per non usarlo)?

Non consente l'ereditarietà multipla (in modo pulito, comunque).

"super" dovrebbe essere una buona cosa, dovrebbe essere un po 'standardizzato in C ++, o è già abbastanza usato in un typedef?

Per il motivo sopra citato (eredità multipla), n. Il motivo per cui vedi "super" nelle altre lingue che hai elencato è che supportano solo la singola eredità, quindi non c'è confusione su cosa si riferisca a "super". Certo, in quelle lingue è utile ma non ha davvero un posto nel modello di dati C ++.

Oh, e FYI: C ++ / CLI supporta questo concetto sotto forma della parola chiave "__super". Si noti, tuttavia, che C ++ / CLI non supporta l'ereditarietà multipla.


4
Come contrappunto, Perl ha sia eredità multipla che SUPER. C'è un po 'di confusione, ma l'algoritmo che la VM attraversa per trovare qualcosa è chiaramente documentato. Detto questo, raramente vedo l'MI usato in cui più classi di base offrono gli stessi metodi in cui la confusione può sorgere comunque.
Tanktalus,

1
Microsoft Visual Studio implementa la parola chiave __super per C ++ indipendentemente dalla CLI. Se solo una delle classi ereditate offre un metodo per la firma corretta (il caso più comune, come menzionato da Tanktalus), sceglie solo l'unica scelta valida. Se due o più classi ereditate forniscono una corrispondenza di funzione, non funziona e richiede di essere esplicito.
Brian

3

Un ulteriore motivo per utilizzare un typedef per la superclasse è quando si utilizzano modelli complessi nell'eredità dell'oggetto.

Per esempio:

template <typename T, size_t C, typename U>
class A
{ ... };

template <typename T>
class B : public A<T,99,T>
{ ... };

In classe B sarebbe l'ideale avere un typedef per A, altrimenti resteresti bloccato a ripeterlo ovunque volessi fare riferimento ai membri di A.

In questi casi può funzionare anche con eredità multipla, ma non avresti un typedef chiamato 'super', si chiamerebbe 'base_A_t' o qualcosa del genere.

--jeffk ++


2

Dopo la migrazione da Turbo Pascal a C ++ nel passato, lo facevo per avere un equivalente per la parola chiave "ereditata" di Turbo Pascal, che funziona allo stesso modo. Tuttavia, dopo aver programmato in C ++ per alcuni anni ho smesso di farlo. Ho scoperto che non mi serviva molto.


1

Non so se sia raro o no, ma ho sicuramente fatto la stessa cosa.

Come è stato sottolineato, la difficoltà nel rendere questa parte della lingua stessa è quando una classe usa l'ereditarietà multipla.


1

Lo uso di tanto in tanto. Proprio quando mi ritrovo a scrivere il tipo di classe base un paio di volte, lo sostituirò con un typedef simile al tuo.

Penso che possa essere un buon uso. Come dici tu, se la tua classe base è un modello può salvare la digitazione. Inoltre, le classi di modelli possono accettare argomenti che fungono da criteri per il funzionamento del modello. Sei libero di cambiare il tipo di base senza dover correggere tutti i tuoi riferimenti fino a quando l'interfaccia della base rimane compatibile.

Penso che l'uso attraverso il typedef sia già abbastanza. Non riesco comunque a vedere come sarebbe integrato nel linguaggio perché l'ereditarietà multipla significa che possono esserci molte classi di base, quindi puoi scriverlo come ritieni opportuno per la classe che logicamente ritieni sia la classe di base più importante.


1

Stavo cercando di risolvere esattamente lo stesso problema; Ho gettato alcune idee, come l'uso di modelli variadici e l'espansione del pacchetto per consentire un numero arbitrario di genitori, ma mi sono reso conto che avrebbe comportato un'implementazione come 'super0' e 'super1'. L'ho distrutto perché sarebbe a malapena più utile che non averlo per cominciare.

La mia soluzione prevede una classe di supporto PrimaryParented è implementata come segue:

template<typename BaseClass>
class PrimaryParent : virtual public BaseClass
{
protected:
    using super = BaseClass;
public:
    template<typename ...ArgTypes>
    PrimaryParent<BaseClass>(ArgTypes... args) : BaseClass(args...){}
}

Quindi qualsiasi classe tu voglia usare sarebbe dichiarata come tale:

class MyObject : public PrimaryParent<SomeBaseClass>
{
public:
    MyObject() : PrimaryParent<SomeBaseClass>(SomeParams) {}
}

Per evitare la necessità di utilizzare l'ereditarietà virtuale in PrimaryParenton BaseClass, viene utilizzato un costruttore che accetta un numero variabile di argomenti per consentire la costruzione di BaseClass.

Il motivo dietro l' publiceredità di BaseClassinto PrimaryParentè di MyObjectavere il pieno controllo sull'eredità di BaseClassnonostante abbia una classe di supporto tra di loro.

Questo significa che ogni classe che vuoi avere superdeve usare la PrimaryParentclasse helper e ogni figlio può ereditare solo da una classe usando PrimaryParent(da qui il nome).

Un'altra restrizione per questo metodo è che MyObjectpuò ereditare solo una classe da cui eredita PrimaryParente che si deve ereditare usando PrimaryParent. Ecco cosa intendo:

class SomeOtherBase : public PrimaryParent<Ancestor>{}

class MixinClass {}

//Good
class BaseClass : public PrimaryParent<SomeOtherBase>, public MixinClass
{}


//Not Good (now 'super' is ambiguous)
class MyObject : public PrimaryParent<BaseClass>, public SomeOtherBase{}

//Also Not Good ('super' is again ambiguous)
class MyObject : public PrimaryParent<BaseClass>, public PrimaryParent<SomeOtherBase>{}

Prima di scartare questa opzione come opzione a causa del numero apparente di restrizioni e del fatto che esiste una classe intermedia tra ogni eredità, queste cose non sono male.

L'ereditarietà multipla è uno strumento efficace, ma nella maggior parte dei casi ci sarà un solo genitore principale e, se ci sono altri genitori, probabilmente saranno classi Mixin o classi che non ereditano PrimaryParentcomunque. Se l'ereditarietà multipla è ancora necessaria (anche se molte situazioni trarrebbero beneficio dall'uso della composizione per definire un oggetto invece dell'ereditarietà), piuttosto che definire esplicitamente superin quella classe e da cui non ereditare PrimaryParent.

L'idea di dover definire superin ogni classe non è molto allettante per me, usare PrimaryParentconsentesuper , chiaramente, un alias basato sull'ereditarietà, di rimanere nella linea di definizione della classe invece del corpo della classe dove dovrebbero andare i dati.

Potrei essere solo io.

Ovviamente ogni situazione è diversa, ma considera queste cose che ho detto quando ho deciso quale opzione usare.


1

Non dirò molto, tranne il codice attuale con commenti che dimostrano che super non significa chiamare base!

super != base.

In breve, cosa dovrebbe significare "super" comunque? e allora cosa dovrebbe significare "base"?

  1. super significa, chiamando l'ultimo implementatore di un metodo (non metodo di base)
  2. base significa, scegliendo quale classe è la base predefinita in eredità multipla.

Queste 2 regole si applicano ai typedef nella classe.

Considera l'implementatore e l'utente della libreria, chi è super e chi è base?

per maggiori informazioni ecco il codice di lavoro per copia incolla nel tuo IDE:

#include <iostream>

// Library defiens 4 classes in typical library class hierarchy
class Abstract
{
public:
    virtual void f() = 0;
};

class LibraryBase1 :
    virtual public Abstract
{
public:
    void f() override
    {
        std::cout << "Base1" << std::endl;
    }
};

class LibraryBase2 :
    virtual public Abstract
{
public:
    void f() override
    {
        std::cout << "Base2" << std::endl;
    }
};

class LibraryDerivate :
    public LibraryBase1,
    public LibraryBase2
{
    // base is meaningfull only for this class,
    // this class decides who is my base in multiple inheritance
private:
    using base = LibraryBase1;

protected:
    // this is super! base is not super but base!
    using super = LibraryDerivate;

public:
    void f() override
    {
        std::cout << "I'm super not my Base" << std::endl;
        std::cout << "Calling my *default* base: " << std::endl;
        base::f();
    }
};

// Library user
struct UserBase :
    public LibraryDerivate
{
protected:
    // NOTE: If user overrides f() he must update who is super, in one class before base!
    using super = UserBase; // this typedef is needed only so that most derived version
    // is called, which calls next super in hierarchy.
    // it's not needed here, just saying how to chain "super" calls if needed

    // NOTE: User can't call base, base is a concept private to each class, super is not.
private:
    using base = LibraryDerivate; // example of typedefing base.

};

struct UserDerived :
    public UserBase
{
    // NOTE: to typedef who is super here we would need to specify full name
    // when calling super method, but in this sample is it's not needed.

    // Good super is called, example of good super is last implementor of f()
    // example of bad super is calling base (but which base??)
    void f() override
    {
        super::f();
    }
};

int main()
{
    UserDerived derived;
    // derived calls super implementation because that's what
    // "super" is supposed to mean! super != base
    derived.f();

    // Yes it work with polymorphism!
    Abstract* pUser = new LibraryDerivate;
    pUser->f();

    Abstract* pUserBase = new UserBase;
    pUserBase->f();
}

Un altro punto importante qui è questo:

  1. chiamata polimorfica: chiamate verso il basso
  2. super chiamata: chiama verso l'alto

all'interno main()usiamo i polimeri polimeri callici che super chiamano verso l'alto, non molto utili nella vita reale, ma dimostrano la differenza.



0

Questo è un metodo che uso che utilizza macro anziché un typedef. So che questo non è il modo C ++ di fare le cose, ma può essere conveniente quando si concatenano iteratori insieme attraverso l'ereditarietà quando solo la classe base più lontana nella gerarchia agisce su un offset ereditato.

Per esempio:

// some header.h

#define CLASS some_iterator
#define SUPER_CLASS some_const_iterator
#define SUPER static_cast<SUPER_CLASS&>(*this)

template<typename T>
class CLASS : SUPER_CLASS {
   typedef CLASS<T> class_type;

   class_type& operator++();
};

template<typename T>
typename CLASS<T>::class_type CLASS<T>::operator++(
   int)
{
   class_type copy = *this;

   // Macro
   ++SUPER;

   // vs

   // Typedef
   // super::operator++();

   return copy;
}

#undef CLASS
#undef SUPER_CLASS
#undef SUPER

La configurazione generica che utilizzo semplifica la lettura e la copia / incolla tra l'albero delle eredità che ha un codice duplicato ma deve essere sovrascritto perché il tipo restituito deve corrispondere alla classe corrente.

Si potrebbe usare una lettera minuscola superper replicare il comportamento visto in Java ma il mio stile di codifica è usare tutte le lettere maiuscole per le macro.

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.