Você já se perguntou qual dos dois usar, static_cast
vs dynamic_cast
, e até hoje não entendeu muito bem qual é a diferença entre eles? Se sim, você veio ao lugar certo. Tratarei desse assunto neste artigo, e explicarei quais são as principais diferenças entre os dois tipos de cast, assim como quando usar cada um deles. Todavia, para os apressados, aqui vai a resposta rápida: use static_cast para conversões entre tipos que já são suportadas pela linguagem (int para float, ou int para bool, por exemplo), e use dynamic_cast para conversões entre referencias ou ponteiros de classes pertencentes a uma mesma hierarquia.
O que significa “casting“?
Em programação, o termo casting é utilizado para indicar a conversão de um objeto de um tipo num objeto de um outro tipo. Pode-se usar um cast, por exemplo, para realizar a conversão de um valor float em um valor de tipo inteiro, como mostrado no exemplo a seguir.
Exemplo 1 – Casting em C++: float para int
#include <iostream> using namespace std; int main () { const float pi = 3.1415; const int integerPi = (int)pi; // Conversão no estilo C cout << "O valor de pi convertido para inteiro eh: " << integerPi << endl; }
O que vemos no exemplo 1 é uma conversão de um float pi
(representando o valor de pi com 4 casas decimais) para um valor inteiro integerPi
, e duas coisas são importantes de se notar: 1) durante a conversão, o valor de pi perde suas casas decimais e o resultado, integerPi
, guarda apenas a parte inteira – 3 -, como esperado; 2) a conversão foi feita usando o chamado C-style casting, ou uma conversão de tipos ao modo da linguagem C, cuja sintaxe é a seguinte: (novo-tipo)valor-antigo
. Esse tipo de conversão é muito simples de se fazer e de se lembrar de como fazer, mas ele não é recomendado para o C++ moderno.
Não use C-style casts em C++!
As conversões entre tipos como feitas em C são perigosas, pois elas não realizam quaisquer tipos de verificações de compatibilidade entre os tipos involvidos na conversão (isso é ainda perigoso para conversões entre ponteiros). Além disso, esse tipo de conversão não expressa claramente o intuito do código em questão. Ao invés de usar esse tipo de casting, use os tipos discutidos neste artigo: static_cast e dynamic_cast.
Static_cast ao invés de C-style casts
Em C++, uma alternativa ao C-style cast foi introduzida para permitir que o compilador verifique se os tipos envolvidos em uma conversão são compatíveis entre si: o static_cast
. O comportamento dessa função template é muito parecido com o cast usado no exemplo #1: basta invocá-la passando como argumento do template o novo tipo desejado, e como argumento da função o próprio objeto que se deseja converter. O retorno da função é um objeto do novo tipo desejado. Vejamos a seguir um exemplo do uso do static_cast.
Exemplo 2 – static_cast em C++ – float para int
#include <iostream> using namespace std; int main () { const float pi = 3.1415; const auto integerPi = static_cast<int>(pi); cout << "O valor de pi convertido para inteiro eh: " << integerPi << endl; }
É importante notar que o static_cast não pode converter qualquer tipo que se deseje em um outro tipo qualquer: os tipos convertidos devem ser convertíveis entre si de acordo com regras do C++, como por exemplo ints e floats, ints e booleans. Outro tipo de conversão possível é aquela entre tipos definidos pelo usuário (como classes, por exemplo), se os tipos pertencem a uma mesma “hierarquia” (um herda do outro) ou se um dos tipos possui um construtor que recebe o outro como argumento; todavia, conversões desse tipo apenas podem ser realizadas entre ponteiros e referências, e não diretamente sobre objetos.. Vejamos alguns exemplo do uso do static_cast a seguir.
Exemplo 3 – static_cast em C++ com classes
// Exemplo #3 - static_cast em C++ com classes #include <iostream> using namespace std; class A { public: A() { cout << "Construtor padrao da classe A" << endl; } virtual ~A() = default; }; class B final: public A { public: B() { cout << "Construtor padrao da classe B" << endl; } explicit B(const A& parentObj) { cout << "Construtor de conversao A -> B" << endl; } void explode() { cout << internalMessage << endl; } ~B() = default; private: const std::string internalMessage{"Isto nao eh bom!"}; }; int main () { A aObj; B bObj {static_cast<A>(aObj)}; A* aPtr {&aObj}; B* bPtr{static_cast<B*>(aPtr)}; bPtr->explode(); // Isso causa um comportamento indefinido }
static_cast – sintaxe
static_cast<novo-tipo-desejado>(objeto-a-ser-convertido)
No exemplo 3 há duas classes em uma mesma hierarquia: A (a classe de base), e B (a classe derivada de A). Na linha 32 vemos o uso do static_cast para converter um objeto da classe A em um objeto da classe B (isso se chamda downcasting e deve ser evitado sempre que possível, mas este é o assunto de um outro artigo): B bObj {static_casst<A>(aObj)};
. Nessa linha, o construtor de conversão definido na classe B (linha 19) é invocado – esse construtor está marcado como explicit, o que exige o uso do static_cast (ou algum outro cast) para que conversões de A para B sejam realizadas; isto é, a seguinte linha não compilaria (pois tentaria realizar uma conversão implícita usando este construtor): B bObj {aObj};
.
Vemos ainda na linha 35 uma conversão de ponteiro para a classe A (aPtr
) em um ponteiro para a classe B (bPtr
) usando o static_cast. Note que nenhuma verificação é realizada aqui, e a conversão acontece “sem problemas”. Contudo, o ponteiro bPtr
aponta para um objeto da classe A, e não um objeto da classe B, mas o programa “não sabe” que o objeto por trás do ponteiro pertence à classe A e não à B; assim sendo, a chamada da funcão explode()
na linha 36 é permitida, ainda que o objeto da classe A não possua um método chamado explode()
– catástrofe na certa! Veja a última linha dos resultados do exemplo 3 no bloco acima: o programa se comporta de modo imprevisível e encerra a sua execução .Percebe-se, portanto, que o uso do static_cast para conversões de ponteiros ou referências entre classes não é seguro e carece de checks.
Mas espere um momento: se o static_cast é o substituto mais seguro do C-style cast, e ele ainda não é seguro o suficiente para se usado com conversões de ponteiros e referências entre classes, quem poderá nos ajudar? Calma, nem tudo está perdido: o dynamic_cast existe precisamente para esse tipo de cenário.
Dynamic_cast em C++
O dynamic_cast
serve para realizar conversões entre objetos pertencentes a uma mesma hierarquia de classes. Com o dynamic_cast, antes da conversão ser realizada são verificados os tipos dos objetos envolvidos, e se a conversão resultar em um ponteiro “mal-formado” (como no caso da linha 35 do exemplo 3, onde tento inicializar um ponteiro para a classe B usando um objeto da classe A), o dynamic_cast indica que há um problema de uma das duas formas a seguir:
- O retorno do dynamic_cast é um
nullptr
se a tentativa de conversão foi feita entre ponteiros; - Uma exceção do tipo
std::bad_cast
é lançada se a conversão foi realizada entre referências.
É importante notar que para realizar as verificações entre tipos mencionadas no parágrafo anterior, o dynamic_cast usa as vtables (ou tabelas virtuais, um mecanismo usado pelas classes para possibilitar o polimorfismo dinâmico – assunto este também de um outro artigo) das classes dos objetos envolvidas na conversão para determinar se a conversão é válida ou não. Contudo, apenas classes com métodos virtuais possuem vtables, ou seja, o dynamic_cast só pode ser usado entre tipos de uma mesma hierarquia de classes que possuam pelo menos um método virtual.
Vejamos a seguir um exemplo de utilização do dynamic_cast, dessa vez usando as classes Base
e Derived
(a class de base e a classe derivada, respectivamente).
Exemplo 4 – dynamic_cast em C++ com classes
#include <iostream> using namespace std; class Base { public: Base() { cout << "Construtor padrao da classe Base" << endl; } virtual ~Base() = default; }; class Derived final: public Base { public: Derived() { cout << "Construtor padrao da classe Derived" << endl; } explicit Derived(const Base& baseObj) { cout << "Construtor de conversao Base -> Derived" << endl; } void explode() { cout << internalMessage << endl; } ~Derived() = default; private: const std::string internalMessage{"Isto nao eh bom!"}; }; int main () { Base baseObj; Base& baseRef = baseObj; Base* basePtr {&baseObj}; if (Derived* derivedPtr{dynamic_cast<Derived*>(basePtr)}; derivedPtr != nullptr) { derivedPtr->explode(); // Isso causaria um comportamento indefinido } else { cout << "A conversao entre ponteiros nao pode ser realizada" << endl; } try { Derived& derivedRef{dynamic_cast<Derived&>(baseRef)}; derivedRef.explode(); // Isso causaria um comportamento indefinido } catch (std::bad_cast &e) { cout << "A conversao entre referencias falhou." << endl; cout << "Erro: " << e.what() << endl; } Derived derivedObj; Base& fakeBaseRef = derivedObj; try { Derived& derivedRef{dynamic_cast<Derived&>(fakeBaseRef)}; derivedRef.explode(); // Isso causaria um comportamento indefinido } catch (std::bad_cast &e) { cout << "To sentindo que vai funcionar." << endl; cout << "Erro: " << e.what() << endl; } }
dynamic_cast – sintaxe
dynamic_cast<novo-tipo-desejado>(objeto-a-ser-convertido)
No exemplo 4, vemos duas conversões feitas entre os tipos Base
e Derived
(correspondentes aos tipos A
e B
do exemplo 3, respectivamente): uma delas feita entre ponteiros para esses tipos, e a outra feita entre referências. Começamos, na linha 31, com a criação de um objeto do tipo Base – baseObj
-; na linha 32 é criada uma referência para esse objeto – baseRef
-, e na linha 34 é criado um ponteiro para o objeto da linha 31 – basePtr
.
Na linha 35 há a primeira conversão: de ponteiro da classe de base para um ponteiro da classe derivada chamado derivedPtr
. A conversão é feita no campo de declaração do if, novo campo adicional (introduzido no C++ 17) que permite declarar variáveis válidas apenas no escopo do if em questão, e o if testa se a conversão com dynamic_cast se sucedeu bem com a expressão `derivedPtr != nullptr`. Por padrão, conversões mal-sucedidas com dynamic_cast retornam nullptr
, daí o teste.
Como podemos ver na segunda linha dos resultados do exemplo #4, a saída do programa é: “A conversao entre ponteiros nao pode ser realizada”, indicando que houve um problema na conversão do ponteiro da classe de base para o ponteiro da classe derivada (a verificação é feita em runtime, isto é, durante a execução do programa). Na linha 35, o que acontece para que a conversão falhe? O problema é o seguinte: o ponteiro para a classe Base basePtr
aponta para um objeto do tipo Base
, e não para um objeto da classe derived
(isto pode acontecer devido ao polimorfismo dinâmico), portanto durante a conversão o dynamic_cast verifica se o ponteiro resultante seria “seguro” para uso, e sua conclusão é que não, ele não o seria. Mas por que não?
Para entender porque a conversão é julgada perigosa e portanto proibida, consideremos o que aconteceria caso ela fosse realizada. Digamos que a conversão seja feita, e agora derivedPtr
é um ponteiro de tipo Derived*
: aos olhos do compilador, o objeto subjacente ao ponteiro é um objeto do tipo Derived
, e portanto todos os métodos pertencentes à classe Derived
podem ser invocados usando derivedPtr
, incluindo o método `explode()` – e aí está o problema.
Lembre-se que o verdadeiro tipo do objeto para o qual derivedPtr
aponta é Base
, e não Derived
. Base
não possui um método chamado explode()
, mas o compilador acha que o objeto é do tipo Derived
, e por isso o código na linha 43 do exemplo 4 é válido e compilaria sem problemas. Todavia, durante a execução do programa, o comportamento da linha derivedPtr->explode()
é indefinido e poderia até mesmo causar um crash no programa. Em resumo: o dynamic_cast previne o usuário proibindo-o de realizar conversões que resultariam em situações perigosas como a que acabei de descrever, onde um objeto se passaria por outro de outro tipo aos olhos do compilador.
Passemos à conversão entre referências na linha 42. Nessa linha, o dynamic_cast é usado para converter uma referência para Base
em uma referência para Derived
: Derived& derivedRef{dynamic_cast(baseRef)};
. Note que essa porção do código está contida em um grupo try...catch
, grupo dedicado ao gerenciamento de exceções; isto se dá porque não há um equivalente ao nullptr
para referências: quando a conversão de uma referência não pode ser realizada, a forma de indicá-lo é através da emissão de uma exceção – d’onde a necessidade do bloco try...catch
.
Durante a conversão entre os tipos de referência, de Base
para Derived
, as mesmas verificações realizadas para a conversão entre ponteiros são feitas, e a conclusão do dynamic_cast é de que converter um &Base
em um &Derived
resultaria em uma falsa referência a um objeto do tipo Derived
(visto que o objeto ao qual baseRef
faz referência pertence à classe Base
, e não à classe Derived
), e que o usuário poderia tentar invocar um método inexistente na verdadeira classe do objeto (Base
) sobre a suposta referência de tipo Derived
: como é feito na linha 43 com derivedRef.explode()
. O comportamento de tal execução é indefinido, e o dynamic_cast previne o possível problema emitindo uma exceção de tipo std::bad_cast
.
Como vemos nos resultados da execução, entramos no catch
e é impresso na saída “A conversão entre referencias falhou”, seguido de “Erro: std::bad_cast”. Passemos agora a um exemplo de conversão bem-sucedida entre referências.
No bloco de código seguinte, um objeto de tipo Derived
é criado: derivedObj
. Então, cria-se uma referência de tipo &Base
, fakeBaseRef
, assim chamada porque ela se liga a um objeto de tipo Derived
e não Base
. Em seguida, num novo grupo try...catch
é feita a conversão de &Base
para &Derived
, e desta vez ela funciona porque o objeto real, ao qual fakeBaseRef
se refere, pertence à classe Derived
, portanto o usuário não correrá o risco de acessar métodos ou atributos que não existem no objeto em questão. Como podemos ver, o método explode()
é chamado na linha 53 e na saída do programa há “Isso nao eh bom!”
Conclusão – dynamic ou static casts?
Ambos os tipos de conversão possuem casos em que são a escolha correta. O static_cast deve ser usado para conversões entre tipos “normais” da linguagem, por exemplo: int para float, float para int, int para bool etc. Outro cenário onde o static_cast pode ser usado sem problemas é a criação de um objeto da classe A
a partir de um objeto da classe B
, considerando que a classe A
tenha um construtor cujo parâmetro é um objeto da classe B
. Todavia, as conversões deste último gênero são geralmente realizadas automaticamente.
Já o dynamic_cast deve ser usado sempre que se deseje realizar conversões entre tipos pertencentes a uma mesma hierarquia de classes (como conversões de polimorfismo dinâmico). Para converter um tipo A
em outro B
, sendo B
a classe base e A
a classe derivada, por exemplo, dynamic_cast é a ferramenta certa a se usar. Também é possível fazer a conversão usando static_cast, mas este último carece das verificações em tempo de execução que são feitas pelo dynamic_cast, podendo resultar em conversões “incompletas” e perigosas para o usuário.
Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.