Usando "super" en C ++

203

Mi estilo de codificación incluye el siguiente modismo:

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

      // Etc.
} ;

Esto me permite usar "super" como un alias de Base, por ejemplo, en constructores:

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

O incluso cuando se llama al método desde la clase base dentro de su versión anulada:

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

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

Incluso se puede encadenar (aunque todavía tengo que encontrar el uso para eso):

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
}

De todos modos, considero que el uso de "typedef super" es muy útil, por ejemplo, cuando Base es verbosa o con plantilla.

El hecho es que super se implementa en Java, así como en C # (donde se llama "base", a menos que me equivoque). Pero C ++ carece de esta palabra clave.

Entonces, mis preguntas:

  • ¿Es este uso de typedef super común / raro / nunca visto en el código con el que trabaja?
  • ¿Es este uso de typedef super Ok (es decir, ves razones fuertes o no tan fuertes para no usarlo)?
  • ¿debería ser "super" algo bueno, debería estar algo estandarizado en C ++, o este uso ya no es suficiente?

Editar: Roddy mencionó el hecho de que typedef debería ser privado. Esto significaría que cualquier clase derivada no podría usarla sin redeclararla. Pero supongo que también evitaría el super :: super encadenamiento (pero ¿quién va a llorar por eso?).

Edición 2: Ahora, algunos meses después de usar masivamente "super", estoy totalmente de acuerdo con el punto de vista de Roddy: "super" debería ser privado. Votaría su respuesta dos veces, pero supongo que no puedo.

paercebal
fuente
¡Increíble! Exactamente lo que estaba buscando. No creo que haya necesitado usar esta técnica hasta ahora. Excelente solución para mi código multiplataforma.
AlanKley
66
Para mí superparece Javay no es nada malo, pero ... Pero C++admite herencia múltiple.
ST3
2
@ user2623967: Correcto. En el caso de la herencia simple, un "super" es suficiente. Ahora, si tiene herencia múltiple, tener "superA", "superB", etc. es una buena solución: DESEA llamar al método desde una implementación u otra, por lo que debe decirle qué implementación desea. El uso de typedef tipo "super" le permite proporcionar un nombre de fácil acceso / escritura (en lugar de, por ejemplo, escribir en MyFirstBase<MyString, MyStruct<MyData, MyValue>>todas partes)
paercebal
Tenga en cuenta que al heredar de una plantilla, no necesita incluir los argumentos de la plantilla al hacer referencia a ella. Por ejemplo: template <class baz> struct Foo {...void bar() {...} ...}; struct Foo2: Foo<AnnoyinglyLongListOfArguments> { void bar2() { ... Foo::bar(); ...} };Esto me funcionó con gcc 9.1 --std = c ++ 1y (c ++ 14).
Thanasis Papoutsidakis
1
... Um, corrección. Parece funcionar en cualquier estándar de C ++, no solo 14.
Thanasis Papoutsidakis

Respuestas:

151

Bjarne Stroustrup menciona en Diseño y evolución de C ++ que, supercomo palabra clave, fue considerada por el comité de normas ISO C ++ la primera vez que C ++ se estandarizó.

Dag Bruck propuso esta extensión, llamando a la clase base "heredada". La propuesta mencionaba el problema de la herencia múltiple y habría señalado usos ambiguos. Incluso Stroustrup estaba convencido.

Después de la discusión, Dag Bruck (sí, la misma persona que hizo la propuesta) escribió que la propuesta era implementable, técnicamente sólida y libre de fallas importantes, y manejaba la herencia múltiple. Por otro lado, no hubo suficiente inversión para el dinero, y el comité debería manejar un problema más espinoso.

Michael Tiemann llegó tarde, y luego demostró que un superdefinido de tipo funcionaría bien, usando la misma técnica que se le preguntó en esta publicación.

Entonces, no, esto probablemente nunca se estandarizará.

