Static_cast vs dynamic_cast em C++: qual usar?

Static_cast vs Dynamic_cast em C++

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 valor de pi convertido para inteiro eh: 3


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;
}

O valor de pi convertido para inteiro eh: 3


É 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
}

Construtor padrao da classe A
Construtor padrao da classe A
Construtor de conversao A -> B
./this.programSPb+`SPnSPSPニSPフSPᅠSPᄆSPkUSER=web_userLOGNAME=web_us


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;
    }
}

Construtor padrao da classe Base
A conversao entre ponteiros nao pode ser realizada
A conversao entre referencias falhou.
Erro: std::bad_cast
Construtor padrao da classe Base
Construtor padrao da classe Derived
Isto nao eh bom!


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 31basePtr.

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.

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.

Foto de perfil de Emanoel

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.

Leave a Reply

Your email address will not be published. Required fields are marked *