Agora é oficial: Os conceitos (concepts) estão presentes no candidate draft do novo padrão C++0x (possivelmente C++09)!
O que são e para que servem os conceitos? Como eles estão relacionados aos templates C++ tradicionais? Quais são as vantagens e desvantagens de utilizá-los? Provavelmente, essas são perguntas bastante comuns para desenvolvedores que não são muito familiarizados com programação genérica. Se você, por acaso, é uma dessas pessoas e está interessado em entender um pouco mais sobre essa fantástica funcionalidade do novo C++, continue lendo.
Antes de falar sobre os conceitos é necessário falar sobre o paradigma da programação genérica. No mundo C++, a grande obra e referência desse paradigma, que vem crescendo a cada dia, é a STL (Standard Template Library). Outras referências importantes são a BGL (Boost Graph Library), a CGAL (Computational Geometry Algorithms Library) e a MTL (Matrix Template Library).
É relativamente comum escutar comentários negativos de programadores C++ pouco experientes em relação à STL. Particularmente, devido ao enorme número de templates de classes e de funções. No entanto, a arquitetura da STL é formulada propositalmente dessa forma com o intuito de promover um alto grau de genericidade (que no caso de C++, é obtida através do uso de templates). Adicionalmente, neste paradigma a flexibilidade e extensibilidade são os principais objetivos. Consequentemente, a “quebra” da orientação por objetos (OO) é inevitável em vários casos, o que contribui para o desconforto de certos programadores.
A grande idéia por trás da STL é a de trabalhar através de interfaces (no sentido abstrato) bem formuladas ao invés de trabalhar com tipos de dados (ou hierarquias de tipos de dados e polimorfismo dinâmico). Essas interfaces são descritas por tipos e operações que determinada implementação deve suportar. Tais interfaces são justamente os conceitos! Um conjunto de requisitos que estabelece os mecanismos pelos quais podemos manipular um tipo de dado específico. Se um tipo de dado satisfaz os requisitos de um conceito, ele representa um modelo para esse conceito.
Se você já visitou a página de documentação da STL no site da SGI, existem boas chances de já ter tido um primeiro contato com os conceitos. Lá estão descritos conceitos como Container, Forward Container, Sequence, Back Insertion Sequence, Trivial Iterator, Input Iterator, Random Access Iterator, entre outros. Clicando nos respectivos links a documentação do conceito em questão é acessada. Então, você irá descobrir quais são os requisitos ou “regras” necessárias para que um tipo de dados satisfaça ou modele o conceito. Por exemplo, o conceito Container determina que devem existir tipos associados chamados value_type
, size_type
, difference
, entre outros. Também determina um conjunto de expressões válidas como begin()
, end()
, size()
, empty()
, considerando os retornos e inclusive a semântica.
A pergunta natural então é a seguinte: Os conceitos já existem… já estão documentados… então, o que é exatamente e qual o porque ou necessidade dessa nova funcionalidade?
A resposta é simples e o primeiro ponto é entender como funciona o mecanismo de templates C++. O que acontece quando o compilador encontra uma função template com nome print
e que é parametrizada por um tipo T
? Grosso modo, ele realiza uma validação sintática superficial, na qual são detectados erros simples como, por exemplo, um ponto e vírgula faltante. Mais tarde no processo de compilação, quando o tipo ticket
que você passou para a função print
estiver definido, o compilador volta nesta função, substitui T
por ticket
e realiza uma nova validação sintática, agora mais profunda, para que a função print
possa ser instanciada. Somente nessa segunda validação, o compilador tenta encontrar corretamente os tipos que você declarou que T
possui (utilizando typename
), as chamadas de funções membro que você realizou sobre T
, chamadas de funções globais que recebem T
como argumento, etc. Ou seja, só na validação posterior o compilador pode, de fato, detectar se alguma operação esperada para o parâmetro T
não pôde ser encontrada ou validada para o argumento ticket
fornecido para a função.
struct ticket
{
int id;
};
template
void print(T const& t)
{
std::cout << t;
}
int main()
{
ticket obj;
print(obj);
return 0;
}
[/sourcecode]
A parte chata de todo o processo é que você pode cometer um erro e se esquecer de fornecer um operator<<
válido para o tipo ticket, como no código acima. Mas o compilador só descobrirá esse problema no momento em que fizer o parse de std::cout << t
na função isntanciada (a que contém T
substituído por ticket
). Particularmente para esse o exemplo, a mensagem de erro é relativamente simples. Porém, em programas mais complexos e que envolvem um número grande de templates as mensagens de erro se tornam gigantescas e pouco inteligíveis. Inclusive, existe até uma ferramenta chamada STLFilt que objetiva “decifrar” as mensagens obscuras geradas por templates na STL.
Resumindo toda a história, o problema é que atualmente os requisitos esperados para um template estão no “papel”. Nós, programadores, trabalhamos com código-fonte. Obviamente, o compilador não conhece o papel e, consequentemente, não possui um mecanismo eficiente para nós auxiliar a escrever código-fonte de maneira consistente. Qual é a solução então? Colocar os conceitos dentro do código-fonte; Formalizá-los; Submetê-los ao compilador para que possamos trabalhar com templates de forma mais segura e menos sujeita a erros; Basicamente, gostaríamos de validar a função print
mais ou menos conforme descrito abaixo.
template
/* Se e somente se existe um operator<< válido para T. */
void print(T const& t)
{
std::cout << t;
}
[/sourcecode]
Apesar de ser uma das principais motivações para a introdução dos conceitos no C++, a idéia de validação sintática conforme no código acima é apenas de suas características. Através dos mapas de conceitos é possível transformar tipos de dados para se comportarem de acordo com os requisitos esperados sem que haja alterações em suas respectivas implementações. Também é possível a criação de axiomas que determinam a semântica esperada para funções associadas a um conceito. Resolução de sobrecarga de templates também levam em consideração os conceitos especificados. Enfim, há vários detalhes nessa nova funcionalidade que trazem um enorme poder de expressão para os templates C++. E tudo sem a necessidade de qualquer relação hierárquica entre os tipos de dados envolvidos.
Em um próximo post mostrarei como está ficando a "cara" dos conceitos no novo C++. Para quem quiser sentir um gostinho, deixo um trecho que código que corresponde ao exemplo da função print
.
auto concept OutputStreamable
{
std::ostream& operator<<(std::ostream&, const T&);
}
template
requires OutputStreamable
void print(T const& t)
{
std::cout << t;
}
struct ticket
{
int id;
};
std::ostream& operator<<(std::ostream& out, ticket const& t)
{
out << "Id: " << t.id;
return out;
}
int main()
{
ticket obj;
print(obj);
return 0;
}
[/sourcecode]
Leandro T. C. Melo
Read Full Post »