Static Metaprogramming In C++

 

Edson R. Amboni1

John C. Jaraceski1

Ronivaldo F. Moretti1

amboni@inf.ufsc.br

john@inf.ufsc.br

moretti@inf.ufsc.br

 

 

 

1Universidade Federal de Santa Catarina

Centro Tecnológico

Departamento de Informática e Estatística

 

Desenvolvimento Orientado a Objetos II

Prof. Dr. Antônio Augusto M. Fröhlich

 

1.   Introdução

A otimização, geração de código e automação do processo desenvolvimento são os objetivos que podem sumarizar a Static Metaprogramming In C++ descrita em [CE99]. Mesmo assim, vista isoladamente, esta técnica pode parecer complexa e de difícil utilização.

Então antes de discorrer sobre ela, é interessante entender o paradigma de Generative Programming e os conceitos básicos sobre Metaprogramming. E então após isto analisar a Static Metaprogramming In C++, e verificar sua relação com a Generative Programming.

2.   Generative Programming

A Generative Programming trata sobre a modelagem de famílias de sistemas como componentes de software, analisando os requisitos específicos de cada componentes, permitindo alta customização e otimização, e fazendo uso de bases de conhecimento que fornecem subsídios para criação de geradores de código para automatização do processo de desenvolvimento [CEGV98].

Com base nesta definição pode-se relacionar e ressaltar as metas da Generative Programming: aumento da reusabilidade e adaptabilidade, controle da complexidade, gerenciamento de grande número de variações e aumento da eficiência.

Estes objetivos não pertencem unicamente a este paradigma. Muitos outros paradigmas tentam alcançar estes mesmos objetivos de maneiras diferentes. Mas então qual o diferencial da Generative Programming?

Na realidade ela retira aspectos e características de outros paradigmas, unificando-os, aproveitando assim o melhor de cada universo. Conforme citado por [CEGV98]:

§         Generic Programming: utilização das características de reutilização de componentes através de parametrização, aumentando a capacidade de criar-se componentes customizáveis e altamente eficazes.

§         Aspect-Oriented Programming: decomposição do domínio do problema em unidades funcionais e aspectos. Fazendo então a associação de componentes e aspectos para produzir o sistema final.

A Generative Programming vai além disso, pois prevê mecanismos para automatização de configurações e prevê maior generalidade de código.

§         Domain-Specific Language: fornece mecanismo para aproximar os conceitos do domínio do problema ao ambiente de programação. Aumentar a capacidade da linguagem de programação para manipular níveis mais altos de abstração.

3.   Metaprogramming

[RIDEAU99] define Metaprogramming como: “A arte de programar programas que lêem, transformam ou escrevem outros programas”.

Partindo deste conceito pode-se relacionar alguns dos metaprogramas mais conhecidos do público:

§         Compiladores

§         Interpretadores

§         Depuradores

Estas três classes de metaprogramas recebem programas como parâmetro, fazem algum processamento sobre estes programas ¾ no caso de um compilador é feita a análise léxica e sintática ¾, e ao final podem gerar outros programas, como no de um compilador.

Fazendo então uma análise sobre as características destes três metaprogramas surge uma forma de classificação. Esta classificação relaciona-se as entradas recebidas (programas) por um metaprograma e as saídas retornadas (programas) [RIDEAU99], descrita a seguir:

§         0 ® 0: não recebem e não retornam código, então não classificam-se como metaprogramas.

§         N ® 0: recebem entradas e não retornam nenhuma.

Pode-se inserir neste grupo os interpretadores (recebem um programa e o executam), metainterpretadores (interpretam um programa de acordo com uma linguagem), testadores automáticos (verificam o comportamente de um programa recebido), analisadores de diferenças (recebem dois programas e analisa as diferenças entre ambos), entre outros.

§         N ® N: recebem entradas e retornam programas.

Neste grupo temos os geradores de programas (baseados em definições de alto nível ou em dados externos, como um banco de dados), tradutores, compiladores (podem ser vistos como tradutores de linguagem de alto nível para baixo nível), metacompiladores(semelhante os compiladores, mas recebem a especificação da linguagem na qual o programa a ser compilado está escrito) ,weavers (recebem aspectos que devem ser introduzidos dentro de programas), entre outros.

4.   Static Metaprogramming In C++

Com o conceito de Metaprogramming já definido, a Static Metaprogramming pode ser encarada como a construção de metaprogramas que executam antes do load-time dos programas manipulados.

Mas independentemente disto, qual a relação entre Static Metaprogramming e Generative Programming? Segundo [CES97] os templates de metaprogramming de C++ implementam eficientemente os conceitos e características da Generative Programming.

