Usando "super" em C ++

203

Meu estilo de codificação inclui o seguinte idioma:

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

      // Etc.
} ;

Isso me permite usar "super" como um alias para o Base, por exemplo, em construtores:

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

Ou mesmo ao chamar o método da classe base dentro de sua versão substituída:

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

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

Pode até ser encadeado (ainda preciso encontrar o uso para isso):

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 qualquer forma, acho muito útil o uso de "typedef super", por exemplo, quando o Base é detalhado e / ou modelado.

O fato é que super é implementado em Java e também em C # (onde é chamado de "base", a menos que eu esteja errado). Mas o C ++ não possui essa palavra-chave.

Então, minhas perguntas:

  • esse uso de typedef é super comum / raro / nunca visto no código com o qual você trabalha?
  • esse uso do typedef é super Ok (ou seja, você vê razões fortes ou não tão fortes para não usá-lo)?
  • "super" deve ser uma coisa boa, deve ser um pouco padronizada em C ++, ou esse uso já é suficiente através de um typedef?

Edit: Roddy mencionou o fato de que o typedef deve ser privado. Isso significaria que qualquer classe derivada não seria capaz de usá-lo sem redeclará-lo. Mas acho que isso também impediria o super :: super encadeamento (mas quem vai chorar por isso?).

Edit 2: Agora, alguns meses depois de usar massivamente "super", concordo plenamente com o ponto de vista de Roddy: "super" deve ser privado. Eu votaria sua resposta duas vezes, mas acho que não posso.

paercebal
fonte
Impressionante! Exatamente o que eu estava procurando. Não pense que eu precisei usar essa técnica até agora. Excelente solução para o meu código de plataforma cruzada.
22468 AlanKley
6
Para mim, superparece Javae não é nada ruim, mas ... Mas C++suporta herança múltipla.
ST3
2
@ user2623967: Certo. No caso de herança simples, um "super" é suficiente. Agora, se você tem herança múltipla, ter "superA", "superB" etc. é uma boa solução: você DESEJA chamar o método de uma implementação ou de outra, portanto, você DIGA que implementação deseja. Usando "super" -como typedef permite que você forneça um nome de acesso / gravável fácil (em vez de, digamos, escrever MyFirstBase<MyString, MyStruct<MyData, MyValue>>em todos os lugares)
paercebal
Observe que, ao herdar de um modelo, não é necessário incluir os argumentos do modelo ao fazer referência a ele. Por exemplo: template <class baz> struct Foo {...void bar() {...} ...}; struct Foo2: Foo<AnnoyinglyLongListOfArguments> { void bar2() { ... Foo::bar(); ...} };Isso funcionou para mim com o gcc 9.1 --std = c ++ 1y (c ++ 14).
Thanasis Papoutsidakis
1
... Hum, correção. Parece funcionar em qualquer padrão c ++, não apenas em 14. #
Thanasis Papoutsidakis

Respostas:

151

Bjarne Stroustrup menciona no Design and Evolution of C ++ que supercomo uma palavra-chave foi considerada pelo comitê de padrões ISO C ++ na primeira vez em que o C ++ foi padronizado.

Dag Bruck propôs essa extensão, chamando a classe base de "herdada". A proposta mencionava a questão da herança múltipla e teria sinalizado usos ambíguos. Até Stroustrup estava convencido.

Após a discussão, Dag Bruck (sim, a mesma pessoa que fez a proposta) escreveu que a proposta era implementável, tecnicamente sólida e livre de grandes falhas, além de lidar com a herança múltipla. Por outro lado, não havia dinheiro suficiente para o dinheiro, e o comitê deveria lidar com um problema mais espinhoso.

Michael Tiemann chegou atrasado e depois mostrou que um super digitado funcionaria muito bem, usando a mesma técnica solicitada neste post.

Portanto, não, isso provavelmente nunca será padronizado.

