Archive for 2nd March 2010

Namespaces: colocando cada coisa em seu lugar

Evitando conflitos de nomes

namespace é uma palavra reservada do C++ que implementa um mecanismo capaz de expressar agrupamento lógico. Em outras palavras, se um conjunto de declarações estão logicamente associadas de acordo com algum critério, elas podem ser colocadas juntas no mesmo namespace para expressar este fato. Um namespace também pode ser chamado de um contexto. O uso de namespaces, além de proporcionar agrupamento lógico,  serve para evitar o conflito de nomes: dois identificadores podem ter o mesmo nome se estiverem em namespaces diferentes. Como é um mecanismo que define escopo, o nome de um identificador fora de seu namespace deve ter o nome do namespace na frente, seguido do operador de escopo “::”.  Namespaces também afetam o mecanismo de mangling que o C++ usa.

A sintaxe para namespace é bem intuitiva, semelhante à de uma classe. No entanto, um namespace define apenas escopo, e não um tipo em si. Funções declaradas dentro de um namespace não são métodos, são apenas funções.

Exemplo:

#include <cstdlib>
#include <iostream>

namespace Teste {
  void escreveMensagem() {
    std::cout << "Uma mensagem qualquer!" << std::endl;
  }
}

int main(int argc, char *argv[])
{
  Teste::escreveMensagem();

  system("PAUSE");
  return EXIT_SUCCESS;
}

No exemplo acima, declaramos um namespace Teste e dentro deste, a função escreveMensagem(). Note na linha 6 o uso de identificadores declarados em outro namespace: std::cout e std::endl. cout e endl são variáveis declaradas dentro do namespace std. O uso de um identificador com o seu namespace é chamado de nome qualificado ou completo.

Usando Namespaces

Em um determinado ponto pode se tornar tedioso ter de repetir o nome do namespace sempre que se quer utilizar um determinado identificador de um outro namespace. Para estas situações existem duas abordagens previstas no C++.

1) using namespace std;

Essa construção, chamada de global using-directive, é mais geral pois importa todos os identificadores do namespace std, e por isso deve ser utilizada com bastante parcimônia: tem como consequência justamente anular o mecanismo de separação de escopos proposto pelo namespace.

Há autores inclusive que defendem que seu uso deve ser limitado apenas a código herdado, ou seja, sistemas e bibliotecas antigos, escritos antes da introdução desse mecanismo em C++. Não sou tão radical, para pequenos programas acho essa sintaxe bastante conveniente. Deve ser porque programo em C++ desde 1989, quando não havia namespaces (nem templates, nem exceções, nem herança múltipla… é, pode me chamar de velho! :) ). Para sistemas grandes porém, é melhor realmente não utilizar essa opção. Em seu livro “The C++ Programming Language” Bjarne Stroustrup recomenda o seguinte (tradução livre): “Diretivas globais de using são uma ferramenta de transição e de outra forma devem ser evitadas. Em um namespace, a diretiva using é uma ferramenta para a composição de namespaces.  Em uma função (somente), uma diretiva using pode ser seguramente utilizada como uma conveniência notacional”.

2) using std::cout;

Essa construção importa apenas o identificador cout, sendo bem mais seletiva. Nesse caso o identificador endl não foi importado, sendo necessário para o seu uso o nome completo: std::endl.

Cabe dizer que uma diretiva using (global ou não) pode ser colocada nos seguintes  escopos: global, dentro de um namespace, dentro de uma função, dentro um método ou mesmo dentro de um bloco. Não é permitido colocar using diretamente dentro de uma classe ou estrutura. Esse posicionamento por si só define o escopo da importação em questão. Via de regra, deve-se utilizar o menor escopo possível, pesando-se sempre a conveniência da notação com a violação da separação de escopos.

Boas práticas

Um namespace não precisa declarar tudo o que será utilizado de uma só vez, e embora seja confuso ter mais de uma declaração com o mesmo namespace isso é permitido (porém desaconselhável). No entanto, é razoável ter em um lugar (um arquivo “.h”, por exemplo) apenas as declarações e em outro lugar a implementação propriamente dita. Há duas formas de se fazer isso, ilustradas a seguir. A primeira faz uma extensão na declaração do namespace, a segunda usa o nome qualificado do identificador sendo definido. Particularmente acho perigosa a primeira opção, prefiro a segunda.

Dividindo namespaces

#include <cstdlib>
#include <iostream>

namespace Teste {
  void escreveMensagem();
}

int main(int argc, char *argv[])
{
  Teste::escreveMensagem();

  system("PAUSE");
  return EXIT_SUCCESS;
}

// em qualquer outro lugar ou arquivo!!

namespace Teste {
  void escreveMensagem() {
    using namespace std;

    cout << "Uma mensagem qualquer!" << endl;
  }
}

Implementação usando nomes qualificados

#include <cstdlib>
#include <iostream>

namespace Teste {
  void escreveMensagem();
}

int main(int argc, char *argv[])
{
  Teste::escreveMensagem();

  system("PAUSE");
  return EXIT_SUCCESS;
}

// Em qualquer outro lugar ou arquivo!!

void Teste::escreveMensagem() {
  using std::cout;
  using std::endl;

  cout << "Uma mensagem qualquer!" << endl;
}

Um namespace pode conter outros namespaces. Nesse caso teremos um namespace aninhado a outro namespace. O conceito é o mesmo de aninhamento de blocos ou aninhamento de classes. Inclusive, se o namespace A contém o namespace B que por sua vez contém o identificador X, o nome qualificado de X é A::B::X. Por outro lado, um namespace somente pode ser declarado no escopo global ou dentro de outro namespace. Não é permitido que um namespace seja declarado dentro de uma função, classe ou método.

Podemos também definir aliases para namespaces. O escopo é semelhante ao de uma declaração using: global, dentro de funções, métodos ou blocos. Um alias é uma forma conveniente de dar outro nome a um namespace. A vantagem é poder trocar rapidamente um pacote de uma biblioteca por outro (desde que as interfaces sejam compatíveis, lógico). A sintaxe é:

void foo() {
  namespace NovoNome = std;

  NovoNome::cout << "Uma mensagem" << NovoNome::endl;
}

Por último, por mais estranho que pareça, é permitido declarar um namespace sem nome. Nesse caso o compilador cria um nome temporário, que não pode ser acessado de nenhum outro lugar a não ser a unidade sendo compilada – esta inclusive pode acessar livremente qualquer identificador dentro desse namespace. Ou seja, funciona como uma restrição de escopo no estilo static functions.

“In my place, in my place / Were lines that I couldn’t change