• Nenhum resultado encontrado

Declare os construtores como virtuais em classes-base polimórficas

CONSTRUTORES, DESTRUTORES E OPERADORES DE ATRIBUIÇÃO

Item 7: Declare os construtores como virtuais em classes-base polimórficas

Existem diversas maneiras de acompanhar o tempo, então seria razoável criar uma classe-base TimeKeeper (contador de tempo) juntamente com classes derivadas para abordagens diferentes para a contagem de tempo:

class TimeKeeper { public: TimeKeeper( ); ~TimeKeeper( ); ... };

class AtomicClock: public TimeKeeper { ... }; class WaterClock: public TimeKeeper { ... }; class WristWatch: public TimeKeeper { ... };

Muitos clientes vão querer ter acesso ao tempo sem se preocupar com os detalhes e como ele é calculado; então, uma função fábrica – que retorna um ponteiro da classe-base para um objeto recém-criado de classe derivada – pode ser usada para retornar um ponteiro para um objeto de acompanha- mento de tempo:

TimeKeeper* getTimeKeeper( ); // retorna um ponteiro para um objeto // alocado dinamicamente de uma // classe derivada de TimeKeeper

Para manter as convenções das funções fábrica, os objetos retornados por getTimeKeeper (obtém o contador de tempo) estão no monte; então, para evitar vazamento de memória e de outros recursos, é importante que cada objeto retornado seja liberado de maneira apropriada:

TimeKeeper *ptk = getTimeKeeper( ); // obtém dinamicamente o objeto alocado // da hierarquia de TimeKeeper

... // use-o

delete ptk; // libere-o para evitar vazamento de recursos O Item 13 explica que esperar do cliente a realização da exclusão pode acarretar erros, e o Item 18 explica como a interface para a função fábri- ca pode ser modificada para impedir erros comuns dos clientes, mas es- sas preocupações são secundárias aqui, porque, neste item, tratamos de uma fraqueza mais fundamental do código acima: mesmo que os clientes façam tudo certo, não existe uma maneira de saber como o programa se comportará.

O problema é que getTimeKeeper retorna um ponteiro para um objeto de uma classe derivada (por exemplo, AtomicClock – relógio atômico); esse objeto está sendo apagado com um ponteiro da classe-base (ou seja, um ponteiro TimeKeeper*), e a classe-base (TimeKeeper) possui um destrutor não virtual. Essa é uma receita para o desastre, porque C++ especifica que, quando um objeto de uma classe derivada é liberado por

meio de um ponteiro para uma classe-base com um destrutor não vir- tual, os resultados são indefinidos. O que geralmente acontece em tem- po de execução é que a parte derivada do objeto nunca é destruída. Se uma chamada a getTimeKeeper retornasse um ponteiro para um objeto AtomicClock, a parte AtomicClock do objeto (ou seja, os membros de dados declarados na classe AtomicClock) provavelmente não seria des- truída, nem o destrutor de AtomicClock seria executado. Entretanto, a parte da classe-base (ou seja, a parte TimeKeeper) em geral seria destru- ída, levando a um curioso objeto “parcialmente destruído”. Esta é uma maneira excelente de vazar recursos, corromper estruturas de dados e gastar um bocado de tempo com um depurador.

Eliminar o problema é simples: dê à classe-base um destrutor virtual. En- tão, apagar um objeto da classe derivada fará exatamente o que você quer. O objeto inteiro será destruído, incluindo todas as suas partes derivadas:

class TimeKeeper { public: TimeKeeper( ); virtual ~TimeKeeper( ); ... }; TimeKeeper *ptk = getTimeKeeper( ); ...

delete ptk; // agora se comporta corretamente Classes-base como TimeKeeper geralmente contêm funções virtuais além de seu destrutor, pois o propósito das funções virtuais é permitir a perso- nalização das implementações das classes derivadas (veja o Item 34). Por exemplo, TimeKeeper poderia ter uma função virtual, getCurrentTime (obtém o tempo atual), que seria implementada diferentemente nas vá- rias classes derivadas. Qualquer classe com funções virtuais deve, quase certamente, ter um destrutor virtual.

Se uma classe não contiver funções virtuais, isso frequentemente indica que ela não deve ser usada como classe-base. Quando se pretende que uma classe não seja estendida, tornar seu destrutor virtual costuma ser uma má ideia. Considere uma classe para representar pontos em um espaço bidimensional:

class Point { // um ponto 2D public:

Point(int xCoord, int yCoord); ~Point( );

private: int x, y; };

Se um int ocupa 32 bits, um objeto Point (ponto) pode caber em um registrador de 64 bits. Além disso, esse objeto Point pode ser passado como uma quantidade de 64 bits para as funções escritas em outras lin-

62 C++ EFICAZ: 55 MANEIRASDEAPRIMORARSEUSPROGRAMASEPROJETOS

guagens, como C ou FORTRAN. Se o destrutor de Point torna-se virtual, entretanto, a situação muda.

A implementação de funções virtuais requer que os objetos carreguem in- formações que podem ser usadas em tempo de execução para determinar quais funções virtuais devem ser invocadas no objeto. Essa informação, em geral, tem a forma de um ponteiro chamado vptr (“ponteiro para a tabela virtual, ou “virtual table pointer”). O vptr aponta para um vetor de ponteiros de funções chamado de vtbl (“tabela virtual, ou “virtual table”); cada classe com funções virtuais possui uma vtbl associada. Quando uma função vir- tual é invocada em um objeto, a função realmente chamada é determinada pelo vptr de um objeto até a vtbl, buscando o ponteiro para a função apro- priada na vtbl.

