O próximo padrão de C++

Quem quer que já tenha programado em C++, decerto já possui conhecimento suficiente da linguagem e das bibliotecas padrão para ter familiaridade com os seus múltiplos paradigmas. Objectivamente, o C++ é uma linguagem complexa e muito abrangente: permite facilmente programação procedimental (que muitos chamam programar a la C), permite o uso de polimorfismo, tanto dinâmico como estático, e finalmente, permite também metaprogramação. Na realidade, C++ pode ser igualmente utilizado tanto por iniciados como por peritos, possui um certo grau de flexibilidade, uma sintaxe por vezes particularmente dúbia, e de uma curva de aprendizagem desproporcionalmente íngreme para os programadores mais ambiciosos.

São as imperfeições, as limitações, e as potencialidades sub-aproveitadas da linguagem que levam a Comissão do Padrão de C++ a reformá-la quando necessário. Entre as figuras mais proeminentes desta Comissão, encontra-se por exemplo, o autor da linguagem, Bjarne Stroustrup, bem como diversos profissionais de tecnologias de informação que trabalham com, ou para, diversas empresas que ao longo dos anos têm promovido a linguagem e proposto extensões. Para tornar o C++ uma linguagem mais intuitiva e de mais simples aprendizagem, e ao mesmo tempo manter compatibilidade com código existente, eis algumas das muitas interessantes alterações e novas funcionalidades que nos esperam.

Alterações na sintaxe e inferência de tipos

O C++ é invariavelmente alvo de críticas devido à sua sintaxe e regras de semântica complicadas. No próximo padrão recomendado, haverá algumas novidades significativas e outras de menor impacto. Para começar, podemos mencionar o famoso exemplo de código dado por Stroustrup, que qualquer compilador que respeite o padrão de 2003 rejeita, tornado correcto no próximo padrão.

// Novo código em C++ (Baseado em Stroustrup, 2006)

template<class T> using Vec = vector<T,My_alloc<T>>;
Vec<double> v = { 2.3, 1.2, 6.7, 4.5 };
sort(v);
for(auto p = v.begin(); p!=v.end(); ++p)
    cout << *p << endl;
decltype(&v) v_ptr = &v;

Uma novidade menos importante é a desambiguidade na sintaxe de templates. Ao processar vector<T,My_alloc<T>>, um bom compilador deverá conseguir discernir entre o operador de right shift >> e templates compostos. Na verdade, já existiam muitos compiladores com esta extensão, pelo que incorporar este comportamento nos requerimentos padrão de C++ não deverá constituir qualquer surpresa.

A própria linha de código em que notamos esta pequena conveniência mostra outra bem mais interessante: passará a ser possível usar designações alternativas parametrizadas para templates, da mesma forma que nos habituámos a fazer uso do typedef (que não permitia qualquer forma de parametrização). A palavra-chave using permitirá simplificar a sintaxe para templates de classes que normalmente seriam demasiados complicados e que dificultariam a manutenção do código. Neste exemplo, vector<T,My_alloc<T>> passa a poder declarar-se usando Vec<T>. Da mesma forma, using passa a poder ser utilizado em substituição da antiga sintaxe com typedef.

A linha seguinte mostra-nos uma inicialização de um objecto vector que faz lembrar a inicialização de vectores estáticos. No novo padrão, a chaveta {} deverá ser utilizada em detrimento do parêntesis na chamada de construtores de objectos. O motivo por trás desta adição é a necessidade de permitir ao programador diferenciar melhor construtores, funções, e operadores de conversão (que até agora, partilhavam o uso indiscriminado dos parêntesis). Será também possível, através da nova classe STL list_initializer<>, atribuir a qualquer classe a capacidade de inicialização através de vector estático. Note-se que isto já é possível com a ajuda de algumas bibliotecas independentes (por exemplo, a boost utiliza este estilo de construtores em algumas das suas classes), mas o método virá a tornar-se parte da STL.

