C++0x, enumerações com escopo definido

Introdução e padrões de bom uso para enumerações com escopo definido

Este artigo apresenta o novo recurso de enumerações com escopo definido do novo padrão de C++, C++0x. O objetivo das enumerações com escopo definido é permitir a criação de código com maior portabilidade e expressão. Por exemplo, essas enumerações podem ter um tipo explícito subjacente que melhora a portabilidade, podem ter declaração de encaminhamento e não passam por conversões implícitas. Isso aumenta a segurança de tipo. O comportamento das enumerações anteriores ainda existe, sendo chamado agora de enumeração sem escopo. As enumerações sem escopo podem herdar parte da funcionalidade das enumerações com escopo definido, como tipo explícito subjacente e declaração de encaminhamento. Este artigo descreve todos os aspectos das enumerações com escopo definido e dá exemplos e padrões de bom uso que podem ser úteis para você.

Wael Y. Yehia, Software Developer, IBM

Wael Yehia é desenvolvedor de software no frontend do compilador XL C++.



08/Out/2012

Introdução

Enumerações, geralmente chamadas de enums, são construções em uma linguagem de programação que permitem aos usuários agrupar um conjunto de valores e atribuir um nome a cada valor. Às vezes queremos representar vários estados ou valores que são estáticos e constantes. Nesse caso, é melhor enumerar esses estados, atribuir-lhes alguns valores integrais para que possam ser comparados e estabelecer relacionamentos entre os valores se necessário, como a = 1 e b = a+2.

É isso que as enums do C permitem fazer: são uma maneira melhor e mais rápida de criar um conjunto de valores #define que não são visíveis globalmente no escopo do arquivo. Mas ainda é possível melhorar essa representação de enumerações, e você verá nas seções adiante como as enums de C++ e as enums com escopo definido fazem isso. Mas antes iremos revisar a história das enums.


História das enums no C e C++

Tudo começou (como a maioria das coisas no C++) em C, como enums de C. E antes de surgirem enums de C, a enumeração de um conjunto de valores numéricos era feita com diretivas #define simples. A Figura 1 mostra a linha do tempo das enumerações em C/C++ e um exemplo da enumeração de quatro valores (superior esquerdo, superior direito, inferior esquerdo, inferior direito).

Figura 1. Linha do tempo das enumerações de C
Linha do tempo das enumerações de C

Observe que incluídos a enum direction, mas mudamos os nomes de seus enumeradores para serem diferentes dos da enum windowCorner, para evitar conflito de nomes. (Os enumeradores são injetados no escopo fechado. Assim, TOP_LEFT e TOPLEFT estão no mesmo escopo e, portanto, não podem ter o mesmo nome.)


Enums em C e C++

Uma enumeração em C/C++ é como um struct com números integrais constantes estáticos (Figura 2), mas os membros são injetados no escopo que contém struct. Em C++, não é possível inicializar ou designar para uma variável enum um valor que não seja de um enumerador ou variável do mesmo tipo de enumeração. Mas isso é permitido em C, o que torna as enums dessa linguagem menos seguras em relação ao tipo do que as enums de C++.

Figura 2. Struct com membros de dados estáticos constantes simulando enums
Struct com membros de dados estáticos constantes simulando enums

É melhor não deixar que membros de um struct sejam comparados a membros de outro struct. Isso porque, quando enumeramos um conjunto de valores, estamos criando um tipo exclusivo, com membros que podem ser comparados uns aos outros mas não a valores externos, independente da semelhança entre suas representações. É claro que, em muitas situações, o importante é o valor integral, mas nunca deveria fazer sentido comparar dois enumeradores de enumerações diferentes. Por exemplo, uma enum pode representar cores e a outra, dias da semana. Embora ambas tenham um valor integral que permita uma comparação matemática, a comparação não seria válida semanticamente. É melhor que as enums tenham essa propriedade.