Os detalhes de como as funções virtuais são implementadas não são importantes. O que é importante é que, se a classe Point contém uma função virtual, os objetos desse tipo aumentarão de tamanho. Em uma arquitetura de 32 bits, eles irão de 64 bits (para os dois inteiros) até 96 bits (para os dois inteiros mais o vptr); em uma arquitetura de 64 bits, podem ir de 64 a 128 bits, porque os ponteiros nessas arquiteturas pos- suem tamanho de 64 bits. A inclusão de um vptr a Point aumentará, então, seu tamanho em 50 a 100%! Os objetos Point não mais cabem em um registrador de 64 bits. Além disso, os objetos Point em C++ não po- dem mais se parecer com a mesma estrutura declarada em outra lingua- gem tal como C, porque sua linguagem externa correspondente não terá o vptr. Como resultado, não é mais possível passar pontos para e a partir de funções escritas em outras linguagens, a menos que você compense explicitamente pelo vptr, o que é, na verdade, um detalhe de implementa- ção e dessa forma não é portável.

A moral da história, aqui, é que declarar desnecessariamente todos os des- trutores como virtuais é tão errado quanto nunca declará-los como virtuais. Na verdade, muitas pessoas resumem a situação da seguinte maneira: de- clare um destrutor virtual em uma classe se e somente se essa classe conti- ver ao menos uma função virtual.

É possível ser atingido pelo problema dos destrutores não virtuais mes- mo na completa ausência de funções virtuais. Por exemplo, o tipo padrão string não contém funções virtuais, mas os programadores desavisados algumas vezes usam essa classe como classe-base da mesma forma:

class SpecialString: public std::string { // péssima ideia! std::string possui ... // um destrutor não virtual };

À primeira vista, isso pode parecer inócuo, mas, se em algum lugar da apli- cação você, de alguma forma, converter um ponteiro para SpecialString

(cadeia de caracteres especial) em um ponteiro para string, e usar delete no ponteiro para string, será instantaneamente transportado para o mun- do do comportamento indefinido:

SpecialString *pss =new SpecialString("Impending Doom"); std::string *ps;

...

ps = pss; // SpecialString* ⇒ std::string* ...

delete ps; // indefinido! Na prática, os

// recursos de SpecialString *ps // serão vazados, porque o // destrutor de SpecialString // não será chamado.

A mesma análise se aplica a qualquer classe que não possui um destrutor virtual, incluindo todos os tipos contêiner da STL (ou seja, vector, list, set, tr1:unordered_map – veja o Item 54, etc.). Se você alguma vez se sentir tentado a herdar de um contêiner padrão ou de qualquer outra classe com um destrutor não virtual, resista à tentação! (Infelizmente, C++ não oferece mecanismos de prevenção de derivação, como as classes final de Java ou as classes seladas [sealed] de C#.)

Ocasionalmente, pode ser conveniente dar um destrutor puramente virtual para uma classe. Lembre-se de que as funções virtuais puras resultam em classes abstratas – classes que não podem ser instanciadas (ou seja, você não pode criar objetos desse tipo). Algumas vezes, entretanto, você tem uma classe que gostaria que fosse abstrata, mas não tem função virtual pura. O que fazer? Bem, como há a intenção de que uma classe abstrata seja usada como classe-base, e como uma classe-base deve ter um destrutor virtual, e como também uma função puramente virtual leva a uma classe abstrata, a solução é simples: declare um destrutor puramente virtual na classe que você quer que seja abstrata. Aqui está um exemplo:

class AWOV { // AWOV = "Abstract w/o virtuals" (Abstrata sem funções virtuais) public:

virtual ~AWOV( ) = 0; // declara um destrutor puramente virtual };

Essa classe possui uma função virtual pura, então ela é abstrata e possui um destrutor virtual; com isso, você não precisa se preocupar com o pro- blema do destrutor. Existe um detalhe, no entanto: você deve fornecer uma definição para o destrutor virtual puro:

AWOV::~AWOV( ) { } // definição do destrutor puramente virtual

Os destrutores funcionam assim: o destrutor da classe mais derivada é chama- do primeiro, então o destrutor de cada uma das classes-base é chamado. Os compiladores gerarão uma chamada para ~AWOV a partir dos destrutores de

64 C++ EFICAZ: 55 MANEIRASDEAPRIMORARSEUSPROGRAMASEPROJETOS

suas classes derivadas, então você precisa se certificar para fornecer um corpo para a função. Se você não o fizer, o ligador reclamará.

A regra para dar destrutores virtuais às classes-base aplica-se apenas àque- las polimórficas – a classes-base projetadas para permitir a manipulação de tipos da classe derivada por meio de interfaces da classe-base. TimeKeeper é uma classe-base polimórfica, porque esperamos ser capazes de manipular objetos AtomicClock e WaterClock (relógio de água), mesmo que tenha- mos ponteiros do tipo TimeKeeper para eles.

Nem todas as classes-base são projetadas para a utilização de maneira polimórfica. Nem o tipo padrão string, por exemplo, nem os tipos con- têiner da SQL, são projetados para serem classes-base de alguma forma, muito menos para ser classes-base polimórficas. Algumas classes são projetadas para uso como classes-base, mas não são projetadas para uso polimórfico. Essas classes – por exemplo, Uncopyable do Item 6 e input_iterator_tag da biblioteca padrão (veja o Item 47) – não são projetadas para permitir a manipulação de objetos da classe derivada através de interfaces da classe-base. Como resultado, elas não precisam de destrutores virtuais.

Lembretes

» As classes-base polimórficas devem declarar destrutores virtuais. Se uma classe possui quaisquer funções virtuais, ela deve ter destrutores virtuais. » As classes não projetadas para serem classes-base ou não projetadas

para uso de forma polimórfica não devem declarar destrutores virtuais.