Se você não possui uma cópia, Design and Evolution vale bem o preço de capa. Cópias usadas podem ser adquiridas por cerca de US $ 10.

Max Lybbert
fonte
5
D & E é realmente um bom livro. Mas parece que vou precisar relê-lo - não me lembro de nenhuma história.
227 Michael Burr
2
Lembro-me de três recursos que não foram aceitos discutidos em D&E. Este é o primeiro (procure "Michael Tiemann" no índice para encontrar a história), a regra de duas semanas é a segunda (procure "regra de duas semanas" no índice) e a terceira foi denominada parâmetros (procure " argumentos nomeados "no índice).
Max Lybbert 8/08
12
Existe uma falha importante na typedeftécnica: ela não respeita a SECA. A única maneira de contornar seria usar macros feias para declarar classes. Quando você herda, a base pode ser uma classe de modelo de vários parâmetros longa ou pior. (por exemplo, multi-classes), você teria que reescrever tudo isso uma segunda vez. Finalmente, vejo um grande problema com as bases de modelos que possuem argumentos de classe de modelo. Nesse caso, o super é um modelo (e não uma instanciação de um modelo). O qual não pode ser digitado. Mesmo em C ++ 11, você precisa usingpara este caso.
v.oddou
105

Eu sempre usei "herdado" em vez de super. (Provavelmente devido ao background do Delphi), e eu sempre o faço privado , para evitar o problema quando o 'herdado' é erroneamente omitido de uma classe, mas uma subclasse tenta usá-lo.

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

Meu 'modelo de código' padrão para a criação de novas classes inclui o typedef, por isso tenho poucas oportunidades de omiti-lo acidentalmente.

Eu não acho que a sugestão encadeada "super :: super" seja uma boa idéia. Se você está fazendo isso, provavelmente está muito ligada a uma hierarquia específica, e alterá-la provavelmente quebrará muito as coisas.

Roddy
fonte
2
Quanto ao encadeamento de super :: super, como mencionei na pergunta, ainda preciso encontrar um uso interessante para isso. Por enquanto, eu o vejo apenas como um hack, mas vale a pena mencionar, mesmo que apenas pelas diferenças com o Java (onde você não pode encadear "super").
paercebal
4
Depois de alguns meses, fui convertido para o seu ponto de vista (e eu tive um bug por causa de um "super" esquecido, como você mencionou ...). Você está certo na sua resposta, incluindo o encadeamento, eu acho. ^ _ ^ ...
paercebal
como usar isso agora para chamar métodos de classe pai?
mLstudent33
isso significa que eu tenho que declarar todos os métodos da classe Base, virtualconforme mostrado aqui: martinbroadhurst.com/typedef-super.html
mLstudent33
36

Um problema é que, se você esquecer (re) definir super para classes derivadas, qualquer chamada para super :: something será compilada corretamente, mas provavelmente não chamará a função desejada.

Por exemplo:

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

        ...
    }
};

(Conforme apontado por Martin York nos comentários a esta resposta, esse problema pode ser eliminado tornando o typedef privado, em vez de público ou protegido.)

