Typedefs internos em C ++ - bom ou ruim?

179

Algo que me vejo fazendo frequentemente ultimamente é declarar typedefs relevantes para uma classe específica dentro dessa classe, ou seja,

class Lorem
{
    typedef boost::shared_ptr<Lorem> ptr;
    typedef std::vector<Lorem::ptr>  vector;

//
// ...
//
};

Esses tipos são usados ​​em outras partes do código:

Lorem::vector lorems;
Lorem::ptr    lorem( new Lorem() );

lorems.push_back( lorem );

Razões pelas quais eu gosto:

  • Reduz o ruído introduzido pelos modelos de classe, std::vector<Lorem>torna-se Lorem::vector, etc.
  • Serve como uma declaração de intenções - no exemplo acima, a classe Lorem deve ser contada como referência boost::shared_ptre armazenada em um vetor.
  • Ele permite que a implementação mude - isto é, se o Lorem precisou ser alterado para ser contado intrusivamente como referência (via boost::intrusive_ptr) posteriormente, isso teria um impacto mínimo no código.
  • Eu acho que parece "mais bonito" e é sem dúvida mais fácil de ler.

Razões pelas quais eu não gosto:

  • Às vezes, há problemas com dependências - se você deseja incorporar, digamos, um Lorem::vectordentro de outra classe, mas só precisa (ou deseja) encaminhar declarar Lorem (em vez de introduzir uma dependência em seu arquivo de cabeçalho), então você acaba usando o tipos explícitos (por exemplo, em boost::shared_ptr<Lorem>vez de Lorem::ptr), o que é um pouco inconsistente.
  • Pode não ser muito comum e, portanto, mais difícil de entender?

Eu tento ser objetivo com o meu estilo de codificação, então seria bom ter outras opiniões sobre ele para poder dissecar um pouco o meu pensamento.

Will Baker
fonte

Respostas:

153

Eu acho que é um estilo excelente e eu mesmo o uso. É sempre melhor limitar o escopo dos nomes o máximo possível, e o uso de classes é a melhor maneira de fazer isso em C ++. Por exemplo, a biblioteca C ++ Standard faz uso pesado de typedefs nas classes.


fonte
Esse é um bom ponto, imagino que parecer "mais bonito" seja meu subconsciente apontando delicadamente que o alcance limitado é uma coisa boa . Eu me pergunto, porém, o fato de o STL usá-lo predominantemente nos modelos de classe o torna um uso sutilmente diferente? É mais difícil justificar em uma classe 'concreta'?
Will Baker
1
Bem, a biblioteca padrão é composta de modelos em vez de classes, mas acho que a justificativa é a mesma para ambos.
14

Serve como uma declaração de intenções - no exemplo acima, a classe Lorem deve ser contada como referência através do boost :: shared_ptr e armazenada em um vetor.

É exatamente isso que não faz.

Se eu vir 'Foo :: Ptr' no código, não faço a menor idéia se é um shared_ptr ou um Foo * (a STL tem :: pointer typedefs que são T *, lembre-se) ou o que for. Esp. se for um ponteiro compartilhado, não forneço um typedef, mas mantenho o shared_ptr explicitamente no código.

Na verdade, eu quase nunca uso typedefs fora da metaprogramação de modelos.

O STL faz esse tipo de coisa o tempo todo

O design do STL com conceitos definidos em termos de funções-membro e typedefs aninhados é um beco sem saída histórico, as bibliotecas de modelos modernas usam funções gratuitas e classes de características (cf. Boost.Graph), porque elas não excluem tipos internos de modelar o conceito e porque facilita a adaptação de tipos que não foram projetados com os conceitos das bibliotecas de modelos fornecidas.

Não use o STL como motivo para cometer os mesmos erros.

Marc Mutz - mmutz
fonte
Concordo com a sua primeira parte, mas sua edição recente é um pouco míope. Esses tipos aninhados simplificam a definição de classes de características, pois fornecem um padrão sensato. Considere a nova std::allocator_traits<Alloc>classe ... você não precisa se especializar para cada alocador que escreve, porque simplesmente empresta os tipos diretamente Alloc.
Dennis Zickefoose
@Dennis: No C ++, a conveniência deve estar do lado de / user / de uma biblioteca, não do lado de / author /: o usuário deseja uma interface uniforme para uma característica, e somente uma classe de características pode fornecer isso, por causa dos motivos mencionados acima). Mas, mesmo como Allocautor, não é exatamente mais difícil se especializar std::allocator_traits<>para seu novo tipo do que adicionar os typedefs necessários. Também editei a resposta, porque minha resposta completa não cabia em um comentário.
Marc Mutz - mmutz
Mas está do lado do usuário. Como usuário de allocator_traitstentar criar um alocador personalizado, não preciso me preocupar com os quinze membros da classe de características ... tudo o que preciso fazer é dizer typedef Blah value_type;e fornecer as funções de membro apropriadas, e o padrão allocator_traitsdescobrirá o descansar. Além disso, veja o seu exemplo de Boost.Graph. Sim, ele faz uso pesado da classe de características ... mas a implementação padrão de graph_traits<G>simplesmente consulta Gseus próprios typedefs internos.
Dennis Zickefoose
1
E até a biblioteca padrão 03 utiliza classes de características, quando apropriado ... a filosofia da biblioteca não é operar genericamente em contêineres, mas operar em iteradores. Portanto, ele fornece uma iterator_traitsclasse para que seus algoritmos genéricos possam consultar facilmente as informações apropriadas. O que, novamente, assume como padrão consultar o iterador para obter suas próprias informações. O longo e curto disso é que os traços e os typedefs internos dificilmente são mutuamente exclusivos ... eles se apoiam.
Dennis Zickefoose
1
@ Dennis: iterator_traitstornou - se necessário porque T*deveria ser um modelo de RandomAccessIterator, mas você não pode colocar os typedefs necessários T*. Uma vez que tivéssemos iterator_traits, os typedefs aninhados se tornaram redundantes, e eu gostaria que eles fossem removidos de vez em quando. Pelo mesmo motivo (impossibilidade de adicionar typedefs internos), T[N]não modela o Sequenceconceito STL e você precisa de kludges como std::array<T,N>. O Boost.Range mostra como um conceito moderno de Sequência pode ser definido T[N]para modelar, porque não requer typedefs aninhados nem funções-membro.
Marc Mutz - mmutz
8