Finalmente, e ainda parte do mesmo exemplo, é a sintaxe simplificada na declaração do iterator do ciclo for. Com o próximo padrão, será possível contar com o compilador para deduzir o tipo das variáveis, com as palavras-chave decltype e auto. Ao declarar uma variável usando auto, o compilador verá no lado direito da expressão qual o tipo de variável que o programador tenciona usar. Claro que em circunstâncias dúbias se poderá ainda obter erros, mas em casos convenientes, como o que é dado no exemplo, é escusado estar a escrever explicitamente o tipo do iterador que se espera. Quem quer que esteja habituado a utilizar iteradores com a STL, reconhece de imediato as vantagens da declaração automática de variáveis. O exemplo que se segue mostra-nos também a nova sintaxe alternativa para o ciclo for, com inferência de iterador. Este novo tipo de ciclo for deverá funcionar com vectores estáticos (como o do exemplo) ou contentores STL.

int my_array[5] = {1, 2, 3, 4, 5};
for(int &x : my_array) // Equivalente a um for_each
{
  x *= 2;
}

Este novo tipo de ciclo for deverá funcionar com vectores estáticos (como o do exemplo) ou contentores STL.

Alterações a uniões, classes e tipos enumerados

No próximo padrão de C++, as uniões terão regras mais relaxadas de declaração. Por enquanto, não é permitido utilizar dentro de uniões tipos que envolvam construtores não triviais (i.e., construtores de classes com mais de uma variável). No próximo padrão, isto deverá ser possível, tal como exemplificado no seguinte código.

class Ponto
{
private:
  double x = 0.0;
  double y = 0.0;
public:
  Ponto() = default; // Construtor inferido por omissão
  Ponto(double xx, double yy) : x(xx), y(yy) {}
  Ponto(double dd) : Ponto(dd,dd) {} 
  // Operador new proibido
  void *operator new(std::size_t) = delete;
};

class Coordenada : public Ponto
{
public:
  using Ponto::Ponto; // Construtores por herança
}

union exemplo // União com construtor não trivial
{
  double r;
  Ponto p;
};

enum class Ordinal
{
  primeiro = 1,
  segundo,
  terceiro
};

Também patente no exemplo dado acima é a classe Ponto, que mostra novidades significativas que irão tornar a vida mais fácil a muitos programadores no futuro. Utilizando o novo padrão, as classes terão à sua disposição novas facilidades para a sua definição. A primeira novidade é a introdução de inicialização na declaração dos membros da classe. Os membros x e y podem ser inicializados dentro da sua declaração na classe, sem necessidade de utilizar sequer valores de omissão nos construtores. De forma semelhante, igualando um construtor à palavra-chave default, pode-se definir um construtor de omissão como “construtor óbvio de omissão”, que dispensa definição. Por oposição, pode-se também proibir certos construtores, operadores, ou destrutores, igualando-os a delete (no exemplo, proíbe-se a alocação dinâmica de objectos Ponto, proibindo o operador new).