Kristopher Johnson
fonte
Obrigado pela observação. Esse efeito colateral escapou do meu conhecimento. Embora provavelmente não seja compilado para o uso do construtor, em qualquer outro lugar, eu acho, o bug estaria lá.
28468 paercebal
5
No entanto, o typedef privado impedirá o uso encadeado, mencionado na postagem original.
27/10/09
1
Correndo para este erro exato é o que me levam a esta pergunta :(
Steve Vermeulen
então use super1para Base e super2em Derivado? O DerivedAgain pode usar os dois?
mLstudent33
20

FWIW A Microsoft adicionou uma extensão para __super em seu compilador.

krujos
fonte
Alguns desenvolvedores aqui começaram a pressionar para usar __super. No começo, eu recuei, pois senti que era "errado" e "não normal". No entanto, eu comecei a amar isso.
Aardvark
8
Estou trabalhando em um aplicativo do Windows e adoro a extensão __super. Fico triste pelo fato de o comitê de padrões ter rejeitado o favor do truque typedef mencionado aqui, porque, embora esse truque seja bom, requer mais manutenção do que uma palavra-chave do compilador quando você altera a hierarquia de herança e manipula a herança múltipla corretamente (sem exigir duas typedefs como super1 e super2). Em resumo, concordo com o outro comentarista que a extensão MS é MUITO útil, e qualquer pessoa que utilize o Visual Studio exclusivamente deve considerar o uso.
Brian
15

Super (ou herdado) é muito bom, porque se você precisar colar outra camada de herança entre Base e Derivado, precisará alterar apenas duas coisas: 1. a "classe Base: foo" e 2. o typedef

Se bem me lembro, o comitê de padrões do C ++ estava pensando em adicionar uma palavra-chave para isso ... até Michael Tiemann apontar que esse truque de digitação funciona.

Quanto à herança múltipla, como está sob controle do programador, você pode fazer o que quiser: talvez super1 e super2, ou o que for.

Colin Jensen
fonte
13

Acabei de encontrar uma solução alternativa. Eu tenho um grande problema com a abordagem typedef que me mordeu hoje:

  • O typedef requer uma cópia exata do nome da classe. Se alguém alterar o nome da classe, mas não alterar o typedef, você terá problemas.

Então, eu vim com uma solução melhor usando um modelo muito simples.

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

Então agora, em vez de

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

Você tem

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

Nesse caso, BaseAliasnão é particular e tentei me proteger contra o uso descuidado, selecionando um nome de tipo que deve alertar outros desenvolvedores.

paperjam
fonte
4
publicalias é uma desvantagem, como você está aberto para o bug mencionado no de Roddy e de Kristopher respostas (por exemplo, você pode (por engano) derivar Derivedem vez de MakeAlias<Derived>)
Alexander Malakhov
3
Você também não tem acesso aos construtores da classe base em suas listas de inicializadores. (Isso pode ser compensado no C ++ 11 usando herdadores de construtores MakeAliasou encaminhamento perfeito, mas requer que você se refira aos MakeAlias<BaseAlias>construtores em vez de apenas se referir Cdiretamente aos construtores de s.)
Adam H. Peterson
12

Não me lembro de ter visto isso antes, mas à primeira vista eu gosto. Como Ferruccio observa, ele não funciona bem em face do MI, mas o MI é mais uma exceção do que a regra e não há nada que diga que algo precisa ser utilizável em qualquer lugar para ser útil.

Michael Burr
fonte
6
Voto positivo apenas para a frase "não há nada que diga que algo precisa ser utilizável em qualquer lugar para ser útil".
Tanktalus 07/10/08
1
Acordado. É útil. Eu estava apenas olhando para a biblioteca de características do tipo impulso para ver se havia uma maneira de gerar o typedef através de um modelo. Infelizmente, não parece que você pode.
Ferruccio
9

Eu já vi esse idioma empregado em muitos códigos e tenho certeza de que já o vi em algum lugar nas bibliotecas do Boost. No entanto, tanto quanto me lembro, o nome mais comum é base(ou Base) em vez de super.

Esse idioma é especialmente útil se estiver trabalhando com classes de modelo. Como exemplo, considere a seguinte classe (de um projeto 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;
    // …
}

Não ligue para os nomes engraçados. O ponto importante aqui é que a cadeia de herança usa argumentos de tipo para obter polimorfismo em tempo de compilação. Infelizmente, o nível de aninhamento desses modelos fica bastante alto. Portanto, as abreviações são cruciais para facilitar a leitura e a manutenção.

Konrad Rudolph
fonte
Eu tinha usado o "super" ultimamente pelo mesmo motivo. A herança do público foi muito doloroso para lidar de outra forma ... :-) ...
paercebal
4

