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?
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?
Respostas:
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 final
palavra-chave contextual exatamente para este propósito:
class Sealed final // ...
Eu uso isso o tempo todo. Alguns exemplos em cima da minha cabeça:
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...
};
push_back
, MyVector
obtenha-as gratuitamente.
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 using
declaração dá-lhe, você tem esse monstro para cada função (e não se esqueça const
e volatile
sobrecargas!).
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.
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.
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.
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).
À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.
BigClass
existe neste exemplo? Acho isso interessante, mas grita hackly na minha cara.
Encontrei um ótimo aplicativo para herança privada, embora tenha um uso limitado.
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 ++.
É 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:
struct
erradostruct
Podemos usar C ++, então por que não usar todos os seus poderes?
Os problemas acima estão basicamente todos ligados ao gerenciamento manual de recursos. A solução que vem à mente é herdar Widget
e adicionar uma instância de gerenciamento de recursos à classe derivada WidgetImpl
para 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 WidgetImpl
e atribuir aoWidget
membros diretamente.
Para encapsular os Widget
membros, 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 WidgetImpl
nã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 .
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.
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.
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.
Se você precisar de um std::ostream
com algumas pequenas mudanças (como nesta questão ), pode ser necessário
MyStreambuf
que derive std::streambuf
e implemente as mudanças láMyOStream
que deriva std::ostream
disso também inicializa e gerencia uma instância de MyStreambuf
e passa o ponteiro para essa instância para o construtor destd::ostream
A primeira ideia pode ser adicionar a MyStream
instância como um membro de dados à MyOStream
classe:
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::streambuf
instância ainda não construída para a std::ostream
qual é 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::streambuf
instâ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 }
{}
};
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.