Quando devo usar a herança privada C ++?


116

Ao contrário da herança protegida, a herança privada C ++ encontrou seu caminho para o desenvolvimento C ++ convencional. No entanto, ainda não encontrei um bom uso para isso.

Quando vocês usam?

c++  oop 

Respostas:


60

Nota após a aceitação da resposta: esta NÃO é uma resposta completa. Leia outras respostas como aqui (conceitualmente) e aqui (teórica e prática) se você estiver interessado na pergunta. Este é apenas um truque sofisticado que pode ser alcançado com a herança privada. Embora seja extravagante , não é a resposta à pergunta.

Além do uso básico de apenas herança privada mostrado no C ++ FAQ (vinculado em comentários de outros), você pode usar uma combinação de herança privada e virtual para selar uma classe (na terminologia .NET) ou para tornar uma classe final (na terminologia Java) . Este não é um uso comum, mas de qualquer maneira eu achei interessante:

class ClassSealer {
private:
   friend class Sealed;
   ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{ 
   // ...
};
class FailsToDerive : public Sealed
{
   // Cannot be instantiated
};

Selado pode ser instanciado. Ele deriva de ClassSealer e pode chamar o construtor privado diretamente, pois é um amigo.

FailsToDerive não compilará, pois deve chamar o construtor ClassSealer diretamente (requisito de herança virtual), mas não pode, pois é privado na classe Sealed e, neste caso, FailsToDerive não é amigo de ClassSealer .


EDITAR

Foi mencionado nos comentários que isso não poderia ser tornado genérico na época usando o CRTP. O padrão C ++ 11 remove essa limitação, fornecendo uma sintaxe diferente para se tornar amigo dos argumentos do modelo:

template <typename T>
class Seal {
   friend T;          // not: friend class T!!!
   Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...

Claro que tudo isso é discutível, já que o C ++ 11 fornece uma finalpalavra-chave contextual exatamente para este propósito:

class Sealed final // ...

Essa é uma ótima técnica. Vou escrever uma entrada no blog sobre isso.

1
Pergunta: se não usamos herança virtual, então FailsToDerive compilaria. Corrigir?

4
+1. @Sasha: Correto, a herança virtual é necessária já que a classe mais derivada sempre chama os construtores de todas as classes virtualmente herdadas diretamente, o que não é o caso da herança simples.
j_random_hacker

5
Isso pode ser tornado genérico, sem fazer um ClassSealer personalizado para cada classe que você deseja selar! Confira: class ClassSealer {protected: ClassSealer () {}}; Isso é tudo.

+1 Iraimbilanja, muito legal! BTW, vi seu comentário anterior (agora excluído) sobre o uso do CRTP: Eu acho que deveria de fato funcionar, é apenas complicado obter a sintaxe correta para amigos de modelo. Mas, em qualquer caso, sua solução não-template é muito mais incrível :)
j_random_hacker

138

Eu uso isso o tempo todo. Alguns exemplos em cima da minha cabeça:

  • Quando desejo expor parte, mas não toda a interface de uma classe base. A herança pública seria uma mentira, já que a substituibilidade de Liskov foi quebrada, enquanto a composição significaria escrever um monte de funções de encaminhamento.
  • Quando desejo derivar de uma classe concreta sem um destruidor virtual. A herança pública convidaria os clientes a deletar por meio de um ponteiro para a base, invocando um comportamento indefinido.

Um exemplo típico é derivar de forma privada de um contêiner STL:

class MyVector : private vector<int>
{
public:
    // Using declarations expose the few functions my clients need 
    // without a load of forwarding functions. 
    using vector<int>::push_back;
    // etc...  
};
  • Ao implementar o padrão de adaptador, herdar de forma privada da classe Adaptado evita ter que encaminhar para uma instância fechada.
  • Para implementar uma interface privada. Isso surge frequentemente com o Padrão Observer. Normalmente, minha classe Observer, digamos MyClass, se inscreve em algum assunto. Então, apenas MyClass precisa fazer a conversão MyClass -> Observer. O resto do sistema não precisa saber sobre isso, então a herança privada é indicada.

4
@Krsna: Na verdade, acho que não. Há apenas uma razão aqui: preguiça, além da última, que seria mais difícil de contornar.
Matthieu M.

11
Não tanto preguiça (a menos que você queira dizer no bom sentido). Isso permite a criação de novas sobrecargas de funções que foram expostas sem nenhum trabalho extra. Se em C ++ 1x eles adicionarem 3 novas sobrecargas a push_back, MyVectorobtenha-as gratuitamente.
David Stone

@DavidStone, você não pode fazer isso com um método de template?
Julien__ de

5
@Julien__: Sim, você pode escrever template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); }ou pode escrever usando Base::f;. Se você quer a maioria da funcionalidade e flexibilidade que a herança privada e uma usingdeclaração dá-lhe, você tem esse monstro para cada função (e não se esqueça conste volatilesobrecargas!).
David Stone