Também interessante é a nova capacidade de delegar construtores, definindo um construtor em termos de outro construtor (à semelhança do que já acontece em Java ou C#). Outra nova mecânica de construção, exemplificada na classe Coordenada, é a herança de construção. Através da palavra-chave using, indicamos ao compilador que a classe Coordenada replica todos os métodos de construção de Ponto. Note-se que tirando partido deste estilo de construção, não se podem definir quaisquer outros construtores.

Finalmente, os tipos enumerados passarão a ser mais distintos de inteiros. Adicionando a palavra-chave class à declaração de um tipo enumerado (como exemplificado em Ordinal), podemos também explicitar outros tipos inteiros, tais como long ou char, e mesmo caso seja baseado em int, dizemos ao compilador que qualquer conversão entre inteiros e o novo tipo enumerado declarado é proibida. Isto evita, em particular, os problemas da conversão implícita entre inteiros e tipos enumerados. As constantes das classes enumeradas passarão a habitar no seu próprio namespace (no caso do exemplo, Ordinal::primeiro, Ordinal::segundo, etc.). Note-se que este reforço do tipo é, tal como maior parte das adições à linguagem, opcional.

Novo operador de sufixo

Uma nova adição à linguagem é o novo operador de sufixo, ou melhor, a possibilidade de sobrecarregar o operador de sufixo. O operador de sufixo permite definir símbolos literais para definição automática de tipos ou para unidades arbitrárias. A STL fará uso imediato desta funcionalidade no tipo std::complex. No entanto, a definição de símbolos literais pode também ser utilizada em cálculos científicos, tornando código numérico mais fácil de escrever, ler e gerir. O exemplo seguinte mostra como esta facilidade ajuda a reduzir complexidade na sintaxe.

// Unidade imaginária
template<typename T>
std::complex<T> operator "" i(T& numero)
{
  return std::complex<T>(T(), numero);
}

// Exprimir velocidade em unidades de c
double operator "" c(double numero)
{
  return numero*299792458.0; // S.I.
}

//…

const std::complex<double> o = 1.0 + 5.2i;
const double veloc = 0.98c;    // Velocidade

Semântica de movimento

Não só se beneficia de escrita mais simples, como também de mais ferramentas para tornar os nossos programas mais eficientes. Variáveis temporárias são a maior contribuição para o excesso de reserva de memória num programa de C++. E isto ainda acontece numa era em que a esmagadora maioria de processadores existentes tem instruções para mover porções arbitrárias de memória directamente entre endereços distintos. Até agora, pudemos contar com compiladores que façam optimizações inteligentes em determinadas circunstâncias para tirar proveito do movimento de variáveis. Com o novo padrão, é possível declarar argumentos de funções ou variáveis usadas temporariamente, com o prefixo &&. Desta forma, indica-se ao compilador quando deve forçar o movimento de variáveis. Com esta nova semântica, classes de C++ poderão beneficiar de construtores de movimento no futuro, efectivamente transformando objectos, mas com um mínimo de intervenção por parte do programador. Esta semântica pode ser melhor explicada através de um simples exemplo que utiliza a célebre função swap:

// Esta função força o movimento de objectos
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& a)
{
    return a;
}

class objecto
{
  // ...
  public:
    objecto() = default;
    objecto(const objecto &o); // cópia
    objecto(objecto &&o); // movimento
};

template<typename T>
void swap(T &a, T &b)
{
  T tmp(move(a)); // move o conteúdo de a para tmp
  a = move(b); // move o conteúdo de b para a
  b = move(tmp); // move o conteúdo de  tmp para b
}

A função swap do exemplo dado permite efectuar permutação sem cópia, e utiliza um método auxiliar move que força o movimento de variáveis, independentemente de serem chamadas por referência. Note-se que apesar de poupar recursos como visado, este swap baseado em movimentos não é completamente seguro, nem é sempre desejável. Como é fácil imaginar, a semântica de movimento é desejada para optimizações em algoritmos, e o seu desuso potencialmente cria mais problemas que aqueles que supostamente resolve.

Novidades para templates

No próximo padrão, os templates verão a sua utilidade expandida por intermédio dos templates variádicos: da mesma forma que existem funções variádicas, com um número de argumentos que não está pré-definido, templates variádicos aceitam qualquer número, ou tipo, de argumentos.

Tirando partido desta nova funcionalidade, é possível criar templates de funções variádicas, ou mais interessante ainda, com templates de expressões, que abrem mais possibilidades em metaprogramação com templates. Até agora, o limite imposto pela pré-determinação da parâmetros de templates forçava autores de bibliotecas que façam uso extenso de templates a recorrer a truques de macros e a repetir código para esconder esses limites ao programador. Com templates variádicos, o próprio compilador gera templates com parâmetros repetidos, à medida que estes sejam necessários. De notar que esta iteração é de um grau diferente da iteração de classes e métodos que a metaprogramação com templates utiliza, e que como tal, permitirá reduzir a quantidade de código necessário.