O mesmo argumento vale para usar variáveis ou constantes de enum em um contexto em que se espera um enum diferente ou um tipo integral: isso não faz sentido. Por exemplo, uma função foo que toma um int não deve ser chamada com um argumento do tipo enum. Pode ser útil permitir isso em alguns casos, mas, em geral, uma enum deve conter uma certa semântica, e não apenas ser convertida em números inteiros simples.

Infelizmente, nenhum desses mecanismos de enumeração oferece essas duas garantias. É óbvio que, com #defines, estamos lidando com os números inteiros diretamente e não há outro tipo a não ser ints, portanto, não há restrições. Em enums de C e C++, é permitido comparar, por exemplo, TOP_LEFT com TOPRIGHT (referência à Figura 3), pois os dois enumeradores são convertidos para o tipo inteiro e comparados como números inteiros. Também é permitido usar uma enum em qualquer contexto de inteiro, ou seja, em qualquer lugar em que se espera um tipo de número inteiro (veja a Figura 3).

Figura 3. Código de amostra para uso das enums de C/C++ sem segurança de tipo
Código de amostra para uso das enums de C/C++ sem segurança de tipo

Observe que tivemos que alterar o nome dos enumeradores de enum direction, para que sejam diferentes dos enumeradores de enum windowCorner. O motivo disso é que todos os enumeradores pertencem ao escopo que contém a enumeração. Ou seja, se a enum foi definida dentro de uma classe, os enumeradores tornam-se membros da classe; se foi definida no escopo global, os enumeradores adquirem visibilidade global. Isso é inconveniente, especialmente se as enums são declaradas no namespace ou escopo global, onde pode haver muitas delas.

Há mais um problema com nossas enums: tamanho e sinal. Isso é consistente entre diferentes compiladores? Não, não é. De acordo com o padrão de C++, o tipo subjacente de uma enum é semidefinido, pois o compilador pode escolher qual tipo integral usar para representar as enums. De modo que o tipo é menor que int se todos os valores da enum puderem ser representados com int. Do contrário, pode ser outro tipo maior.

A falta de um tipo subjacente bem-definido não permite fazer declaração de encaminhamento de uma enum. O que é estranho, pois structs e uniões podem ser declaradas dessa forma sem saber nada sobre elas. O problema está na maneira como as enums são manipuladas e passadas.

A declaração de encaminhamento das enums ajuda a separar mais a interface da implementação (como todas as outras formas de declaração de encaminhamento). Outros benefícios da declaração de encaminhamento é o desacoplamento de unidades acopladas por definição de enum, que reduz o tempo total de compilação, e ocultação dos detalhes da implementação para o usuário. Colocar a definição da enum em cada unidade de compilação significa que toda a enumeração é visível, quando ela não deveria ser. Mas seria útil se possível.

Vamos analisar o exemplo de código da Figura 4. O programa consiste em três arquivos:

  • interface.h
  • implementation.C
  • usage.C

Há duas versões da implementação e interface:

  • A Versão 1 é como o código ficará em C++03
  • A Versão 2, que é como gostaríamos que ficasse, é semelhante ao resultado com C++0x.