Si no tiene una copia, Design and Evolution bien vale el precio de portada. Se pueden obtener copias usadas por aproximadamente $ 10.

Max Lybbert
fuente
55
D&E es de hecho un buen libro. Pero parece que tendré que volver a leerlo; no recuerdo ninguna de las historias.
Michael Burr
2
Recuerdo tres características que no fueron aceptadas discutidas en D&E. Esta es la primera (busque "Michael Tiemann" en el índice para encontrar la historia), la regla de dos semanas es la segunda (busque "regla de dos semanas" en el índice), y la tercera se denominó parámetros (buscar " argumentos nombrados "en el índice).
Max Lybbert
12
Hay una falla importante en la typedeftécnica: no respeta DRY. La única forma de hacerlo sería usar macros feas para declarar clases. Cuando hereda, la base podría ser una clase de plantilla larga de múltiples parámetros, o peor. (por ejemplo, varias clases), tendría que volver a escribir todo eso por segunda vez. Finalmente, veo un gran problema con las bases de plantillas que tienen argumentos de clase de plantilla. En este caso, el super es una plantilla (y no una instancia de una plantilla). Que no puede ser typedefed. Incluso en C ++ 11 necesitas usingpara este caso.
v.oddou
105

Siempre he usado "heredado" en lugar de super. (Probablemente debido a un fondo de Delphi), y siempre lo hago privado , para evitar el problema cuando el 'heredado' se omite erróneamente de una clase pero una subclase intenta usarlo.

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

Mi 'plantilla de código' estándar para crear nuevas clases incluye typedef, por lo que tengo pocas oportunidades de omitirla accidentalmente.

No creo que la sugerencia "super :: super" encadenada sea una buena idea. Si lo hace, probablemente esté muy vinculado a una jerarquía particular, y cambiarla probablemente romperá las cosas.

Roddy
fuente
2
En cuanto a encadenar super :: super, como mencioné en la pregunta, todavía tengo que encontrar un uso interesante para eso. Por ahora, solo lo veo como un truco, pero valió la pena mencionarlo, aunque solo sea por las diferencias con Java (donde no se puede encadenar "super").
Paercebal
44
Después de unos meses, me convertí a su punto de vista (y tuve un error debido a un "súper" olvidado, como usted mencionó ...). Supongo que tiene toda la razón en su respuesta, incluido el encadenamiento. ^ _ ^ ...
paercebal
¿Cómo uso esto ahora para llamar a los métodos de clase principal?
mLstudent33
¿significa que tengo que declarar todos los métodos de la clase Base virtualcomo se muestra aquí: martinbroadhurst.com/typedef-super.html
mLstudent33
36

Un problema con esto es que si olvida (re) definir super para las clases derivadas, cualquier llamada a super :: something se compilará bien pero probablemente no llame a la función deseada.

Por ejemplo:

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()

        ...
    }
};

(Como señaló Martin York en los comentarios a esta respuesta, este problema puede eliminarse haciendo que typedef sea privado en lugar de público o protegido).