Eu já o vi muitas vezes usado, às vezes como super_t, quando a base é um tipo de modelo complexo ( boost::iterator_adaptorfaz isso, por exemplo)

James Hopkin
fonte
1
Você está certo. Encontrei a referência aqui. boost.org/doc/libs/1_36_0/libs/iterator/doc/…
paercebal 7/08/08
4

esse uso de typedef é super comum / raro / nunca visto no código com o qual você trabalha?

Eu nunca vi esse padrão específico no código C ++ com o qual trabalho, mas isso não significa que não esteja lá fora.

esse uso do typedef é super Ok (ou seja, você vê razões fortes ou não tão fortes para não usá-lo)?

Não permite herança múltipla (de qualquer maneira, de maneira limpa).

"super" deve ser uma coisa boa, deve ser um pouco padronizada em C ++, ou esse uso já é suficiente através de um typedef?

Pelo motivo acima citado (herança múltipla), não. A razão pela qual você vê "super" nos outros idiomas que você listou é que eles suportam apenas uma herança, portanto não há confusão sobre o que "super" está se referindo. É verdade que nessas linguagens é útil, mas na verdade não tem um lugar no modelo de dados C ++.

Ah, e FYI: C ++ / CLI suporta esse conceito na forma da palavra-chave "__super". Observe, porém, que o C ++ / CLI também não suporta herança múltipla.

Toji
fonte
4
Como contraponto, o Perl tem herança múltipla e SUPER. Há alguma confusão, mas o algoritmo usado pela VM para encontrar algo está claramente documentado. Dito isto, raramente vejo o MI usado onde várias classes básicas oferecem os mesmos métodos em que a confusão pode surgir de qualquer maneira.
Tanktalus
1
O Microsoft Visual Studio implementa a palavra-chave __super para C ++, seja para CLI ou não. Se apenas uma das classes herdadas oferece um método da assinatura correta (o caso mais comum, como mencionado por Tanktalus), apenas escolhe a única opção válida. Se duas ou mais classes herdadas fornecerem uma correspondência de função, ela não funcionará e exigirá que você seja explícito.
Brian
3

Um motivo adicional para usar um typedef para a superclasse é quando você está usando modelos complexos na herança do objeto.

Por exemplo:

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

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

Na classe B, seria ideal ter um typedef para A, caso contrário você ficaria repetido em todos os lugares em que desejasse referenciar os membros de A.

Nesses casos, também pode funcionar com herança múltipla, mas você não teria um typedef chamado 'super', seria chamado 'base_A_t' ou algo parecido.

--jeffk ++

jdkoftinoff
fonte
2

Depois de migrar do Turbo Pascal para C ++, eu costumava fazer isso para ter um equivalente para a palavra-chave "herdada" do Turbo Pascal, que funciona da mesma maneira. No entanto, depois de programar em C ++ por alguns anos, parei de fazê-lo. Descobri que não precisava muito disso.

Greg Hewgill
fonte
1

Não sei se é raro ou não, mas certamente fiz a mesma coisa.

Como já foi apontado, a dificuldade de fazer essa parte da linguagem é quando uma classe faz uso de herança múltipla.

Matt Dillard
fonte
1

Eu uso isso de tempos em tempos. Apenas quando eu me encontrar digitando o tipo de classe base algumas vezes, eu o substituirei por um typedef semelhante ao seu.

Eu acho que pode ser um bom uso. Como você diz, se sua classe base for um modelo, poderá salvar a digitação. Além disso, as classes de modelo podem aceitar argumentos que atuam como políticas de como o modelo deve funcionar. Você é livre para alterar o tipo de base sem precisar corrigir todas as suas referências, desde que a interface da base permaneça compatível.

Eu acho que o uso através do typedef já é suficiente. De qualquer maneira, não consigo ver como isso seria incorporado à linguagem, porque a herança múltipla significa que pode haver muitas classes base; portanto, você pode digitá-la como achar melhor para a classe que logicamente considera a classe base mais importante.