Entendida esta relação e a extensão do conceito, deve-se analisar como é feita a implementação da Static Metaprogramming In C++. A implementação desta técnica é feita basicamente através do uso de preprocessadores e templates.

4.1.        Preprocessador

Preprocessador pode ser entendido como um programa, ou instruções de um programa, que processo um código fonte e eventualmente o altera, durante o processo de compilação.

A linguagem C++ possui alguns preprocessadores que são utilizados durante o processo de compilação, permitindo uso de instruções que visam a diminuição do overhead e aumentam a especialização dos programas.

A seguir é apresentado um pequeno programa que faz uso de preprocessadores. Definindo um preprocessor que é utilizado para verificar qual o código a ser inserido no programa compilado:

#include <iostream.h>

 

 

#define SOUnix

 

 

void main() {

  #ifdef SOUnix

    cout << "SO Unix" << endl;

  #else

    cout << "SO Not Unix - MS-Windows?" << endl;

  #endif

}

 

4.2.        Templates

Templates são um conjunto de estruturas e algoritmos associados que são executados durante a compilação, determinando o programa a ser produzido [DOWNEY99]. E enquanto um programa normal opera sobre dados (inteiros, strings, objetos, etc.), um template opera sobre tipos de dados e constantes numéricas.

Maiores detalhes sobre templates são melhor visualizados através dos exemplos apresentados a seguir.

Neste exemplo é feita a definição de um template para cálculo de fatorial. Fazendo com que o compilador vá criando estruturas para cada um dos fatoriais a serem feitos, até encontrar a definição do template especializado (fatorial de zero), finalizando neste momento o processo de recursividade utilizado para resolver o fatorial. E ao final no programa compilado o valor do fatorial já calculado (sem a estrutura do template), diminuindo o tamanho do código final e o tempo de processamento do programa, pois o valor do fatorial já foi calculado durante a compilação.

#include <iostream.h>

 

 

template<int N>

struct fatorial {

  enum { RET = N * fatorial<N – 1>::RET };

};

 

 

template<>

struct fatorial<0> {

  enum { RET = 1 };

};

 

 

void main() {

  cout << fatorial<7>::RET << endl;

}

 

 

Além de algoritmos de recursividade podem ser utilizadas outras estruturas de controle: repetitivas e condicionais, semelhantes as disponíveis na linguagem C++ (while, if-then-else, etc. ).

Aparentemente o exemplo anterior não é complexo, mas  a medida que é feito uso de estruturas para repetição ou aninhamento de templates a complexidade e dificuldade aumenta consideravelmente. Isto tanto na definição de templates como na utilização destes.

Neste outro exemplo é apresentada uma estrutura de repetição. Definindo um template para a estrutura de repetição e outro com a lógica a ser executada em cada ciclo de repetição. Ilustrando o aumento considerável da complexidade e dificuldade de utilização:

#include <iostream.h>

 

struct Parar {

  static void EXECUTAR() {};

  static void executar() {};

};

 

 

template <class Condicao, class Comando>

struct WHILE {

  static void EXECUTAR() {

    IF< ( Condicao::Teste<Comando>::avaliacao != 0 ) ,

           Comando, Parar

      >::RET::executar();

   

    IF< ( Condicao::Teste<Comando>::avaliacao != 0 ) ,

           WHILE<Condicao, Comando::proximo>, Parar

      >::RET::EXECUTAR();

  }

};

 

 

 

 

template<int i>

struct meuComando {

  enum { n = i };

  static void executar() { cout << i << endl; };

  typedef meuComando<n + 1> proximo;

};

 

 

struct minhaCondicao {

  template<class Comando>

  struct Teste {

    enum { avaliacao = ( Comando::n <= 10 ) };

  };

};

 

 

void main() {

  WHILE< minhaCondicao , meuComando<1> >::EXECUTAR();

}

 

Visto isoladamente o uso de metaprogramação e templates parece um tanto complexo e de grande dificuldade de utilização. Mas a partir do momento que este possa ser inserido dentro de uma arquitetura de desenvolvimento, pode-se tirar grandes proveitos de suas características.

Em [CE299] é apresentado uma arquitetura de desenvolvimento fazendo uso dos conceitos de Static Metaprogramming e do paradigma de Generative Programming, as características básicas desta arquitetura são apresentadas a seguir:

5.   Uma arquitetura unificadora

Foi apresentado por [CE299] uma arquitetura de desenvolvimento fazendo uso da técnica de Static Metaprogramming dentro do paradigma de Generative Programming. Esta arquitetura foco-se no desenvolvimento de um gerador de códigos, com templates, para uma linha de produção de carros.