Kristopher Johnson
fuente
Gracias por el comentario. Este efecto secundario se me había escapado. Si bien probablemente no se compilará para el uso del constructor, en cualquier otro lugar, supongo, el error estaría allí.
paercebal
55
Sin embargo, typedef privado evitará el uso encadenado, mencionado en la publicación original.
AnT
1
Encontrar este error exacto es lo que me llevó a esta pregunta :(
Steve Vermeulen
así que úsalo super1para Base y super2en Derivado? El DerivedAgain puede usar ambos?
mLstudent33
20

FWIW Microsoft ha agregado una extensión para __super en su compilador.

krujos
fuente
Algunos desarrolladores aquí comenzaron a presionar para usar __super. Al principio, retrocedí porque sentí que estaba "equivocado" y "no estándar". SIN EMBARGO, he llegado a amarlo.
Aardvark
8
Estoy trabajando en una aplicación de Windows y me encanta la extensión __super. Me entristece que el comité de estándares lo rechazara a favor del truco typedef mencionado aquí, porque si bien este truco typedef es bueno, requiere más mantenimiento que una palabra clave del compilador cuando cambia la jerarquía de herencia, y maneja la herencia múltiple correctamente (sin requerir dos typedefs como super1 y super2). En resumen, estoy de acuerdo con el otro comentarista en que la extensión MS es MUY útil, y cualquier persona que use Visual Studio exclusivamente debería considerar usarla.
Brian
15

Super (o heredado) es algo muy bueno porque si necesita pegar otra capa de herencia entre Base y Derivada, solo tiene que cambiar dos cosas: 1. la "clase Base: foo" y 2. el typedef

Si no recuerdo mal, el comité de estándares de C ++ estaba considerando agregar una palabra clave para esto ... hasta que Michael Tiemann señaló que este truco de typedef funciona.

En cuanto a la herencia múltiple, ya que está bajo el control del programador, puede hacer lo que quiera: tal vez super1 y super2, o lo que sea.

Colin Jensen
fuente
13

Acabo de encontrar una solución alternativa. Tengo un gran problema con el enfoque typedef que me mordió hoy:

  • Typedef requiere una copia exacta del nombre de la clase. Si alguien cambia el nombre de la clase pero no cambia el typedef, entonces se encontrará con problemas.

Entonces se me ocurrió una solución mejor usando una plantilla muy simple.

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

Entonces ahora, en lugar de

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

tienes

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

En este caso, BaseAliasno es privado y he tratado de evitar el uso descuidado seleccionando un nombre de tipo que debería alertar a otros desarrolladores.

Mermelada de papel
fuente
44
publicalias es un inconveniente, ya que estás abierto al error mencionado en las respuestas de Roddy y Kristopher (por ejemplo, puedes (por error) derivar de en Derivedlugar de MakeAlias<Derived>)
Alexander Malakhov
3
Tampoco tiene acceso a constructores de clase base en sus listas de inicializador. (Esto se puede compensar en C ++ 11 utilizando constructores heredados en MakeAliaso reenvío perfecto, pero requiere que se refiera a los MakeAlias<BaseAlias>constructores en lugar de referirse Cdirectamente a los constructores directamente).
Adam H. Peterson,
12

No recuerdo haber visto esto antes, pero a primera vista me gusta. Como señala Ferruccio , no funciona bien frente a MI, pero MI es más la excepción que la regla y no hay nada que diga que algo debe ser utilizable en todas partes para ser útil.

Michael Burr
fuente
66
Vota solo por la frase, "no hay nada que diga que algo debe ser utilizable en todas partes para ser útil".
Tanktalus
1
Convenido. Es útil. Solo estaba mirando la biblioteca de rasgos de tipo boost para ver si había una manera de generar el typedef a través de una plantilla. Desafortunadamente, no parece que puedas.
Ferruccio
9

He visto este modismo empleado en muchos códigos y estoy bastante seguro de haberlo visto en algún lugar de las bibliotecas de Boost. Sin embargo, hasta donde recuerdo el nombre más común es base(o Base) en lugar de super.

Este modismo es especialmente útil si se trabaja con clases de plantilla. Como ejemplo, considere la siguiente clase (de un proyecto real ):

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;
    // …
}

No te preocupes por los nombres graciosos. El punto importante aquí es que la cadena de herencia usa argumentos de tipo para lograr el polimorfismo en tiempo de compilación. Desafortunadamente, el nivel de anidación de estas plantillas es bastante alto. Por lo tanto, las abreviaturas son cruciales para la legibilidad y la mantenibilidad.

Konrad Rudolph
fuente
Había usado el "super" últimamente por la misma razón. La herencia pública era demasiado dolorosa para manejar de otra manera ... :-) ...
paercebal
4

A menudo lo he visto usado, a veces como super_t, cuando la base es un tipo de plantilla compleja ( boost::iterator_adaptorhace esto, por ejemplo)

