Nota: o código a seguir é C ++ 03, mas esperamos uma mudança para o C ++ 11 nos próximos dois anos, portanto, devemos ter isso em mente.
Estou escrevendo uma diretriz (para iniciantes, entre outros) sobre como escrever uma interface abstrata em C ++. Li os dois artigos de Sutter sobre o assunto, procurei na Internet exemplos e respostas e fiz alguns testes.
Este código NÃO deve ser compilado!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Todos os comportamentos acima encontram a origem do problema no fatiamento : A interface abstrata (ou classe não folha na hierarquia) não deve ser construtiva nem copiável / atribuível, MESMO, se a classe derivada puder ser.
Solução 0: a interface básica
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Essa solução é simples e um tanto ingênua: falha em todas as nossas restrições: pode ser construída por padrão, copiada e atribuída a cópia (nem tenho certeza sobre mover construtores e atribuição, mas ainda tenho 2 anos para descobrir fora).
- Não podemos declarar o destruidor como virtual puro porque precisamos mantê-lo em linha, e alguns de nossos compiladores não digerem métodos virtuais puros com o corpo vazio em linha.
- Sim, o único ponto dessa classe é tornar os implementadores praticamente destrutíveis, o que é um caso raro.
- Mesmo se tivéssemos um método virtual puro adicional (que é a maioria dos casos), essa classe ainda seria atribuível por cópia.
Então não...
1ª Solução: boost :: noncopyable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Esta solução é a melhor, porque é simples, clara e C ++ (sem macros)
O problema é que ele ainda não funciona para essa interface específica porque o VirtuallyConstructible ainda pode ser construído por padrão .
- Não podemos declarar o destruidor como virtual puro porque precisamos mantê-lo em linha, e alguns de nossos compiladores não o digerem.
- Sim, o único ponto dessa classe é tornar os implementadores praticamente destrutíveis, o que é um caso raro.
Outro problema é que as classes que implementam a interface não copiável devem declarar / definir explicitamente o construtor de cópias e o operador de atribuição, caso precisem ter esses métodos (e, em nosso código, temos classes de valor que ainda podem ser acessadas por nosso cliente através de interfaces).
Isso vai contra a Regra do Zero, que é para onde queremos ir: se a implementação padrão estiver correta, poderemos usá-la.
2ª Solução: proteja-os!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Esse padrão segue as restrições técnicas que tínhamos (pelo menos no código do usuário): MyInterface não pode ser construído por padrão, não pode ser construído por cópia e não pode ser designado por cópia.
Além disso, ele não impõe restrições artificiais às classes de implementação , que são livres para seguir a Regra do Zero ou até mesmo declarar alguns construtores / operadores como "= padrão" no C ++ 11/14 sem problemas.
Agora, isso é bastante detalhado, e uma alternativa seria usar uma macro, algo como:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
O protegido deve permanecer fora da macro (porque não tem escopo).
Corretamente "namespaced" (ou seja, prefixado com o nome da sua empresa ou produto), a macro deve ser inofensiva.
E a vantagem é que o código é fatorado em uma fonte, em vez de ser copiado e colado em todas as interfaces. Se o movimento-construtor e a movimentação-atribuição forem explicitamente desabilitados da mesma maneira no futuro, isso seria uma mudança muito leve no código.
Conclusão
- Estou paranóico ao querer que o código seja protegido contra o corte nas interfaces? (Eu acredito que não sou, mas nunca se sabe ...)
- Qual é a melhor solução entre as alternativas acima?
- Existe outra solução melhor?
Lembre-se de que este é um padrão que servirá como diretriz para iniciantes (entre outros); portanto, uma solução como: "Cada caso deve ter sua implementação" não é uma solução viável.
Recompensa e resultados
Eu concedi a recompensa ao coredump por causa do tempo gasto para responder às perguntas e da relevância das respostas.
Minha solução para o problema provavelmente irá para algo assim:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... com a seguinte macro:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Esta é uma solução viável para o meu problema pelos seguintes motivos:
- Esta classe não pode ser instanciada (os construtores estão protegidos)
- Esta classe pode ser virtualmente destruída
- Essa classe pode ser herdada sem impor restrições indevidas às classes herdadas (por exemplo, a classe herdada pode ser copiável por padrão)
- O uso da macro significa que a "declaração" da interface é facilmente reconhecível (e pesquisável) e seu código é fatorado em um único local, facilitando a modificação (um nome com prefixo adequado removerá conflitos de nome indesejáveis)
Observe que as outras respostas forneceram informações valiosas. Obrigado a todos vocês que deram uma chance.
Note que acho que ainda posso colocar outra recompensa nessa questão, e eu valorizo as respostas esclarecedoras o suficiente para que eu veja uma; eu abriria uma recompensa apenas para atribuí-la a essa resposta.
virtual ~VirtuallyDestructible() = 0
de herança virtual de classes de interface (somente com membros abstratos). Você pode omitir esse VirtuallyDestructible, provavelmente.
virtual void bar() = 0;
por exemplo? Isso impediria que sua interface fosse instanciada.