Mutable, O legado
Continuando nosso artigo mutable, que raios é isso? agora irei demonstrar um segunda maneira de “burlar” o mesmo método const sem usar atributos mutáveis (mutable).
Temos a seguinte interface de exemplo, Entity (pure abstract)
class Entity
{
public:
virtual void logic() = 0;
virtual void draw() const = 0;
};
Note que classes como a acima são muito comuns em vários projetos
Agora a classe Player que implementa Entity
class Player : public Entity
{
public:
void logic()
{
}
void draw() const
{
mDrawCount++;
// Erro! pois é um método const
}
private:
std::size_t mDrawCount;
};
Mais skhaz porque não controlar isso no método logic que não é const? eu tenho bons motivos para isso, um deles é se você implementar um sistema de ticks como descrito no artigo Controle de velocidade de jogo com ticks onde quase sempre o método draw não será chamado no mesmo ciclo que logic.
ponteiro e const_cast os “vilões”
Com o uso do cast const_cast podemos burlar o atributo declarado como mutable (assim como para const e volatile) para não constantes.
void Player::draw() const
{
const_cast<player *>(this)->mDrawCount++;
// work fine =D
}
Quando o ponteiro nulo não é inválido
Existe coisa mais prazerosa do que admitir um erro que foi cometido na mesma semana? Existe: quando você sabia que estava certo, mas resolveu usar o senso comum por falta de provas.
Pois bem. O mesmo amigo que me recomendou que escrevesse sobre o assunto do ponteiro nulo achou um livro sobre armadilhas em C com um exemplo que demonstra exatamente o contrário: dependendo da plataforma, ponteiros nulos são sim válidos.
Nesse caso, se tratava de um programa que iria rodar em um microprocessador, daqueles que o DQ costuma programar. Pois bem. Quando o dito cujo ligava era necessário chamar uma rotina que estava localizada exatamente no endereço 0. Para fazer isso, o código era o seguinte:
( * (void(*)()) 0 ) ();
Nada mais simples: um cast do endereço 0 (apesar de normalmente inválido, 0 pode ser convertido para endereço) para ponteiro de função que não recebe parâmetros e não retorna nada, seguido de deferência (“o apontado de”) e chamada (a dupla final de parênteses).
(* (void(*)()) 0 ) ();
É bem o que o autor diz depois de jogar esta expressão: “expressions like these strike terror into the hearts of C programmers”. É lógico que isso não é bem verdade para as pessoas que acompanham este blogue =)
Códigos de entrevista – o ponteiro nulo
Bom, parece que o “mother-fucker” wordpress ferrou com meu artigo sobre o Houaiss. Enquanto eu choro as pitangas aqui vai um outro artigo um pouco mais simples, mas igualmente interessante.
“Wanderley, tenho umas sugestões para teu blog.
A primeira:
Que tal analisar o código abaixo e dizer se compila ou não. Se não compilar, explicar porquê não compila. Se compilar, o que acontecerá e por quê.”
O código é o que veremos abaixo:
#include <stdio.h> #include <stdlib.h> void func() { *(int *)0 = 0; return 0; } int main(int argc, char **argv) { func(); return 0; }
Bem, para testar a compilação basta compilar. Porém, se estivermos em uma entrevista, geralmente não existe nenhum compilador em um raio de uma sala de reunião senão seu próprio cérebro.
E é nessas horas que os entrevistadores testam se você tem um bom cérebro ou um bom currículo.
Por isso, vamos analisar passo a passo cada bloco de código e entender o que pode estar errado. Se não encontrarmos, iremos supor que está tudo certo.
#include <stdio.h> #include <stdlib.h>
Dois includes padrões, ultranormal, nada de errado aqui.
void func() { *(int *)0 = 0; return 0; }
Duas ressalvas aqui: a primeira quanto ao retorno da função é void, porém a função retorna um inteiro. Na linguagem C, isso funciona, no máximo um warning do compilador. Em C++, isso é erro brabo de tipagem.
A segunda ressalva diz respeito à linha obscura, sintaticamente correta, mas cuja semântica iremos guardar para o final, já que ainda falta o main para analisar.
int main(int argc, char **argv)
{
func();
return 0;
}
A clássica função inicial, nada de mais aqui. Retorna um int, e de fato retorn. Chama a função func, definida acima.
A linha obscura
A linha que guardamos para analisar contém uma operação de casting, atribuição e deferência, sendo o casting executado primeiro, operador unário que é, seguido pelo segundo operador unário, a deferência. Como sempre, a atribuição é uma das últimas. Descomprimida a expressão dessa linha, ficamos com algo parecido com as duas linhas abaixo:
int* p = (int*) 0; *p = 0;
Não tem nada de errado em atribuir o valor 0 a um ponteiro, que é equivalente ao define NULL da biblioteca C (e C++). De acordo com a referência GNU, é recomendado o uso do define, mas nada impede utilizar o 0 “hardcoded”.
Porém, estamos escrevendo em um ponteiro nulo, o que com certeza é um comportamento não-definido de conseqüências provavelmente funestas. O ponteiro nulo é um ponteiro inválido que serve apenas para marcar um ponteiro como inválido. Se escrevermos em um endereço inválido, bem, não é preciso ler o padrão para saber o que vai acontecer =)
Atualização
Alguns amigos me avisaram sobre algo muito pertinente: dizer que acessar um ponteiro nulo, portanto inválido, é errado e nunca deve ser feito. Como um ponteiro nulo aponta para um endereço de memória inválido, acessá-lo irá gerar uma exceção no seu sistema operacional e fazer seu programa capotar. Um ponteiro nulo é uma maneira padrão e confiável de marcar o ponteiro como inválido, e testar isso facilmente através de um if. Mais uma vez: ponteiros nulos apontando para um endereço de memória inválido (o endereço 0) nunca devem ser acessados, apenas atribuído a ponteiros.
Em código. Isso pode:
int* p = 0; // atribuindo nulo a um ponteiro int* p2 = p; // isso também podeIsso não pode:
*p = 15; // NUNCA acessar ponteiros nulos int x = *p; // isso também não pode, ler de um ponteiro nuloDito isso, me sinto melhor =)
Os diferentes erros na linguagem C
Uma coisa que me espanta de vez em quando é o total desconhecimento por programadores mais ou menos experientes dos níveis de erros que podem ocorrer em um fonte escrito em C ou C++. Desconheço o motivo, mas desconfio que o fato de outras linguagens não terem essa divisão de processos pode causar alguma nivelação entre as linguagens e fazer pensar que o processo de compilação em C é como em qualquer outra linguagem.
Porém, para começar, só de falarmos em compilação já estamos pegando apenas um pedaço do todo, que é a geração de um programa executável em C. Tradicionalmente, dividimos esse processo em três passos:
- Preprocessamento
- Compilação
- Linkedição
Vamos dar uma olhada mais de perto em cada um deles e descobrir erros típicos de cada processo.
Preprocessamento
O preprocessamento é especificado pelos padrões C e C++, mas, tecnicamente, não faz parte da linguagem. Ou seja, antes que qualquer regra de sintaxe seja verificada no código-fonte, o preprocessamento já terá terminado.
Essa parte do processo lida com substituição de texto e diretivas baseadas em arquivos e símbolos. Por exemplo, a diretiva de preprocessamento mais conhecida
#include <stdio.h>faz com que todo o conteúdo do arquivo especificado seja incluído exatamente no ponto onde for colocada essa diretiva. Isso quer dizer que, antes sequer do código-fonte ser compilado, todo o conteúdo desse header padrão estará no corpo do arquivo C.
Para evitar que o mesmo header seja incluído inúmeras vezes dentro da mesma unidade em C, causando assim erros de redefinição, existe outra diretiva muito usada para cercar esses arquivos públicos:
#ifndef __MEUHEADER__ // se já estiver definido, caio fora até endif #define __MEUHEADER__ // conteúdo do header #endif // __MEUHEADER__Esse conjunto de duas diretivas, por si só, é capaz de gerar os mais criativos e bizarros erros de compilação em C. E estamos falando de erros que ocorrem antes que sequer seja iniciado o processo de compilação propriamente dito. Obviamente que os erros serão capturados durante a compilação, mas o motivo deles terem ocorrido foi um erro decorrente do processo de preprocessamento. Por exemplo, vamos supor que um determinado fonte necessita de uma declaração de função contida em meuheader.h:
#include "header-do-mal.h" #include "meuheader.h" int func() { meuheaderFunc(); }Porém, num daqueles acasos da natureza, o header-do-mal.h define justamente o que não poderia definir jamais (obs.: e isso pode muito bem acontecer na vida real, se usamos definições muito comuns):
#ifndef __HEADERDOMAL__ #define __HEADERDOMAL__ // tirei header da jogada, huahuahua (risos maléficos) #define __MEUHEADER__ #endif // __HEADERDOMAL__Na hora do preprocessamento, o preprocessador não irá mais incluir o conteúdo dentro de header.h:
#ifndef __MEUHEADER__ // se já estiver definido, caio fora até endif #define __MEUHEADER__ int meuheaderFunc(); // talvez alguém precise disso #endif // __MEUHEADER__Conseqüentemente, durante a compilação do código-fonte já preprocessado, sem a declaração da função meuheaderFunc, irá ocorrer o seguinte erro:
error C3861: 'meuheaderFunc': identifier not foundIsso em fontes pequenos é facilmente identificável. Em fontes maiores, é preciso ter um pouco mais de cuidado.
Após o processo de preprocessamento, de todos os arquivos indicados terem sido incluídos, de todas as macros terem sido substituídas, todas as constantes colocadas literalmente no código-fonte, temos o que é chamado unidade de compilação, que será entregue ao compilador, que, por sua vez, irá começar a análise sintática de fato, descobrindo novos erros que podem ou não (como vimos) ter a ver com a fase anterior. A figura abaixo ilustra esse processo, com algumas trocas conhecidas:
Dica: quando o bicho estiver pegando, e tudo o que você sabe sobre linguagem C não estiver te ajudando a resolver um problema, tente gerar uma unidade de compilação em C e analisar sua saída. Às vezes o que é claro no código pode se tornar obscuro após o preprocessamento. Para fazer isso no VC++ em linha de comando, use o parâmetro /E.
Compilação
Se você conseguir passar ileso para a fase de compilação, pode se considerar um mestre do preprocessamento. Por experiência própria, posso afirmar que a maior parte do tempo gasto corrigindo erros de compilação, por ironia do destino, não terá origem na compilação em si, mas no preprocessamento e linkedição. Isso porque o preprocessamento confunde muito o que vimos no nosso editor preferido, e a linkedição ocorre em uma fase onde não importa mais o que está dentro das funções, mas sim o escopo de nomes, um assunto um pouco mais vago do que a linguagem C.
Aqui você irá encontrar geralmente erros bem comportados, como conversão entre tipos, else sem if e esquecimento de pontuação ou parênteses.
int cannotConvertError(const char* message) { int ret = message[0]; return ret; } int ret = cannotConvertError(3); error C2664: 'cannotConvertError' : cannot convert parameter 1 from 'int' to 'const char *'if( test() ) something; something-else; else else-something;error C2181: illegal else without matching ifwhile( (x < z) && func(x, func2(y) != 2 ) { something; }error C2143: syntax error : missing ')' before '{'Claro, não estamos falando de erros relacionados a templates, que são um pesadelo à parte.
Dica: nunca subestime o poder de informação do compilador e da sua documentação. Se o erro tem um código (geralmente tem), procure a documentação sobre o código de erro específico, para ter uma idéia de por que esse erro costuma ocorrer, exemplos de código com esse erro e possíveis soluções. Ficar batendo a cabeça não vai ajudar em nada, e com o tempo, você irá ficar sabendo rapidamente o que aconteceu.
Linkedição
Chegando aqui, onde a esperança reside, tudo pode vir por água abaixo. Isso porque você já espera confiante que tudo dê certo, quando, na verdade, um erro bem colocado pode fazer você desistir pra sempre desse negócio de programar em C.
As características mais desejadas para corrigir erros nessa fase são:
- Total conhecimento da fase do preprocessamento
- Total conhecimento da fase da compilação
- Total conhecimento de níveis de escopo e assinatura de funções
Os dois primeiros itens são uma maldição previsível que deve-se carregar para todo o sempre. Se você não consegue entender o que aconteceu nas duas primeiras fases, dificilmente irá conseguir seguir adiante com essa empreitada. O terceiro item significa que deve-se levar em conta as bibliotecas que estamos usando, headers externos (com dependências externas), conflitos entre nomes, etc.
Alguns erros mais encontrados aqui são as funções não encontradas por falta da LIB certa ou por LIBs desatualizadas que não se encaixam mais com o projeto, fruto de muitas dores de cabeça de manutenção de código. Essa é a parte em que mais vale a pena saber organizar e definir uma interface clara entre os componentes de um projeto.
Do ponto de vista técnico, é a fase onde o linker junta todos os arquivos-objeto especificados, encontra as funções, métodos e classes necessárias e monta uma unidade executável, como ilustrado pela figura abaixo.
Dica: uma LIB, ou biblioteca, nada mais é que uma coleção de arquivos-objeto que já foram compilados, ou seja, já passaram pelas duas primeiras fases, mas ainda não foram linkeditados. Muitas vezes é importante manter compatibilidade entre LIBs e os projetos que as usam, de forma que o processo de linkedição ocorra da maneira menos dolorosa possível.
Erros além da imaginação
É óbvio que, por ter passado pelas três fases de transformação de um código-fonte em um programa executável, não quer dizer que este programa está livre de erros. Os famigerados erros de lógica podem se disfarçar até o último momento da compilação e só se mostrarem quando o código estiver rodando (de preferência, no cliente).
Entre esses erros, os mais comuns costumam se aproveitar de macros, como max, que usa mais de uma vez o parâmetro, que pode ser uma chamada com uma função. A função será chamada duas vezes, mesmo que aparentemente no código a chamada seja feita uma única vez:
#define max(a, b) ( a > b ? a : b ) int z = max( func(10), 30 );Um outro erro que já encontrei algumas vezes é quando a definição de uma classe tem um sizeof diferente do compilado em sua LIB, pela exclusão ou adição de novos membros. Isso pode (vai) fazer com que, durante a execução, a pilha seja corrompida, membros diferentes sejam acessados, entre outras traquinagens. Esses erros costumam acusar a falta de sincronismo entre os headers usados e suas reais implementações.
Enfim, na vida real, é impossível catalogar todos os erros que podem ocorrer em um fonte em C. Se isso fosse possível, ou não existiriam bugs, ou pelo menos existiria uma ferramenta para automaticamente procurar por esses erros e corrigi-los.
Um projeto cheio de erros