A modelagem desta arquitetura, em linhas gerais, foi dividida em três etapas:

§         Análise do domínio do problema: visava a definição do escopo e características relacionadas aos domínio do problema. Buscando descobrir semelhanças e variabilidades entre os conceitos encontrados, identificando características obrigatórias e opcionais.

§         Análise do domínio da solução: definição da arquitetura dos componentes a serem utilizados, identificando como ocorriam as conexões de componentes, suas inter-dependências, valores padrão, entre outras informações. Após todas as informações eram inseridas dentro de configurações numa base de conhecimento.

§         Implementação dos componentes: utilização das técnicas de Static Metaprogramming. Criando-se numa primeira etapa os componentes básicos do domínio do problema, depois as configurações possíveis destes componentes e por último um gerador de componentes baseado nas configurações feitas anteriormente.

A seguir é exibido um modelo semelhante ao proposto por [CE299], que ilustra a utilização gerador de carros. Neste exemplo é feita a utilização de um template para a geração de carros, sendo passado como parâmetro outro template que define o modelo do carro desejado (juntamento com informações extra, como tipo de motor e tipo de roda a ser utilizada):

 

#include <iostream.h>

#include “ger_carro.h”

 

void main() {

  typedef GERADOR_CARRO<Esportivo<> >::Carro Modelo_A;

  typedef GERADOR_CARRO<Esportivo<esportiva> >::Carro Modelo_B;

  typedef GERADOR_CARRO<Basico<> >::Carro Modelo_C;

  typedef GERADOR_CARRO<Basico<alcool> >::Carro Modelo_D;

 

  Modelo_A meu_carro1;

  Modelo_B meu_carro2;

  Modelo_C meu_carro3;

  Modelo_D meu_carro4;

}

 

 

6.   Considerações finais

Embora a Static Metaprogramming In C++ traga alguns característica poderosas ao processo de desenvolvimento de software, alguns pontos devem ser levantandos com relação a sua utilização:

§         Falta de padronização dos compiladores.

Nos exemplos apresentados por [CE299] foram utilizados os compiladores a seguir para verificar a utilização da arquitetura, porém cada compilador tratou diferentemente os templates utilizados (gerando erros ou warnings diferentes): Microsoft Visual C++ 6.0, Intel C++ Compiler 5.0.1 e G++ 3.0.4.

§         Complexidade de implementação, mesmo tendo-se uma abstração do domínio do problema bem feita.

§         Necessidade de liberação do código fonte e falta de mecanismos para controle da propriedade intelectual dos templates.

Porém mesmo inserida dentro de uma arquitetura como a mencionada anteriormente, outros problemas são encontrados, dificultando ainda mais a utilização desta abordagem. [RIDEAU99] cita alguns destes outros problemas:

§         Necessidade de abstrações completas sobre o domínio do problema, o que raramente é alcançado pelas equipes de desenvolvimento de software.

§         Permissão de manipulação de código fonte por geradores totalmente permitida.

§         Falta de avaliação de considerações econômicas sobre a utilização de componentes fornecidos por terceiros já compilados ou, quando em fonte, com interfaces não padronizadas e não passíveis de manipulação por geradores.

 

 

 

Referências Bibliográficas

[CE99]

K. Czarnecki e U. Eisenecker. Generative Programming: Methods, Techniques, and Applications, Addison-Wesley, 1.999, pg. 243-269.

[CE299]

K. Czarnecki e U. Eisenecker.  Components and Generative Programming, Proceedings of the European Software Engineering Conference, 1.999, http://www.prakinf.tu-ilmenau.de/~czarn/esec99/.

[CEGV98]

K. Czarnecki, U. Eisenecker, R. Glueck e D. Vandevoorde. Generative Programming and Active Libraries, Proceedings of the Dagstuhl-Seminar on Generic Programming Conference, 1.998, http://osl.iu.edu/~tveldhui/papers/dagstuhl1998/dagstuhl.html.

[CES97]

K. Czarnecki, U. Eisenecker e P. Steyaert. Beyond Objects: Generative Programming, ECCOP’97, 1.997, http://citeseer.nj.nec.com/408302.htm.

[DOWNEY99]

S. Downey. Template Metaprogramming, Borland Community, 1.999, http://community.borland.com/article/print/0,1772,10526,00.html.

[RIDEAU99]

F. Rideau. Metaprogramming and Free Availability of Sources. Conference Autour du Libre, 1.999, http://fare.tunes.org/articles/ll99/index.en.html.