2
Digo a maior parte da funcionalidade porque você ainda está chamando um construtor de movimento extra que não está presente na versão de instrução using. Em geral, você esperaria que isso fosse otimizado, mas a função poderia teoricamente estar retornando um tipo não móvel por valor. O modelo de função de encaminhamento também tem uma instanciação de modelo extra e profundidade constexpr. Isso pode fazer com que seu programa atinja os limites de implementação.
David Stone

31

O uso canônico de herança privada é o relacionamento "implementado em termos de" (graças a Scott Meyers em '' C ++ Efetivo 'por este texto). Em outras palavras, a interface externa da classe herdada não tem nenhum relacionamento (visível) com a classe herdada, mas a usa internamente para implementar sua funcionalidade.


6
Pode valer a pena citar um dos motivos pelos quais é utilizado neste caso: Permite que seja realizada a otimização da classe base vazia, o que não ocorreria se a classe fosse membro em vez de uma classe base.
jalf

2
seu principal uso é reduzir o consumo de espaço onde realmente importa, por exemplo, em classes de strings controladas por políticas ou em pares compactados. na verdade, boost :: compressed_pair usou herança protegida.
Johannes Schaub - litb

jalf: Ei, eu não sabia disso. Achei que a herança não pública era usada principalmente como um hack quando você precisa acessar os membros protegidos de uma classe. Eu me pergunto por que um objeto vazio ocuparia qualquer espaço ao usar a composição. Provavelmente para endereçamento universal ...

3
Também é útil tornar uma classe não copiável - simplesmente herde de uma classe vazia que não pode ser copiada. Agora você não precisa passar pelo árduo trabalho de declarar, mas não definir um construtor de cópia privada e operador de atribuição. Meyers fala sobre isso também.
Michael Burr

Eu não percebi que essa questão realmente é sobre herança privada em vez de herança protegida. sim, eu acho que há um pouco de aplicativos para isso. não consigo pensar em muitos exemplos para herança protegida: / parece que raramente é útil.
Johannes Schaub - litb

23

Um uso útil da herança privada é quando você tem uma classe que implementa uma interface, que é então registrada com algum outro objeto. Você torna essa interface privada para que a própria classe tenha que se registrar e apenas o objeto específico com o qual ela está registrada possa usar essas funções.

Por exemplo:

class FooInterface
{
public:
    virtual void DoSomething() = 0;
};

class FooUser
{
public:
    bool RegisterFooInterface(FooInterface* aInterface);
};

class FooImplementer : private FooInterface
{
public:
    explicit FooImplementer(FooUser& aUser)
    {
        aUser.RegisterFooInterface(this);
    }
private:
    virtual void DoSomething() { ... }
};

Portanto, a classe FooUser pode chamar os métodos privados de FooImplementer por meio da interface FooInterface, enquanto outras classes externas não podem. Este é um ótimo padrão para lidar com retornos de chamada específicos que são definidos como interfaces.


1
Na verdade, a herança privada é IS-A privada.
curioso

18

Acho que a seção crítica do C ++ FAQ Lite é:

Um uso legítimo e de longo prazo para herança privada é quando você deseja construir uma classe Fred que usa código em uma classe Wilma, e o código da classe Wilma precisa invocar funções de membro de sua nova classe, Fred. Nesse caso, Fred chama não-virtuais em Wilma e Wilma chama (geralmente virtuais puros) em si mesmo, que são substituídos por Fred. Isso seria muito mais difícil de fazer com a composição.

Em caso de dúvida, você deve preferir a composição à herança privada.


4

Acho útil para interfaces (viz. Classes abstratas) que estou herdando onde não quero que outro código toque na interface (apenas a classe herdada).

[editado em um exemplo]

Veja o exemplo relacionado acima. Dizendo isso

classe Wilma precisa invocar funções de membro de sua nova classe, Fred.

é para dizer que Wilma está exigindo que Fred seja capaz de invocar certas funções-membro, ou melhor, está dizendo que Wilma é uma interface . Portanto, conforme mencionado no exemplo

herança privada não é má; é apenas mais caro de manter, pois aumenta a probabilidade de que alguém altere algo que quebrará seu código.

comentários sobre o efeito desejado dos programadores que precisam atender aos nossos requisitos de interface ou quebrar o código. E, uma vez que fredCallsWilma () é protegido, apenas amigos e classes derivadas podem tocá-lo, ou seja, uma interface herdada (classe abstrata) que apenas a classe herdeira pode tocar (e amigos).

[editado em outro exemplo]

Esta página discute brevemente as interfaces privadas (ainda de outro ângulo).


Realmente não parece útil ... você pode postar um exemplo

Acho que vejo onde você está indo ... Um caso de uso típico pode ser que Wilma é algum tipo de classe de utilitário que precisa chamar funções virtuais em Fred, mas outras classes não precisam saber que Fred é implementado em termos de Wilma. Certo?
j_random_hacker

Sim. Devo ressaltar que, no meu entendimento, o termo 'interface' é mais comumente usado em Java. Quando ouvi falar dele pela primeira vez, pensei que poderia ter recebido um nome melhor. Visto que, neste exemplo, temos uma interface com a qual ninguém faz a interface da maneira que normalmente pensamos na palavra.
viés de

@Não: Sim, acho que sua declaração "Wilma é uma interface" é um pouco ambígua, pois a maioria das pessoas interpretaria isso como significando que Wilma é uma interface que Fred pretende fornecer ao mundo , em vez de um contrato apenas com a Wilma.
j_random_hacker

@j_ É por isso que acho que interface é um nome ruim. Interface, o termo, não precisa significar para o mundo como se poderia pensar, mas sim uma garantia de funcionalidade. Na verdade, questionei o termo interface em minha aula de Design de programas. Mas, usamos o que nos é dado ...
viés

2

Às vezes, acho útil usar herança privada quando desejo expor uma interface menor (por exemplo, uma coleção) na interface de outra, onde a implementação da coleção requer acesso ao estado da classe exposta, de maneira semelhante às classes internas em Java.

class BigClass;

struct SomeCollection
{
    iterator begin();
    iterator end();
};

class BigClass : private SomeCollection
{
    friend struct SomeCollection;
    SomeCollection &GetThings() { return *this; }
};

Então, se SomeCollection precisa acessar BigClass, ele pode static_cast<BigClass *>(this). Não há necessidade de um membro de dados extra ocupando espaço.


Não há necessidade de declaração de encaminhamento de BigClassexiste neste exemplo? Acho isso interessante, mas grita hackly na minha cara.
Thomas Eding,

2

Encontrei um ótimo aplicativo para herança privada, embora tenha um uso limitado.

Problema para resolver

Suponha que você receba a seguinte API C:

#ifdef __cplusplus
extern "C" {
#endif

    typedef struct
    {
        /* raw owning pointer, it's C after all */
        char const * name;

        /* more variables that need resources
         * ...
         */
    } Widget;

    Widget const * loadWidget();

    void freeWidget(Widget const * widget);

#ifdef __cplusplus
} // end of extern "C"
#endif

Agora, seu trabalho é implementar essa API usando C ++.

Abordagem C-ish

É claro que poderíamos escolher um estilo de implementação C-ish como este:

Widget const * loadWidget()
{
    auto result = std::make_unique<Widget>();
    result->name = strdup("The Widget name");
    // More similar assignments here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    free(result->name);
    // More similar manual freeing of resources
    delete widget;
}

Mas existem várias desvantagens:

  • Gerenciamento manual de recursos (por exemplo, memória)
  • É fácil configurar o structerrado
  • É fácil esquecer a liberação de recursos ao liberar o struct
  • É C-ish

Abordagem C ++

Podemos usar C ++, então por que não usar todos os seus poderes?

Apresentando o gerenciamento automatizado de recursos

Os problemas acima estão basicamente todos ligados ao gerenciamento manual de recursos. A solução que vem à mente é herdar Widgete adicionar uma instância de gerenciamento de recursos à classe derivada WidgetImplpara cada variável:

class WidgetImpl : public Widget
{
public:
    // Added bonus, Widget's members get default initialized
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

private:
    std::string m_nameResource;
};

Isso simplifica a implementação para o seguinte:

Widget const * loadWidget()
{
    auto result = std::make_unique<WidgetImpl>();
    result->setName("The Widget name");
    // More similar setters here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    // No virtual destructor in the base class, thus static_cast must be used
    delete static_cast<WidgetImpl const *>(widget);
}

Assim, resolvemos todos os problemas acima. Mas um cliente ainda pode esquecer os setters de WidgetImple atribuir aoWidget membros diretamente.

Herança privada entra em cena

Para encapsular os Widgetmembros, usamos herança privada. Infelizmente, agora precisamos de duas funções extras para lançar entre as duas classes:

class WidgetImpl : private Widget
{
public:
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

    Widget const * toWidget() const
    {
        return static_cast<Widget const *>(this);
    }

    static void deleteWidget(Widget const * const widget)
    {
        delete static_cast<WidgetImpl const *>(widget);
    }

private:
    std::string m_nameResource;
};

Isso torna as seguintes adaptações necessárias:

Widget const * loadWidget()
{
    auto widgetImpl = std::make_unique<WidgetImpl>();
    widgetImpl->setName("The Widget name");
    // More similar setters here
    auto const result = widgetImpl->toWidget();
    widgetImpl.release();
    return result;
}

void freeWidget(Widget const * const widget)
{
    WidgetImpl::deleteWidget(widget);
}

Esta solução resolve todos os problemas. Sem gerenciamento manual de memória e Widgeté bem encapsulado, de modo que WidgetImplnão possui mais membros de dados públicos. Isso torna a implementação fácil de usar corretamente e difícil (impossível?) De usar incorretamente.

Os trechos de código formam um exemplo de compilação no Coliru .


1

Se a classe derivada - precisa reutilizar o código e - você não pode alterar a classe base e - está protegendo seus métodos usando os membros da base sob um bloqueio.

então você deve usar herança privada, caso contrário, você corre o risco de métodos de base desbloqueados exportados por meio desta classe derivada.


1

Às vezes, pode ser uma alternativa à agregação , por exemplo, se você deseja agregação, mas com o comportamento alterado da entidade agregável (substituindo as funções virtuais).

Mas você está certo, não há muitos exemplos do mundo real.


0

Herança privada a ser usada quando a relação não é "é uma", mas a nova classe pode ser "implementada em termos de classe existente" ou a nova classe "funciona como" uma classe existente.

exemplo de "padrões de codificação C ++ por Andrei Alexandrescu, Herb Sutter": - Considere que duas classes Square e Rectangle cada uma tem funções virtuais para definir sua altura e largura. Então Square não pode herdar corretamente de Rectangle, porque o código que usa um Rectangle modificável assumirá que SetWidth não altera a altura (se Rectangle documenta explicitamente esse contrato ou não), enquanto Square :: SetWidth não pode preservar esse contrato e sua própria quadratura invariante em o mesmo tempo. Mas Rectangle também não pode herdar corretamente de Square, se clientes de Square assumem, por exemplo, que a área de um Square é sua largura ao quadrado, ou se eles contam com alguma outra propriedade que não vale para Rectangles.

Um quadrado "é um" retângulo (matematicamente), mas um quadrado não é um retângulo (comportamentalmente). Consequentemente, em vez de "é-um", preferimos dizer "funciona-como-a" (ou, se preferir, "utilizável-como-a") para tornar a descrição menos sujeita a mal-entendidos.


0

Uma classe contém um invariante. O invariante é estabelecido pelo construtor. No entanto, em muitas situações é útil ter uma visão do estado de representação do objeto (que você pode transmitir pela rede ou salvar em um arquivo - DTO se preferir). REST é melhor executado em termos de um AggregateType. Isso é especialmente verdadeiro se você estiver constantemente correto. Considerar:

struct QuadraticEquationState {
   const double a;
   const double b;
   const double c;

   // named ctors so aggregate construction is available,
   // which is the default usage pattern
   // add your favourite ctors - throwing, try, cps
   static QuadraticEquationState read(std::istream& is);
   static std::optional<QuadraticEquationState> try_read(std::istream& is);

   template<typename Then, typename Else>
   static std::common_type<
             decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
             decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
   if_read(std::istream& is, Then then, Else els);
};

// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);

// no operator>> as we're const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);

struct QuadraticEquationCache {
   mutable std::optional<double> determinant_cache;
   mutable std::optional<double> x1_cache;
   mutable std::optional<double> x2_cache;
   mutable std::optional<double> sum_of_x12_cache;
};

class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
                          private QuadraticEquationCache {
public:
   QuadraticEquation(QuadraticEquationState); // in general, might throw
   QuadraticEquation(const double a, const double b, const double c);
   QuadraticEquation(const std::string& str);
   QuadraticEquation(const ExpressionTree& str); // might throw
}

Neste ponto, você pode simplesmente armazenar coleções de cache em contêineres e procurá-las na construção. Útil se houver algum processamento real. Observe que o cache faz parte do QE: as operações definidas no QE podem significar que o cache é parcialmente reutilizável (por exemplo, c não afeta a soma); no entanto, quando não há cache, vale a pena dar uma olhada.

A herança privada quase sempre pode ser modelada por um membro (armazenando referência à base, se necessário). Nem sempre vale a pena modelar dessa forma; às vezes, a herança é a representação mais eficiente.


0

Se você precisar de um std::ostreamcom algumas pequenas mudanças (como nesta questão ), pode ser necessário

  1. Crie uma classe MyStreambufque derive std::streambufe implemente as mudanças lá
  2. Crie uma classe MyOStreamque deriva std::ostreamdisso também inicializa e gerencia uma instância de MyStreambufe passa o ponteiro para essa instância para o construtor destd::ostream

A primeira ideia pode ser adicionar a MyStreaminstância como um membro de dados à MyOStreamclasse:

class MyOStream : public std::ostream
{
public:
    MyOStream()
        : std::basic_ostream{ &m_buf }
        , m_buf{}
    {}

private:
    MyStreambuf m_buf;
};

Mas as classes básicas são construídas antes de quaisquer membros de dados, portanto, você está passando um ponteiro para uma std::streambufinstância ainda não construída para a std::ostreamqual é um comportamento indefinido.

A solução é proposta na resposta de Ben à pergunta acima mencionada , simplesmente herde do buffer de fluxo primeiro, depois do fluxo e, em seguida, inicialize o fluxo com this:

class MyOStream : public MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

No entanto, a classe resultante também pode ser usada como uma std::streambufinstância que geralmente é indesejada. Mudar para herança privada resolve este problema:

class MyOStream : private MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

-1

Só porque C ++ tem um recurso, não significa que seja útil ou que deva ser usado.

Eu diria que você não deveria usá-lo.

Se você estiver usando de qualquer maneira, bem, você está basicamente violando o encapsulamento e diminuindo a coesão. Você está colocando dados em uma classe e adicionando métodos que manipulam os dados em outra.

Como outros recursos do C ++, ele pode ser usado para obter efeitos colaterais, como selar uma classe (conforme mencionado na resposta de dribeas), mas isso não o torna um bom recurso.


você está sendo sarcástico? tudo o que tenho é -1! de qualquer forma, não irei deletar isso mesmo que obtenha -100 votos
hasen

9
" você está basicamente violando o encapsulamento " Você pode dar um exemplo?
curioso

1
dados em uma classe e comportamento em outra soam como um aumento na flexibilidade, uma vez que pode haver mais de uma classe de comportamento e clientes e escolher qual eles precisam para satisfazer o que desejam
makar
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.