C, C++ e escopos

Recentemente um conhecido me fez algumas perguntas sobre a palavra reservada static. Daí, me dei conta de que o operador de resolução de escopo de C++ não é lá essas coisas, afinal de contas. Do que estou falando?

Variáveis e funções podem estar localizadas em dois escopos: global e local. No escopo global o símbolo é acessível por quaisquer funções. No escopo local ela só é acessível pela função onde foi declarada. Por causa disso podemos ter dois símbolos com o mesmo nome em escopos diferentes. Por exemplo:

int x = 1;   /* Escopo global! */

int f(int a)
{
  int x = 2;   /* Escopo local! */

  return a + x;
}

A variável x, dentro da função, não é a mesma variável x de fora da função. De fato, num caso desses, em C, é impossível acessar a variável do escopo global. C++ resolve isso com um operador extra chamado operador de resolução de escopo, ou ::. O mesmo código, em C++, mas acessando a variável global x, poderia ser escrito assim:

int x = 1;   /* Escopo global! */

int f(int a)
{
  int x = 2;   /* Escopo local! */

  return a + ::x; /* Usa o x global. */
}

É útil, não é? Parece que C++ resolveu todos os problemas de escopo com esse novo operador e você poderia pensar assim até se deparar com um código como este:

#include <iostream>

int x = 1;

