Back to TILs

C++ cast

Date: 2021-01-02Last modified: 2023-01-03

Table of contents

Conversão de tipos

O C++ é uma linguagem dita strictly typed, termo geralmente traduzido como fortemente tipada, isto significa que o tipo das variáveis é sempre certo e as operações devem ser definidas para tipos específicos. Gerar um objeto de um tipo através de uma expressão de outro tipo envolve uma conversão, ou casting.

Conversão estática

static_cast<novo_tipo>( expressão );

Esta é a conversão mais comum. É dita estática pois sua validade é analisada durante a compilação, as principais possibilidades são:

Conversão dinâmica

dynamic_cast<novo_tipo>( expressão );

Esta conversão é especial para referências ou ponteiros de objetos polimórficos (classes contendo funções virtuais). É dita dinâmica pois verifica durante a execução do programa se a conversão é válida quando descendo na hierarquia das classes. Em principal:

Conversão de constância

const_cast<novo_tipo>(expressão);

conversão por reinterpretação

reinterpret_cast<novo_tipo>(expressão);

Esta conversão é que mais se distancia da característica fortemente tipada da linguagem C++, pois comanda o compilador a reinterpretar o resultado da expressão como se fosse do novo_tipo, em geral sem realizar nenhuma operação nem verificação sobre os valores sendo convertidos. A grosso modo, reinterpret_cast é forma de dizer ao compilador: “Confie em mim, esses números que estou lhe passando são o que digo serem” . Seus principais usos são:

Quando usar static_cast?

Procure usar a conversão que melhor expressa suas intenções. Como apenas a definição de cada tipo de conversão pode não ser muito elucidativa, seguem exemplos de cada uma:

int   i = 5;
float x = i; //conversão implícita
float y = static_cast<float>(i); //conversão explícita

As conversões implícitas facilitam lidar com variáveis numéricas e locais, mas é recomendado sempre explicitar a conversão quando exportando a variável para alguma função. No exemplo abaixo, o resultado com e sem o cast é o mesmo (logo depois apontarei um possível problema):

int f(int x) { return x*2; }

float x = 1.f;
std::cout << "Sem cast : " << f(x)                   << std::endl; // Sem cast : 2
std::cout << "Com cast : " << f(static_cast<int>(x)) << std::endl; // Com cast : 2

Agora, digamos que seja introduzida uma nova função, sem alterar as partes existentes do código anterior:

int f(double x) { return x*5; }

float x = 1.f;
std::cout << "Sem cast : " << f(x)                   << std::endl; // Sem cast : 5
std::cout << "Com cast : " << f(static_cast<int>(x)) << std::endl; // Com cast : 2

O motivo disso é que a conversão implícita de float para double é preferida ao invés da conversão implícita de float para int.

Embora requerida em alguns casos (como convertendo ponteiros void* para outros tipos) static_cast tem uma função que pende mais para boa organização e manutenção do código.

Quando usar dynamic_cast?

dynamic_cast é a forma de verificar em tempo de execução se o tipo de objeto polimórfico passado é de um determinado tipo (para isso o programa faz uso de RTTI, omitirei detalhes). Em um exemplo simplificado, imagine uma classe base com dois tipos de derivadas:

struct Base {
  virtual ~Base(){};
};

struct DerivadaA : public Base {
};

struct DerivadaB : public Base {
};

Digamos então que temos uma função que deve lidar com objetos do tipo

  void f(Base* ponteiro_base)

Mas em algum momento a execução deve ser diferente se o objeto passado for DerivadaA ou DerivadaB. Como dynamic_cast retorna nullptr em caso de falha, podemos fazer o seguinte:

void f( Base *ponteiro_base )
{
  // tenta cast para ponteiro do tipo DerivadaA
  DerivadaA *objA = dynamic_cast<DerivadaA *>( ponteiro_base );
  if( objA != nullptr ) {
    std::cout << "Objeto do tipo A" << std::endl;
    return;
  }

  // tenta cast para ponteiro do tipo DerivadaB
  DerivadaB *objB = dynamic_cast<DerivadaB *>( ponteiro_base );
  if( objB != nullptr ) {
    std::cout << "Objeto do tipo B" << std::endl;
    return;
  }
}

int main()
{
  DerivadaA A;
  DerivadaB B;
  Base *    ponteiro_base;

  ponteiro_base = &A;
  f( ponteiro_base );
  ponteiro_base = &B;
  f( ponteiro_base );
}

Quando usar const_cast?

Remover a propriedade const de um objeto é necessidade rara, e modificar um objeto declarado const resulta em comportamento indefinido do programa, então cabe ao programador usar de forma coesa essa conversão.

Um exemplo ilustrativo: Quando deseja-se retornar uma referência a um membro de uma classe (os famosos setters e getters):

class ClasseX {
  int X;

public:
  // retorna referência a membro da classe
  int &getRefX()
  {
    return X;
  };
};

Como a função não altera o estado da classe, posso querer marcá-la const:

int& getRefX() const

Mas daí o código não compila (a assinatura const da função torna seus membros const dentro desta)…

error: binding 'const int' to reference of type 'int&' discards qualifiers

const_cast pode ser usado para retornar a referência não-constante:

      //retorna referência a membro da classe
      int& getRefX()
      {
          //(algum comentário explicando o const_cast)
          return const_cast<int&>(X);
      };

const_cast deve ser usado com muito cuidado, pois alterar o valor de uma variável originalmente constante torna o programa mal formado (undefined behaviour). No exemplo acima, se algum objeto da classe ClasseX fosse originalmente constante, a função ainda funcionaria neste, mas o valor de X não deveria ser modificado através da referência retornada.

Quando usar reinterpret_cast?

O reinterpret_cast afasta-se da noção de objetos e aproxima-se da noção de bits (informação).

Por exemplo, digamos que você esteja programando um microcontrolador e no manual deste diz que a placa de som lê as informações a partir do endereço 33 na memória:

// endereço obtido do manual do microchip
static const int ADDR_PLACA_DE_SOM = 33;

Para escrever informações nesta área da memória, você precisa converter esta posição para um ponteiro, com reinterpret_cast você pode fazê-lo:

// cria ponteiro para inteiros na posição dita pelo manual
int *som_pt = reinterpret_cast<int *>( ADDR_PLACA_DE_SOM );

Note que a conversão é bem livre, você pode criar um ponteiro para qualquer tipo de dado:

// cria ponteiro para chars na posição dita pelo manual
char* som_pt_2 = reinterpret_cast<char*>(ADDR_PLACA_DE_SOM);

C++ também suporta o cast no estilo da linguagem C, como ele é interpretado pelo compilador?

As conversões no estilo-C são do tipo:

(novo_tipo)(expressão)

Um exemplo:

  //gera um inteiro através do float 3.14
  (int)(3.14f)

Este tipo de conversão está presente no C++ principalmente por questões de compatibilidade com a linguagem C.

O compilador tenta a seguinte ordem de conversões quando um cast no estilo-C é utilizado:

  1. const_cast.
  2. static_cast, ignorando acesso restrito caso usado em hierarquias de classes.
  3. static_cast seguido de const_cast
  4. reinterpret_cast
  5. reinterpret_cast seguido de const_cast

Ou seja, o compilador tenta quase tudo para gerar um objeto do novo tipo, inclusive removendo const. Esse tipo de conversão pode gerar conversões indesejadas (até o não orientado a objetos reinterpret_cast!) que escondem bugs propagáveis pela lógica do programa.

Esta resposta é uma simplificação. Reitero que compreender bem a conversão de objetos indica boa compreensão do paradigma de orientação a objetos. As principais referências foram:

Possible output

Objeto do tipo A
Objeto do tipo B

Referências