James Hopkin
fuente
1
Tienes razón. Encontré la referencia aquí. boost.org/doc/libs/1_36_0/libs/iterator/doc/…
paercebal
4

¿Es este uso de typedef super común / raro / nunca visto en el código con el que trabaja?

Nunca he visto este patrón particular en el código C ++ con el que trabajo, pero eso no significa que no esté disponible.

¿Es este uso de typedef super Ok (es decir, ves razones fuertes o no tan fuertes para no usarlo)?

No permite la herencia múltiple (limpiamente, de todos modos).

¿debería ser "super" algo bueno, debería estar algo estandarizado en C ++, o este uso ya no es suficiente?

Por la razón antes citada (herencia múltiple), no. La razón por la que ve "super" en los otros idiomas que enumeró es que solo admiten herencia única, por lo que no hay confusión sobre a qué se refiere "super". De acuerdo, en esos lenguajes ES útil, pero en realidad no tiene cabida en el modelo de datos de C ++.

Ah, y para su información: C ++ / CLI admite este concepto en forma de la palabra clave "__super". Sin embargo, tenga en cuenta que C ++ / CLI tampoco admite la herencia múltiple.

Toji
fuente
44
Como contrapunto, Perl tiene herencia múltiple y SUPER. Existe cierta confusión, pero el algoritmo que utiliza la VM para encontrar algo está claramente documentado. Dicho esto, rara vez veo que se use MI donde varias clases base ofrecen los mismos métodos donde podría surgir confusión de todos modos.
Tanktalus
1
Microsoft Visual Studio implementa la palabra clave __super para C ++, sea o no para CLI. Si solo una de las clases heredadas ofrece un método de la firma correcta (el caso más común, como lo menciona Tanktalus), entonces solo elige la única opción válida. Si dos o más clases heredadas proporcionan una coincidencia de funciones, no funciona y requiere que seas explícito.
Brian
3

Una razón adicional para usar un typedef para la superclase es cuando usa plantillas complejas en la herencia del objeto.

Por ejemplo:

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

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

En la clase B, sería ideal tener un typedef para A, de lo contrario, estaría atascado repitiéndolo en todas partes donde quisiera hacer referencia a los miembros de A.

En estos casos, también puede funcionar con herencia múltiple, pero no tendría un typedef llamado 'super', se llamaría 'base_A_t' o algo así.

--jeffk ++

jdkoftinoff
fuente
2

Después de migrar de Turbo Pascal a C ++ en el pasado, solía hacer esto para tener un equivalente para la palabra clave "heredada" de Turbo Pascal, que funciona de la misma manera. Sin embargo, después de programar en C ++ durante algunos años, dejé de hacerlo. Descubrí que no lo necesitaba mucho.

Greg Hewgill
fuente
1

No sé si es raro o no, pero ciertamente hice lo mismo.

Como se ha señalado, la dificultad de hacer que esta parte del lenguaje en sí sea cuando una clase hace uso de la herencia múltiple.

Matt Dillard
fuente
1

Lo uso de vez en cuando. Justo cuando me encuentro escribiendo el tipo de clase base un par de veces, lo reemplazaré con un typedef similar al tuyo.

Creo que puede ser un buen uso. Como usted dice, si su clase base es una plantilla, puede guardar la escritura. Además, las clases de plantilla pueden tomar argumentos que actúan como políticas sobre cómo debería funcionar la plantilla. Puede cambiar el tipo de base sin tener que arreglar todas sus referencias a él, siempre y cuando la interfaz de la base siga siendo compatible.

Creo que el uso a través de typedef ya es suficiente. De todos modos, no puedo ver cómo se incorporaría al lenguaje porque la herencia múltiple significa que puede haber muchas clases base, por lo que puede escribirlo como mejor le parezca para la clase que lógicamente considera que es la clase base más importante.

Scott Langham
fuente
1