Uma grande adição ao mecanismo de templates, com um grande impacto prático, seria o novo mecanismo de conceitos, que teria melhorado drasticamente as mensagens de erro obtidas com o mero uso da STL. Infelizmente, a implementação de conceitos gerou grandes complicações de retrocompatibilidade na sua implementação, e como resultado, estes foram adiados para outra ocasião.

Adições à Standard Template Library

A biblioteca oficial do padrão tem acompanhado as restantes adições, desde a criação dos templates. Com o próximo padrão, a STL verá uma colecção de novas classes, adaptadas de bibliotecas independentes (um grande contribuidor é a biblioteca boost), que serão muito úteis. Por exemplo:

  • std::thread: Classe que permite lançar uma thread, e que necessita de uma classe funcional (i.e., uma classe com um operador de parêntesis) para definir a sua execução. Para gerir recursos partilhados entre operações entre threads, haverá mutexes (std::mutex, por exemplo), ou caso seja necessário código de extrema eficiência, a STL terá também tipos atómicos para o efeito.
  • Tipos atómicos: uma série de classes auxiliares desenhadas para multithreading. Operações, que partilham recursos no tempo entre threads, e que mediante violações de acesso à memória, resultam em mais erros de compilação que erros de execução, poupando assim tempo no teste de software.
  • std::tuple: a extensão de maps (pares ordenados de variáveis, em que a primeira é índice da segunda) a várias variáveis. Tal como maps, tuples são ordenados automaticamente. Tornado possível graças a templates variáticos.
  • std::regex: Expressões regulares, algo que também já se implementara inúmeras vezes em bibliotecas independentes. A STL finalmente fornecerá a programadores uma implementação padrão de uso simples.

Conclusão

Outras novidades que não mencionámos aqui, mas também importantes para o próximo padrão do C++ incluem: um novo símbolo para o apontador nulo: nullptr. O nullptr tem o seu próprio tipo definido, que permite eliminar muitas ambiguidades, fontes de erros no código, e claro, é mais elegante do que escrever NULL; funções Lambda para programação funcional em C++; modificadores de sobrecarga de métodos em classes derivadas; expressões constantes (constexpr) e literais unicode para strings.

O novo padrão de C++, até agora apelidado C++0x (uma indicação de que o prazo para a sua definição seria o final da década) terá efectivamente o seu draft final concluído no final de 2009 (à custa de sacrificar algumas das suas inovações mais ambiciosas), mas só será submetido à ISO na primeira metade de 2010. Em média, os padrões oficiais ISO levam cerca de um ano a ser aprovados, o que levará o próximo padrão a finalmente tornar-se “oficial” no início de 2011. Felizmente, os compiladores mais populares (tais como Visual C++, GCC e Borland C++) já incluem há algum tempo as alterações de drafts já conclusivos, como por exemplo o TR1 (Technical Report 1).

Como uma linguagem de programação de sistemas de interesse académico e tecnológico, o C++ evolui a um passo contido, mas de forma mais ou menos consistente com as aplicações que lhe foram delegadas ao longo do tempo. Claro está, a linguagem não existe num vácuo, e a evolução de outras linguagens também influencia a sua. Apesar desta influência, o novo padrão de C++ ainda não adoptará garbage collection, por motivos de complexidade em manter novo código gerido compatível com código determinista, e a gestão automática de memória opcional.

Mais importantes que as alterações drásticas à linguagem, a comissão do padrão de C++ optou por refinamentos à sintaxe que permitem simplificar a escrita e a leitura de código, bem como inclusões importantes à STL. Melhoramentos estes que decerto beneficiarão programadores independentemente do seu grau de destreza com a linguagem.

Referências

Publicado na edição 22 (PDF) da Revista PROGRAMAR.