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 void bar() = 0;
por exemplo? Isso impediria que sua interface fosse instanciada.virtual ~VirtuallyDestructible() = 0
de herança virtual de classes de interface (somente com membros abstratos). Você pode omitir esse VirtuallyDestructible, provavelmente.Respostas:
A maneira canônica de criar uma interface em C ++ é oferecer a ela um destruidor virtual puro. Isso garante que
delete
um ponteiro para a interface faz a coisa certa: chama o destruidor da classe mais derivada para essa instância.apenas ter um destruidor virtual puro não impede a atribuição de uma referência à interface. Se você também precisar falhar, adicione um operador de atribuição protegido à sua interface.
Qualquer compilador C ++ deve ser capaz de lidar com uma classe / interface como esta (tudo em um arquivo de cabeçalho):
Se você possui um compilador que se engasga com isso (o que significa que ele deve ser anterior ao C ++ 98), sua opção 2 (com construtores protegidos) é uma boa segunda melhor.
O uso
boost::noncopyable
não é aconselhável para esta tarefa, pois envia a mensagem de que todas as classes na hierarquia devem ser não copiáveis e, portanto, pode criar confusão para desenvolvedores mais experientes que não estariam familiarizados com suas intenções de usá-lo dessa maneira.fonte
If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.
: Esta é a raiz do meu problema. Os casos em que preciso de uma interface para dar suporte à atribuição devem ser realmente raros. Por outro lado, os casos em que desejo passar uma interface por referência (os casos em que NULL não é aceitável) e, portanto, queremos evitar um no-op ou fatiar essa compilação são muito maiores.private
? Além disso, convém lidar com o padrão e o copiador.Eu sou paranóico ...
Isso não é um problema de gerenciamento de riscos?
Melhor solução
Sua segunda solução ("torne-os protegidos") parece boa, mas lembre-se de que eu não sou especialista em C ++.
Pelo menos, os usos inválidos parecem ser relatados corretamente como errôneos pelo meu compilador (g ++).
Agora, você precisa de macros? Eu diria "sim", porque mesmo que você não diga qual é o objetivo da diretriz que está escrevendo, acho que isso deve reforçar um conjunto específico de práticas recomendadas no código do seu produto.
Para esse propósito, as macros podem ajudar a detectar quando as pessoas aplicam efetivamente o padrão: um filtro básico de confirmações pode dizer se a macro foi usada:
protected
palavra - chave),Sem macros, você deve inspecionar se o padrão é necessário e bem implementado em todos os casos.
Melhor solução
Fatiar em C ++ nada mais é que uma peculiaridade da linguagem. Como você está escrevendo uma diretriz (especialmente para iniciantes), você deve se concentrar no ensino e não apenas na enumeração de "regras de codificação". Você precisa se certificar de que realmente explica como e por que a fatia ocorre, juntamente com exemplos e exercícios (não reinvente a roda, inspire-se em livros e tutoriais).
Por exemplo, o título de um exercício pode ser " Qual é o padrão para uma interface segura em C ++ ?"
Portanto, sua melhor jogada seria garantir que seus desenvolvedores de C ++ entendessem o que está acontecendo quando ocorre a fatia. Estou convencido de que, se o fizerem, não cometerão tantos erros no código quanto você temeria, mesmo sem impor formalmente esse padrão específico (mas você ainda pode aplicá-lo, os avisos do compilador são bons).
Sobre o compilador
Você diz :
Muitas vezes, as pessoas dizem "não tenho o direito de fazer [X]" , "não devo fazer [Y] ..." , ... porque pensam que isso não é possível, e não porque tentou ou pediu.
Provavelmente faz parte da descrição do seu trabalho dar sua opinião sobre questões técnicas; se você realmente acha que o compilador é a escolha perfeita (ou exclusiva) para o domínio do seu problema, use-o. Mas você também disse que "destruidores virtuais puros com implementação em linha não são o pior ponto de asfixia que eu já vi" ; do meu entendimento, o compilador é tão especial que mesmo desenvolvedores experientes em C ++ têm dificuldades para usá-lo: seu compilador herdado / interno agora é uma dívida técnica e você tem o direito (o dever?) de discutir esse problema com outros desenvolvedores e gerentes .
Tente avaliar o custo de manter o compilador versus o custo de usar outro:
Não conheço a sua situação e, de fato, você provavelmente tem razões válidas para se vincular a um compilador específico.
Mas, no caso de ser uma inércia pura, a situação nunca evoluirá se você ou seus colegas de trabalho não reportarem problemas de produtividade ou de dívida técnica.
fonte
Am I paranoid...
: "Torne suas interfaces fáceis de usar corretamente e difíceis de usar incorretamente". Eu provei esse princípio em particular quando alguém relatou que um dos meus métodos estáticos foi, por engano, usado incorretamente. O erro produzido parecia não ter relação e levou várias horas de um engenheiro para encontrar a fonte. Este "erro de interface" é semelhante à atribuição de uma referência de interface a outra. Então, sim, quero evitar esse tipo de erro. Além disso, em C ++, a filosofia é capturar o máximo possível em tempo de compilação, e a linguagem nos dá esse poder, então vamos com ele.Best solution
: Concordo. . .Better solution
: Essa é uma resposta incrível. Vou trabalhar nisso ... Agora, sobre oPure virtual classes
: O que é isso? Uma interface abstrata C ++? (classe sem estado e apenas métodos virtuais puros?). Como essa "classe virtual pura" me protegeu contra o fatiamento? (métodos virtuais puros tornarão a instanciação não compilada, mas a atribuição de cópia e a atribuição de movimentação também serão IIRC).About the compiler
: Nós concordamos, mas nossos compiladores estão fora do meu escopo de responsabilidade (não que isso me impeça de comentários sarcásticos ... :-p ...). Não divulgarei os detalhes (eu gostaria de poder), mas ele está vinculado a razões internas (como suítes de teste) e razões externas (por exemplo, ligação de cliente com nossas bibliotecas). No final, alterar a versão do compilador (ou até corrigi-la) NÃO é uma operação trivial. Muito menos substitua um compilador quebrado por um gcc recente.O problema de fatiar é um, mas certamente não é o único, apresentado quando você expõe uma interface polimórfica em tempo de execução para seus usuários. Pense em indicadores nulos, gerenciamento de memória, dados compartilhados. Nenhum deles é facilmente resolvido em todos os casos (ponteiros inteligentes são ótimos, mas mesmo eles não são uma bala de prata). De fato, a partir da sua postagem, parece que você não está tentando resolver o problema do fatiamento, mas evita isso, não permitindo que os usuários façam cópias. Tudo o que você precisa fazer para oferecer uma solução para o problema de fatiar é adicionar uma função de membro de clone virtual. Acho que o problema mais profundo da exposição de uma interface polimórfica em tempo de execução é que você força os usuários a lidar com a semântica de referência, mais difícil de raciocinar do que a semântica de valor.
A melhor maneira que conheço para evitar esses problemas no C ++ é usar o apagamento de tipo . Essa é uma técnica em que você oculta uma interface polimórfica em tempo de execução, atrás de uma interface de classe normal. Essa interface de classe normal possui semântica de valores e cuida de toda a 'bagunça' polimórfica por trás das telas.
std::function
é um excelente exemplo de apagamento de tipo.Para uma ótima explicação de por que expor a herança a seus usuários é ruim e como o apagamento de tipo pode ajudar a corrigir o que vê essas apresentações de Sean Parent:
A herança é a classe base do mal (versão curta)
Semântica de valor e polimorfismo baseado em conceitos (versão longa; mais fácil de seguir, mas o som não é ótimo)
fonte
Você não é paranóico. Minha primeira tarefa profissional como programador de C ++ resultou em fatias e falhas. Eu sei dos outros. Não há muitas boas soluções para isso.
Dadas as restrições do compilador, a opção 2 é a melhor. Em vez de criar uma macro, que seus novos programadores verão estranha e misteriosa, sugiro um script ou ferramenta para gerar automaticamente o código. Se seus novos funcionários estiverem usando um IDE, você poderá criar uma ferramenta "Nova interface MYCOMPANY" que solicitará o nome da interface e criará a estrutura que você está procurando.
Se seus programadores estiverem usando a linha de comando, use qualquer linguagem de script disponível para criar o script NewMyCompanyInterface para gerar o código.
Eu usei essa abordagem no passado para padrões de código comuns (interfaces, máquinas de estado etc.). A parte boa é que os novos programadores podem ler a saída e entendê-la facilmente, reproduzindo o código necessário quando precisam de algo que não pode ser gerado.
Macros e outras abordagens de metaprogramação tendem a ofuscar o que está acontecendo, e os novos programadores não aprendem o que está acontecendo "por trás da cortina". Quando eles precisam quebrar o padrão, estão tão perdidos quanto antes.
fonte