int f(void)
{
  int x = 2;

  {
    int x = 3;

    return x + ::x;  /* Usa x deste bloco 
                        e qual dos outros dois x?!
  }
}

int main(void)
{
  std::cout << f() << '\n';
}

O programinha vai imprimir 4. O operador :: sem a especificação de um namespace do lado esquerdo, resolve sempre o escopo para global! Não há como, neste caso, usar o x do escopo local imediatamente acima, fora do bloco interno, da mesma forma que acontece com C.

O motivo é o seguinte: O operador de resolução de escopo não foi criado para resolver escopo local/global, mas para resolver escopo de dois namespaces diferentes — o de classe e o namespace “default”. Só mais tarde a palavra reservada namespace foi adicionada à especificação e namespaces adicionais puderam ser criados. O operador de resolução de escopo passou a resolver escopo desses “namespaces” também. Por isso você tem que rsolver o escopo do símbolo cout que foi declarado no namespace std como std::cout, no exemplo acima.

O que quis dizer com resolução de namespaces é que, ao declarar um membro de uma classe, você pode especificar o nome do membro precedido do nome da classe para resolver ambiguidades como essa:

extern int f(void);

struct X {
  int x;
  int f(void) { return x; }
  int g(void) { return f(x); }
};

Na função-membro g() qual função f() está sendo usada? A definida na estrutura ou a global? A especificação nos diz como isso é resolvido, mas você pode ficar em dúvida, assim a resolução de escopo pode vir em nosso auxílio, bastando usar X::f(x) ou ::f(x), de acordo com quem queremos chamar, explícitamente.

ESSA era a ideia do operador. Suspeito que a resolução local/global seja um efeito colateral porque ele não resolve escopos de locais de níveis superiores, como demonstrei lá em cima.

Símbolos static e membros static:

Uma coisa “estática” é algo que não se move. No contexto de C um símbolo (variável ou função) static global só existe dentro do módulo (do arquivo com extensão ‘c’) em que foi criado. No escopo local o símbolo só existe dentro do escopo do bloco, mas ele é, na verdade, global ao módulo e, portanto, seu conteúdo persiste entre chamadas à função. Podemos pensar em símbolos static, nesses dois escopos, como sendo “membros” do módulo ou da função e tendo um atributo “private”, como em C++.

Mas o uso de static num membro de uma classe, em C++, tem semântica um cadinho diferente. Esse membro é um membro de classe, não de instância! Isso quer dizer que todos os objetos da classe compartilham acesso ao mesmo membro… No caso do membro ser uma função, já que ela não pertence à instância, ela não receberá o ponteiro implícito this e, assim, não conseguirá acessar os membros de instância. No caso de um membro de dados static, ele terá escopo global ao módulo em que foi definido, mas será acessível apenas por membros da classe ou via resolução de escopo.

Ou seja, declarar um membro de dados static é a mesma coisa que declarar um símbolo de escopo global static. De fato, se quisermos inicializar o membro de dados static antes que qualquer instância da classe seja criada, temos que fazer algo assim:

struct X {
  static int a;
};

// Inicializa o membro static da estrutura X.
int X::a = 1;

Qual é a diferença entre declarar a variável a em escopo global, mas static, e declará-la no “escopo” da classe? Nenhuma! Exceto pelo fato de que, num outro módulo, este membro está acessível via resolução de escopo. O mesmo acontece com funções-membro static.

Símbolos em escopo global não static são sempre extern, por default:

Essa é outra dúvida recorrente… Ao declarar uma variável ou função globais, o símbolo que define seus nomes são sempre exportados e acessíveis em outros módulos. Tanto faz declarar:

int x;
extern int x;

Ambos os símbolos x referem-se à mesma variável, não importa aonde… Você pode declarar, num módulo, o símbolo x e inicializá-lo com 1 e, em outro módulo, declará-lo sem a inicialização. Não importa, se foi inicialzado em um módulo ele contém o valor em outro…

/* a.c */
int x = 1;
/* b.c */
int x;  /* É o mesmo x de a.c */

Mas, o que acontece se inicializarmos o símbolo nos dois módulos e depois linkarmos?! Eis um teste:

$ cat > test.c <<EOF
#include <stdio.h>
int x;
void main(void) { printf("%d\n", x); }
EOF
$ cat > a.c <<< "int x=1;"
$ cat > b.c <<< "int x=2;"
$ gcc -c *.c
$ gcc -o test *.o
b.o:(.data+0x0): multiple definition of `x'
a.o:(.data+0x0): first defined here

Repare que o GCC compila os códigos em C sem nenhum problema, o linker é que reclama, na hora de mesclar os símbolos. E ele só te disse que o símbolo foi definido primeiro em a.o porque tentou linká-lo primeiro. Se fizéssemos “gcc -o test b.o a.o test.o“, provavelmente ele diria que x foi definido primeiro em b.o. O mesmo erro vai acontecer se você definir funções com o mesmo nome em mais que um módulo. Para evitar isso temos a declaração de protótipos:

int f(int); /* Protótipo */

int f(int x) { return x+x; } /* Definição */

Da mesma maneira que variáveis, funções são extern por default e as declarações de protótipos só servem como resolução simbólica (diz ao compilador: “o símbolo f é uma função e está definida em algum outro lugar). Ao declarar símbolos como static você restringe o escopo ao próprio módulo, não incomodando o linker. Assim, você poderá declarar duas funções nomeadas f, em módulos diferentes, desde que ambas sejam static.

E a sobrecarga de funções?

Aqui vale uma explicação sobre a sobrecarga de funções do C++. Você pode definir duas ou mais funções com o mesmo nome desde que elas tenham um conjunto diferente de parâmetros. Isso só funciona porque, por baixo dos panos, as funções não têm o mesmo nome! Veja um exemplo:

$ cat > test.cxx <<EOF
int f(int x) { return x + x; }
int f(int x, int y) { return x + y; }
int f(int x, int y, float s) { return (int)(x*s) + y; }
int f(int x, ...) { return x; }
EOF
$ g++ -c test.cxx -o test.o
$ objdump -t test.o

test.o: file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 test.cxx
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g F .text 000000000000000e _Z1fi
000000000000000e g F .text 0000000000000014 _Z1fii
0000000000000022 g F .text 0000000000000028 _Z1fiif
000000000000004a g F .text 000000000000005d _Z1fiz

Graças ao artifício do name mangling esse tipo de coisa é possível.Mas isso tem uma limitação… O tipo de retorno da função não entra no código. Mude o tipo de retorno de uma das funções para float e você verá que seu nome continua exatamente o mesmo.

Esse esquema de “deformar” (to mangle, em inglês) tem consequências para o desenvolvedor assembly: Torna o código ilegível! Experimente lidar com estruturas, como numa declaração do tipo “int f(X& a, X const * const b, int (*fptr)(const void *, const void *), int c);” e você acabará com uma função nomeada _Z1fR1XPKS_PFiPKvS4_Ei (sério!).

Infelizmente cada compilador usa seu próprio esquema de codificação de nomes. O GCC faz de um jeito, o Visual Studio de outro, o Intel C++ Compiler (provavelmente) de outro jeito diferente e por ai vai…

Anúncios