Na Versão 1, a definição de enum E é parte da interface diretamente (ou indiretamente, se quisermos usar #includes), e não há como escapar disso. Isso significa que, se a definição de enum E mudar, usage.C precisa ser recompilado. Mas não deveria, pois ele não depende da definição de enum E. Se pudéssemos escrever código semelhante à Versão 2, a definição de E seria independente de interface.h e, portanto, de usage.C. Além de desacoplar usage.C e interface.h de implementation.C, também ocultamos do usuário a definição de enum E, um recurso que os desenvolvedores de bibliotecas querem muito.

Figura 4. Desacoplando a interface da implementação com o uso de declaração de encaminhamento
Desacoplando a interface da implementação com o uso de declaração de encaminhamento

Recursos das enums com escopo definido

As enums com escopo definido resolvem muitas das limitações das enums regulares: segurança de tipo completa, tipo subjacente bem definido, problemas de escopo e declaração de encaminhamento. A sintaxe das enums com escopo definido é semelhante à das enums regulares (agora chamadas de enums sem escopo), mas é preciso especificar a classe ou palavra-chave de struct antes da palavra-chave de enum. Também é possível especificar o tipo subjacente usando dois pontos seguido de um tipo integral.

Observação:
Enums não são tipos integrais, portanto, não é possível especificar uma enum como o tipo subjacente de outra.

A Figura 5 mostra exemplos de enums.

Figura 5. Enums com escopo definido e sem escopo
Enums com escopo definido e sem escopo

Para obter segurança de tipo, proibimos todas as conversões explícitas de enums com escopo definido para outros tipos. Isso restringe qualquer tipo de operação não aritmética (designação, comparação) em enums apenas ao conjunto de enumeradores e variáveis da mesma enumeração e proíbe operações aritméticas. Ainda é possível usar enums com escopo definido em locais como instruções de comutação, mas estaríamos limitados a manter um tipo de enum uniforme na condição de comutação e rótulos case. Veja o exemplo de código na Figura 6 para entender melhor.

Outro recurso das enums com escopo definido é a introdução de um novo escopo, chamado escopo de enumeração, que basicamente começa e termina com os colchetes de abertura e fechamento do corpo da enum. Portanto, os enumeradores com escopo definido não são mais injetados no escopo adjacente, o que evita os conflitos de nomes das enums regulares. Mas isso traz uma pequena desvantagem: é necessário sempre referir-se a um enumerador com escopo definido pelo seu nome qualificado na enumeração. Por exemplo:
a (antes)
E::a (agora)

A Figura 6 mostra as regras de definição de escopo.

A seguir está o tipo subjacente. As enums com escopo definido permitem especificar o seu tipo subjacente, cujo valor padrão é int caso o tipo não seja especificado. Uma enum sem escopo com o tipo subjacente omitido comporta-se como enums regulares de C++03, com o tipo subjacente sendo definido pela implementação. Quando o tipo subjacente é especificado explícita ou implicitamente (apenas para enum com escopo definido), é chamado de fixo; do contrário, é não fixo. Isso significa que a sintaxe de uma enum regular não tem um tipo subjacente fixo (veja a Figura 5).

Figura 6. Conversão e definição de escopo de enums com escopo definido
Conversão e definição de escopo de enums com escopo definido

Por fim, podemos lidar com o último problema, que se mostra logo que o problema do tipo subjacente é resolvido: declaração de encaminhamento de enums. Basicamente, qualquer enum com um tipo subjacente fixo pode ter declaração de encaminhamento. Como mencionado acima, a declaração de encaminhamento tem muitos benefícios, como desacoplamento do código e ocultação da implementação de uma enum quando ela não é parte do espaço de problema de um usuário. Para fazer declaração de encaminhamento de uma enum, basta declará-la sem a seção de corpo, de modo que o tipo subjacente seja fixo (a Figura 7 ilustra as regras). É possível redeclarar várias vezes, mas cada declaração deve ser consistente com as anteriores. Elas devem ter os mesmos tipos subjacentes e ser do mesmo tipo (com escopo definido ou sem escopo).

Figura 7. Regras de declaração de encaminhamento para enums sem escopo
Regras de declaração de encaminhamento para enums sem escopo

Bons padrões de uso

Enums são usadas geralmente para representar estados, tipos, condições e qualquer coisa que seja um conjunto de membros sem uma funcionalidade em particular, exceto o fato ser uma coleção exclusiva de elementos. Enums regulares oferecem um pouco de segurança de tipo, especificamente durante a designação, mas tudo vai por água abaixo quando se tenta comparar ou usar uma enum em um contexto integral. Há muitos padrões para uso de enums, portanto, este artigo discute algumas que existem com a versão regular e outras que podem ser usadas apenas com a versão com escopo definido.

Herança de classe, a enum kind

Suponha que tenhamos uma classe-pai chamada Widget e várias classes-filhas, como Button, Label etc. Uma maneira comum de identificar um objeto para o qual aponta o ponteiro Widget* é criar uma enumeração -- chame de enum kind -- na classe-pai e ter um enumerador para cada tipo de filha. Em seguida, incluímos uma variável de membro enum kind no pai, para que cada filha configure esse membro para ser seu enumerador designado (veja a Figura 8).

Figura 8. Usando enums para identificar objetos de tipos derivados
Usando enums para identificar objetos de tipos derivados

Dessa forma, basta olhar o membro type e identificar o tipo real do objeto, com base no valor da enum. Isso funciona até que tenhamos um conjunto de classes semelhantes que herdam umas das outras e usam o mesmo mecanismo de enum para conter o tipo das filhas. Mas estamos usando ambos os tipos de enum no mesmo código. Com exceção da designação, nenhuma das operações que envolvem conversões implícitas têm segurança de tipo. Com enums com escopo definido seria diferente, pois somos obrigados a comparar e designar no mesmo conjunto de enumeradores e não podemos usar enums em um contexto de número inteiro sem conversões explícitas. Quando tentamos, como ao comparar duas enums de enumerações diferentes ou usar enums quando uma conversão implícita para outro tipo é necessária, obtemos erros de tempo de compilação. Com enums regulares, esses erros lógicos passam despercebidos.

Bool com segurança de tipo

O tipo booleano (bool) em C++ não tem segurança de tipo, pois pode ser convertido para qualquer outro tipo integral e vice-versa. (Na verdade, não tem segurança de tipo porque ele é um tipo integral.) Às vezes, é necessário um bool com segurança de tipo para representar, por exemplo, condições críticas que permitem apenas manipulação explícita, de modo que não possam ser inicializados, designados ou comparados com outro valor de um tipo diferente. É possível fazer isso com uma classe. Por exemplo, em "A Typesafe Boolean Class" (consulte Recursos), o autor propõe um tipo bool cujos parâmetros de conversão (ou seja, o que pode converter-se em bool e vice-versa) podem ser controlados. Isso pode ser flexível, mas é tedioso de manter, especialmente para iniciantes.

Com uma enum com escopo definida, é possível criar um tipo de condição booleana com segurança de tipo, da maneira que a Figura 9 mostra.

Observação:
Esse caso de uso foi sugerido por Chris Bowler, desenvolvedor do compilador frontend XL C++, IBM Canadá.

Figura 9. Bool com segurança de tipo
Bool com segurança de tipo

Seu uso é muito mais seguro em relação ao tipo booleano C++ integrado. Observe os dois exemplos nas Figuras 10 e 11. Na Figura 10, nós representamos as três condições com três enums com escopo definido. Na Figura 11, usamos três variáveis booleanas para as condições. A função initiate() faz alguns cálculos, talvez alterando os valores de x, y e z, e depois os passa para handle() duas vezes. A primeira chamada extravia os dois últimos argumentos, e a segunda chamada omite o argumento int e usa o argumento padrão para o quarto parâmetro.

Dado que bool pode ser convertido em int e vice-versa, o exemplo na Figura 10 passa despercebido, pois o compilador encontra as conversões implícitas e promoções a integral necessárias para alterar os parâmetros de chamada da função para o tipo correto: y é convertido para int e 3 é convertido para bool. No entanto, o exemplo na Figura 11 resulta em um erro do tempo de compilação, pois uma enum com escopo definido não pode ser convertida em inte um int não pode ser usado para inicializar uma enum com escopo definido.

Figura 10. Tipo bool como tipo de condição
Tipo bool como tipo de condição
Figura 11. Enum com escopo definido como tipo de condição
Enum com escopo definido como tipo de condição

Pode-se argumentar que é possível melhorar a versão de bool usando enums sem escopo (a versão antiga de C++03), mas ainda não seria detectada a conversão implícita de y para int. O uso das enums sem escopo tem outra desvantagem relacionada ao problema do escopo. Observe como os enumeradores verdadeiro e falso de cada condição são chamados apenas de True/False, embora sejam todos definidos no mesmo escopo. Isso não é possível com enums sem escopo, pois elas são injetadas em seu escopo adjacente. Nesse caso, haveria conflito de nomes se dois enumeradores injetados tivessem o mesmo nome.

Outra coisa que vale a pena mencionar é a clareza da interface das funções. Na Figura 11, é possível saber o que cada parâmetro indica sem usar nomes de variável expressivos. E indo mais além, se a declaração das funções estivesse em um arquivo .h separado, (a) seria necessário ir até o arquivo para entender o que bool x e bool y significam, e (b) alguns nomes de variáveis podem estar ausentes na declaração. Mas, com enums com escopo definido, a descrição de cada parâmetro está integrada no nome da enum, o que força o desenvolvedor a mencioná-la na declaração e definição. É claro que o desenvolvedor pode não dar nomes apropriados para o enum com escopo, mas ele estaria apenas prejudicando a si mesmo. Seria como ter um programa com classes com os nomes A1, A2 e A3.

Representação de estado com segurança de tipo

Estados aparecem muito em programas C++. Sempre em situações nas quais há um conjunto de entidades que precisamos representar ou enumerar, usamos enums. Um padrão de programação comum é passar informações contextuais através de uma hierarquia de chamadas de função. Digamos que haja um conjunto de funções em uma parte do espaço de problema, e há informações dinâmicas que devem ser mantidas ao longo da execução das funções. Uma solução é fazer um objeto de acesso global que aja como um banco de dados, mantendo as informações dinâmicas. Mas queremos evitar variáveis globais, pois elas incluem acoplamento no código. Outra maneira seria criar uma classe Context que contém as informações dinâmicas, e passar uma referência ou ponteiro a um objeto Context nas chamadas de função. Ainda outra maneira é passar cada informação explicitamente nas chamadas de função. Isso seria melhor que criar um objeto Context se as informações passadas forem pequenas. Frequentemente, o contexto dinâmico sendo passado tem tipo boolean ou enumerado. Essa é uma oportunidade perfeita para usar nosso bool com segurança de tipo e enum com segurança de tipo.

A Figura 12 mostra como enums com escopo podem ser usados para criar um programa mais seguro que tem mais controle sobre o processo de execução através de um domínio maior (detecção do tempo de compilação) das condições que definem o caminho de execução. As informações contextuais nesse cenário são o gatilho. Por simplicidade, temos duas ações, mas o tamanho das informações pode ser maior em cenários reais.

Assim como nos exemplos anteriores, ao usar enums para as condições, estamos garantindo que a declaração e a definição das funções indicam o que cada parâmetro significa. Na chamada de cada função, os argumentos das funções indicam claramente qual o valor da condição. O extravio de parâmetros ou uso de tipos incompatíveis são erros percebidos no tempo de compilação, em vez de nunca serem percebidos, como seria se bools fossem usados. As informações de contexto podem ser processadas e manipuladas com segurança e de forma consistente, de modo que não haja comparações inseguras ou conversões implícitas silenciosas.

Figura 12. Passagem de contexto
Passagem de contexto

Por fim, esse código tem maior portabilidade, e as enums podem ter declaração de encaminhamento, pois o tipo subjacente de cada enumeração é fixo. A eficiência das enums de escopo definido é claramente demonstrada pelo código mais limpo, condições de segurança de tipo, enums com segurança de tipo e código com portabilidade.

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Rational
ArticleID=839281
ArticleTitle=C++0x, enumerações com escopo definido
publish-date=10082012