Estaba tratando de resolver exactamente el mismo problema; Lancé algunas ideas, como el uso de plantillas variadas y la expansión del paquete para permitir un número arbitrario de padres, pero me di cuenta de que eso resultaría en una implementación como 'super0' y 'super1'. Lo boté porque eso sería apenas más útil que no tenerlo para empezar.

Mi solución implica una clase auxiliar PrimaryParenty se implementa de la siguiente manera:

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

Entonces, cualquier clase que desee utilizar se declarará como tal:

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

Para evitar la necesidad de utilizar la herencia virtual en PrimaryParenton BaseClass, se utiliza un constructor que toma un número variable de argumentos para permitir la construcción de BaseClass.

La razón detrás de la publicherencia de BaseClassinto PrimaryParentes permitir MyObjecttener un control total sobre la herencia de a BaseClasspesar de tener una clase auxiliar entre ellos.

Esto significa que cada clase que desee tener superdebe usar la PrimaryParentclase auxiliar, y cada hijo solo puede heredar de una clase usando PrimaryParent(de ahí el nombre).

Otra restricción para este método es MyObjectque solo se puede heredar una clase de la que se hereda PrimaryParent, y esa se debe heredar usando PrimaryParent. Esto es lo que quiero decir:

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>{}

Antes de descartar esto como una opción debido a la aparente cantidad de restricciones y al hecho de que hay una clase de intermediario entre cada herencia, estas cosas no son malas.

La herencia múltiple es una herramienta sólida, pero en la mayoría de las circunstancias, solo habrá un padre primario, y si hay otros padres, probablemente serán clases Mixin, o clases que no heredan de PrimaryParenttodos modos. Si aún es necesaria la herencia múltiple (aunque muchas situaciones se beneficiarían al usar la composición para definir un objeto en lugar de la herencia), simplemente defina explícitamente superen esa clase y no herede de PrimaryParent.

La idea de tener que definir superen cada clase no es muy atractiva para mí, el uso PrimaryParentpermite super, claramente un alias basado en herencia, permanecer en la línea de definición de clase en lugar del cuerpo de la clase donde deberían ir los datos.

Aunque podría ser yo.

Por supuesto, cada situación es diferente, pero considere estas cosas que he dicho al decidir qué opción usar.

Kevin
fuente
1

¡No diré mucho, excepto el código presente con comentarios que demuestran que super no significa llamar a base!

super != base.

En resumen, ¿qué se supone que significa "super" de todos modos? y entonces, ¿qué se supone que significa "base"?

  1. super significa, llamar al último implementador de un método (no método base)
  2. base significa, elegir qué clase es la base predeterminada en herencia múltiple.

Estas 2 reglas se aplican a la clase typedefs.

Considere el implementador de la biblioteca y el usuario de la biblioteca, ¿quién es súper y quién es la base?

Para obtener más información, aquí está el código de trabajo para copiar y pegar en su 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();
}

Otro punto importante aquí es este:

  1. llamada polimórfica: llamadas hacia abajo
  2. súper llamada: llamadas hacia arriba

en el interior main()utilizamos llamadas polimórficas hacia abajo que superan llamadas hacia arriba, no realmente útiles en la vida real, pero demuestran la diferencia.

metablaster
fuente
0

Este es un método que uso que usa macros en lugar de typedef. Sé que esta no es la forma en C ++ de hacer las cosas, pero puede ser conveniente al encadenar iteradores juntos a través de la herencia cuando solo la clase base más alejada de la jerarquía está actuando sobre un desplazamiento heredado.

Por ejemplo:

// 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 configuración genérica que utilizo hace que sea muy fácil de leer y copiar / pegar entre el árbol de herencia que tiene un código duplicado pero debe anularse porque el tipo de retorno debe coincidir con la clase actual.

Se podría usar una minúscula superpara replicar el comportamiento visto en Java, pero mi estilo de codificación es usar todas las letras mayúsculas para las macros.

Zhro
fuente