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
nullptrse 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.