Scott Langham
fonte
1

Eu estava tentando resolver exatamente o mesmo problema; Eu dei algumas idéias, como o uso de modelos variados e a expansão de pacotes para permitir um número arbitrário de pais, mas percebi que isso resultaria em uma implementação como 'super0' e 'super1'. Eu joguei no lixo porque isso seria pouco mais útil do que não ter para começar.

Minha solução envolve uma classe auxiliar PrimaryParente é implementada da seguinte forma:

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

Então, qual classe que você deseja usar seria declarada como tal:

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

Para evitar a necessidade de usar a herança virtual PrimaryParentem BaseClass, um construtor de tomar um número variável de argumentos é usado para permitir a construção de BaseClass.

A razão por trás da publicherança de BaseClassinto PrimaryParenté permitir que você MyObjecttenha controle total sobre a herança de, BaseClassapesar de ter uma classe auxiliar entre eles.

Isso significa que toda classe que você deseja ter superdeve usar a PrimaryParentclasse auxiliar, e cada filho pode herdar apenas uma classe usando PrimaryParent(daí o nome).

Outra restrição para esse método é MyObject pode herdar apenas uma classe que herda PrimaryParent, e essa deve ser herdada usando PrimaryParent. Aqui está o que eu quero dizer:

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 descartá-lo como uma opção devido ao número aparente de restrições e ao fato de haver uma classe intermediária entre todas as heranças, essas coisas não são ruins.

A herança múltipla é uma ferramenta forte, mas na maioria das circunstâncias, haverá apenas um pai principal e, se houver outros pais, provavelmente serão classes Mixin ou classes que não herdam de PrimaryParentqualquer maneira. Se a herança múltipla ainda for necessária (embora muitas situações sejam benéficas para usar a composição para definir um objeto em vez da herança), basta definir explicitamente supernessa classe e não herdá-la PrimaryParent.

A ideia de ter que definir super em todas as classes não é muito atraente para mim, o uso PrimaryParentpermite super, claramente, um alias baseado em herança, permanecer na linha de definição de classe em vez do corpo da classe para onde os dados devem ir.

Isso pode ser apenas eu embora.

Claro que toda situação é diferente, mas considere essas coisas que eu disse ao decidir qual opção usar.

Kevin
fonte
1

Não vou dizer muito, exceto o código atual com comentários que demonstram que super não significa chamar base!

super != base.

Em suma, o que "super" deveria significar afinal? e então o que "base" deveria significar?

  1. super significa chamar o último implementador de um método (não o método base)
  2. base significa escolher qual classe é a base padrão na herança múltipla.

Essas 2 regras se aplicam aos typedefs da classe.

Considere o implementador e o usuário da biblioteca, quem é super e quem é base?

para obter mais informações, aqui está o código de trabalho para copiar e colar no seu 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();
}

Outro ponto importante aqui é o seguinte:

  1. chamada polimórfica: chamadas para baixo
  2. super chamada: liga para cima

por dentro main(), usamos chamadas polimórficas inferiores que super chamam para cima, não muito úteis na vida real, mas demonstram a diferença.

metablaster
fonte
0

Este é um método que eu uso, que usa macros em vez de um typedef. Sei que essa não é a maneira C ++ de fazer as coisas, mas pode ser conveniente ao encadear iteradores por herança, quando apenas a classe base mais abaixo na hierarquia está agindo sobre um deslocamento herdado.

Por exemplo:

// 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

A configuração genérica que eu uso facilita a leitura e a cópia / colar entre a árvore de herança que possui código duplicado, mas deve ser substituída, porque o tipo de retorno precisa corresponder à classe atual.

Pode-se usar letras minúsculas superpara replicar o comportamento visto em Java, mas meu estilo de codificação é usar todas as letras maiúsculas para macros.

Zhro
fonte