Typdefs são definitivamente são de bom estilo. E todas as suas "razões de que gosto" são boas e corretas.

Sobre os problemas que você tem com isso. Bem, a declaração direta não é um santo graal. Você pode simplesmente criar seu código para evitar dependências de vários níveis.

Você pode mover typedef para fora da classe, mas Class :: ptr é muito mais bonito que ClassPtr que eu não faço isso. É como nos namespaces quanto a mim - as coisas permanecem conectadas dentro do escopo.

As vezes eu fiz

Trait<Loren>::ptr
Trait<Loren>::collection
Trait<Loren>::map

E pode ser o padrão para todas as classes de domínio e com alguma especialização para determinadas.

Mykola Golubyev
fonte
3

O STL faz esse tipo de coisa o tempo todo - os typedefs fazem parte da interface para muitas classes no STL.

reference
iterator
size_type
value_type
etc...

são todos os typedefs que fazem parte da interface para várias classes de modelos STL.

Michael Burr
fonte
É verdade, e eu suspeito que foi aqui que eu o peguei. Parece que isso seria um pouco mais fácil de justificar? Não consigo deixar de ver os typedefs dentro de um modelo de classe como mais parecidos com variáveis, se você pensar na linha de 'metaprogramação'.
Will Baker
3

Outro voto para esta ser uma boa ideia. Comecei a fazer isso ao escrever uma simulação que precisava ser eficiente, no tempo e no espaço. Todos os tipos de valores tinham um tytref Ptr iniciado como um ponteiro compartilhado de aumento. Fiz alguns perfis e mudei alguns deles para um ponteiro intrusivo de aumento sem precisar alterar nenhum código em que esses objetos foram usados.

Observe que isso só funciona quando você sabe onde as classes serão usadas e que todos os usos têm os mesmos requisitos. Eu não usaria isso no código da biblioteca, por exemplo, porque você não pode saber ao escrever a biblioteca o contexto em que ela será usada.

KeithB
fonte
3

Atualmente, estou trabalhando no código, que usa intensivamente esse tipo de typedefs. Até agora tudo bem.

Mas notei que muitas vezes existem typedefs iterativos, as definições são divididas entre várias classes e você nunca sabe realmente com que tipo está lidando. Minha tarefa é resumir o tamanho de algumas estruturas de dados complexas ocultas por trás desses typedefs - portanto, não posso confiar nas interfaces existentes. Em combinação com três a seis níveis de namespaces aninhados, torna-se confuso.

Portanto, antes de usá-los, há alguns pontos a serem considerados

  • Alguém mais precisa desses typedefs? A classe é muito usada por outras classes?
  • Encurto o uso ou oculto a classe? (No caso de se esconder, você também pode pensar em interfaces.)
  • Outras pessoas estão trabalhando com o código? Como eles fazem isso? Será que eles acham que é mais fácil ou ficarão confusos?
Bodo
fonte
1

Eu recomendo mover esses typedefs para fora da classe. Dessa forma, você remove a dependência direta de ponteiros compartilhados e classes de vetores e pode incluí-los somente quando necessário. A menos que você esteja usando esses tipos em sua implementação de classe, considero que eles não devem ser typedefs internos.

As razões pelas quais você gosta ainda são correspondidas, pois são resolvidas pelo tipo de aliasing através de typedef, não declarando-as dentro da sua classe.

Cătălin Pitiș
fonte
Isso poluiria o espaço de nome anônimo com os typedefs, não? O problema com o typedef é que ele oculta o tipo real, que pode causar conflitos quando incluído em / por vários módulos, difíceis de encontrar / corrigir. É uma boa prática contê-los em namespaces ou dentro de classes.
Indy9000 17/04/09
3
Conflitos de nomes e poluição anônima do espaço para nome têm pouco a ver com manter um nome de tipo dentro de uma classe ou fora. Você pode ter um conflito de nome com sua classe, não com seus typedefs. Portanto, para evitar a poluição do nome, use espaços para nome. Declare sua classe e os typedefs relacionados em um espaço para nome.
Cătălin Pitis
Outro argumento para colocar o typedef dentro de uma classe é o uso de funções com modelos. Quando, digamos, uma função recebe um tipo de contêiner desconhecido (vetor ou lista) que contém um tipo de string desconhecido (string ou sua própria variante compatível com string). a única maneira de descobrir o tipo de carga útil do contêiner é com o typedef 'value_type', que faz parte da definição da classe de contêiner.
Marius
1

Quando o typedef é usado apenas dentro da própria classe (isto é, é declarado como privado), acho uma boa ideia. No entanto, exatamente pelas razões que você deu, eu não o usaria se os typedef precisassem ser conhecidos fora da classe. Nesse caso, recomendo movê-los para fora da classe.

Stefan Rådström
fonte