sexta-feira, 23 de setembro de 2011

A palavra mágica é: explicit!

Observe a seguinte classe:

class NumeroEstupido
{
public:
    NumeroEstupido( int num ): mNumero( num ){}
    ~NumeroEstupido(){}
    int GetNumero() const { return mNumero; }
private:
    int mNumero;
};

Note algo incrível nela: o construtor recebe apenas um parâmetro!

Nossa, que incrível! ¬¬

Bom, o que eu quero mostrar com isso são as maneiras de iniciar este objeto. A mais tradicional é:

NumeroEstupido obj( 2 );

Mas, como possui apenas 1 parâmetro no construtor, ele também pode ser iniciado assim:


NumeroEstupido obj = 2;

Nossa, não sabia! Isso é d+!

Não, não é. Se você conviveu até hoje sem isso, pode conviver por mais tempo. A menos que você queira muito que a classe tenha este comportamento, você deve desabilitá-lo. E é para isso que serve o comando explicit.

Se você declarar o construtor da NumeroEstupido da seguinte forma:

explicit NumeroEstupido( int num ): mNumero( num ){}

A inicialização NumeroEstupido obj = 2; não funcionará. Via de regra, sempre que você declarar uma classe com um construtor que recebe apenas 1 parâmetro, você deve declará-lo como explicit.

Ora, mas por quê??

Essa inicialização esquisita é a causa de muitos problemas e comportamentos inesperados em códigos. Vou dar um exemplo simples. Imagine que você tem a seguinte função:

NumeroEstupido soma( const NumeroEstupido& num1, const NumeroEstupido& num2 )
{
   return NumeroEstupido( num1.GetNumero() + num2.GetNumero() );
}

E a chama da seguinte forma:

NumeroEstupido numSoma = soma( 200, 250 );

Basicamente, o compilador vai fazer:
num1 = 200
num2 = 250

E retornará a soma dos dois, fazendo com que numSoma valha 450. Até aí tudo bem. Você tem um código com 10 milhões de linhas rodando perfeitamente desta forma.

Até que um dia alguém resolve adicionar uma nova função ao código:

signed char soma( signed char num1, signed char num2 )
{
   return num1 + num2;
}

Nada demais, afinal C++ permite que você tenha funções com o mesmo nome e parâmetros diferentes.
O problema é que você roda o seu código agora, e na sua antiga chamada

NumeroEstupido numSoma = soma( 200, 250 );

numSoma passa a valer -62, e não mais 450! Como isso é possível?

É simples. Agora, quando você chama soma( 200, 250 ), o compilador vai chamar a função que recebe signed chars, e não mais a que recebe NumeroEstupido. Como esta função retorna um signed char, ocorre um overflow e o resultado é um undefined behavior.

Se o construtor de NumeroEstupido fosse explicit, esse problema seria evitado, pois no primeiro momento você seria obrigado a chamar a função soma definindo explicitamente os objetos, ou seja, algo do tipo:

soma( NumeroEstupido( 200 ), NumeroEstupido( 250 ) );

Assim, quando a função com signed char fosse criada, o comportamento da chamada com NumeroEstupido permaneceria o mesmo.

Consegue pensar em mais problemas que poderiam acontecer?

Um comentário:

  1. Olá Renan. Sensacional seus artigos, li quase todos eles. São bem engraçados e a gente aprende se divertindo. Tenho uma dúvida: é possível programar um arm bare-metal(sem sistema operacional) com a linguagem C++ usando o compilador gcc? tem algum exemplo ou tutorial de como fazer isso?
    Obrigado!

    ResponderExcluir