Tipos de letra internos en C ++: ¿buen o mal estilo?

179

Algo que me he encontrado haciendo a menudo últimamente es declarar typedefs relevantes para una clase particular dentro de esa clase, es decir

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

//
// ...
//
};

Estos tipos se usan luego en otra parte del código:

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

lorems.push_back( lorem );

Razones por las que me gusta:

  • Reduce el ruido introducido por las plantillas de clase, se std::vector<Lorem>convierte Lorem::vector, etc.
  • Sirve como una declaración de intenciones: en el ejemplo anterior, la clase Lorem está destinada a ser contada por referencia boost::shared_ptry almacenada en un vector.
  • Permite que la implementación cambie, es decir, si Lorem necesita ser cambiado para contar de manera intrusiva (vía boost::intrusive_ptr) en una etapa posterior, esto tendría un impacto mínimo en el código.
  • Creo que se ve más bonito y podría decirse que es más fácil de leer.

Razones por las que no me gusta:

  • A veces hay problemas con las dependencias: si desea incrustar, por ejemplo, un Lorem::vectordentro de otra clase pero solo necesita (o desea) declarar Lorem (en lugar de introducir una dependencia en su archivo de encabezado), entonces terminará teniendo que usar el tipos explícitos (por ejemplo, en boost::shared_ptr<Lorem>lugar de Lorem::ptr), lo cual es un poco inconsistente.
  • Puede que no sea muy común y, por lo tanto, más difícil de entender.

Trato de ser objetivo con mi estilo de codificación, por lo que sería bueno obtener algunas otras opiniones al respecto para poder analizar un poco mi pensamiento.

Will Baker
fuente

Respuestas:

153

Creo que es un estilo excelente, y lo uso yo mismo. Siempre es mejor limitar el alcance de los nombres tanto como sea posible, y el uso de clases es la mejor manera de hacerlo en C ++. Por ejemplo, la biblioteca C ++ Standard hace un uso intensivo de typedefs dentro de las clases


fuente
Ese es un buen punto, me pregunto si mi aspecto más bonito era mi subconsciente, señalando delicadamente que el alcance limitado es algo bueno . Sin embargo, me pregunto si el hecho de que el STL lo use predominantemente en plantillas de clase lo convierte en un uso sutilmente diferente. ¿Es más difícil justificar en una clase 'concreta'?
Will Baker
1
Bueno, la biblioteca estándar está compuesta de plantillas en lugar de clases, pero creo que la justificación es la misma para ambos.
14

Sirve como una declaración de intenciones: en el ejemplo anterior, la clase Lorem está destinada a ser contada por referencia a través de boost :: shared_ptr y almacenada en un vector.

Esto es exactamente lo que no hace.

Si veo 'Foo :: Ptr' en el código, no tengo ni idea de si es un shared_ptr o un Foo * (STL tiene :: punteros typedefs que son T *, recuerde) o lo que sea. Esp. si es un puntero compartido, no proporciono un typedef en absoluto, pero mantengo el uso shared_ptr explícitamente en el código.

En realidad, casi nunca uso typedefs fuera de Template Metaprogramming.

El STL hace este tipo de cosas todo el tiempo

El diseño STL con conceptos definidos en términos de funciones miembro y typedefs anidados es un callejón sin salida histórico, las bibliotecas de plantillas modernas usan funciones libres y clases de rasgos (véase Boost.Graph), porque estos no excluyen los tipos incorporados de modelando el concepto y porque facilita la adaptación de tipos que no fueron diseñados teniendo en cuenta los conceptos de las bibliotecas de plantillas dadas.

No use el STL como una razón para cometer los mismos errores.

Marc Mutz - mmutz
fuente
Estoy de acuerdo con su primera parte, pero su edición reciente es un poco miope. Estos tipos anidados simplifican la definición de clases de rasgos, ya que proporcionan un valor predeterminado razonable. Considere la nueva std::allocator_traits<Alloc>clase ... no tiene que especializarse para cada asignador que escriba, porque simplemente toma prestados los tipos directamente Alloc.
Dennis Zickefoose
@Dennis: en C ++, la conveniencia debe estar del lado de / user / de una biblioteca, no del lado de su / author /: el usuario desea una interfaz uniforme para un rasgo, y solo una clase de rasgo puede dar eso, por las razones expuestas anteriormente). Pero incluso como Allocautor, no es exactamente más difícil especializarse std::allocator_traits<>para su nuevo tipo que agregar los tipos de letra necesarios. También he editado la respuesta, porque mi respuesta completa no cabía en un comentario.
Marc Mutz - mmutz
Pero está del lado del usuario. Como usuario de allocator_traitsintentar crear un asignador personalizado, no tengo que molestarme con los quince miembros de la clase de rasgos ... todo lo que tengo que hacer es decir typedef Blah value_type;y proporcionar las funciones de miembro apropiadas, y lo predeterminado allocator_traitsdeterminará el descanso. Además, mira tu ejemplo de Boost.Graph. Sí, hace un uso intensivo de la clase de rasgos ... pero la implementación predeterminada de graph_traits<G>simplemente consultas Gpara sus propios typedefs internos.
Dennis Zickefoose
1
E incluso la biblioteca estándar 03 hace uso de clases de rasgos cuando corresponde ... la filosofía de la biblioteca no es operar en contenedores genéricamente, sino operar en iteradores. Por lo tanto, proporciona una iterator_traitsclase para que sus algoritmos genéricos puedan consultar fácilmente la información adecuada. Lo cual, de nuevo, por defecto consulta al iterador por su propia información. En resumidas cuentas, los rasgos y las definiciones internas no son mutuamente excluyentes ... se apoyan mutuamente.
Dennis Zickefoose
1
@Dennis: se iterator_traitshizo necesario porque T*debería ser un modelo de RandomAccessIterator, pero no puede poner los typedefs requeridos T*. Una vez que lo hicimos iterator_traits, los typedefs anidados se volvieron redundantes, y desearía que hubieran sido eliminados allí y entonces. Por la misma razón (imposibilidad de agregar typedefs internos), T[N]no modela el Sequenceconcepto STL , y necesita kludges como std::array<T,N>. Boost.Range muestra cómo se puede definir un concepto de secuencia moderno que T[N]pueda modelar, ya que no requiere definiciones de tipo anidadas ni funciones miembro.
Marc Mutz - mmutz
8

Typdefs definitivamente son buen estilo. Y todas sus "razones que me gustan" son buenas y correctas.

Sobre los problemas que tienes con eso. Bueno, la declaración adelantada no es un santo grial. Simplemente puede diseñar su código para evitar dependencias de varios niveles.

Puede mover typedef fuera de la clase, pero Class :: ptr es mucho más bonita que ClassPtr que no hago esto. Es como con los espacios de nombres en cuanto a mí: las cosas permanecen conectadas dentro del alcance.

A veces lo hice

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

Y puede ser predeterminado para todas las clases de dominio y con cierta especialización para ciertas.

Mykola Golubyev
fuente
3

El STL hace este tipo de cosas todo el tiempo: los typedefs son parte de la interfaz para muchas clases en el STL.

reference
iterator
size_type
value_type
etc...

son todos los typedefs que forman parte de la interfaz para varias clases de plantillas STL.

Michael Burr
fuente
Es cierto, y sospecho que aquí es donde lo recogí por primera vez. ¿Parece que estos serían un poco más fáciles de justificar? No puedo evitar ver los typedefs dentro de una plantilla de clase como más parecidos a las variables, si se piensa en la línea de 'metaprogramación'.
Will Baker
3

Otra votación para que esta sea una buena idea. Comencé a hacer esto cuando escribía una simulación que tenía que ser eficiente, tanto en tiempo como en espacio. Todos los tipos de valor tenían una definición de tipo Ptr que comenzó como un puntero compartido de impulso. Luego hice algunos perfiles y cambié algunos de ellos a un puntero intrusivo de impulso sin tener que cambiar ninguno de los códigos donde se usaron estos objetos.

Tenga en cuenta que esto solo funciona cuando sabe dónde se utilizarán las clases y que todos los usos tienen los mismos requisitos. No usaría esto en el código de la biblioteca, por ejemplo, porque no puede saber al escribir la biblioteca el contexto en el que se usará.

KeithB
fuente
3

Actualmente estoy trabajando en código, que usa intensivamente este tipo de typedefs. Hasta ahora eso está bien.

Pero me di cuenta de que a menudo hay tipos de letra iterativos, las definiciones se dividen entre varias clases, y nunca se sabe realmente con qué tipo se trata. Mi tarea es resumir el tamaño de algunas estructuras de datos complejas ocultas detrás de estos typedefs, por lo que no puedo confiar en las interfaces existentes. En combinación con tres a seis niveles de espacios de nombres anidados y luego se vuelve confuso.

Entonces, antes de usarlos, hay algunos puntos a considerar

  • ¿Alguien más necesita estos typedefs? ¿La clase se usa mucho por otras clases?
  • ¿Acorto el uso u oculto la clase? (En caso de ocultarse, también podría pensar en interfaces).
  • ¿Hay otras personas trabajando con el código? ¿Cómo lo hicieron? ¿Pensarán que es más fácil o se confundirán?
Bodo
fuente
1

Recomiendo mover esos typedefs fuera de la clase. De esta manera, elimina la dependencia directa en puntero compartido y clases de vectores y puede incluirlos solo cuando sea necesario. A menos que esté utilizando esos tipos en la implementación de su clase, considero que no deberían ser typedefs internos.

Las razones por las que te gusta aún coinciden, ya que se resuelven mediante el alias de tipo a través de typedef, no declarándolos dentro de tu clase.

Cătălin Pitiș
fuente
Eso contaminaría el espacio de nombres anónimo con los typedefs, ¿no? El problema con typedef es que oculta el tipo real, lo que puede causar conflictos cuando se incluye en / por múltiples módulos, que son difíciles de encontrar / solucionar. Es una buena práctica contenerlos en espacios de nombres o dentro de clases.
Indy9000
3
Los conflictos de nombres y la contaminación anónima del espacio de nombres tienen poco que ver con mantener un nombre de tipo dentro de una clase o fuera. Puede tener un conflicto de nombres con su clase, no con sus typedefs. Entonces, para evitar la contaminación de nombres, use espacios de nombres. Declare su clase y los typedefs relacionados en un espacio de nombres.
Cătălin Pitiș
Otro argumento para poner el typedef dentro de una clase es el uso de funciones con plantilla. Cuando, por ejemplo, una función recibe un tipo de contenedor desconocido (vector o lista) que contiene un tipo de cadena desconocido (cadena o su propia variante conforme a la cadena). la única forma de averiguar el tipo de carga útil del contenedor es con el typedef 'value_type', que forma parte de la definición de clase del contenedor.
Marius el
1

Cuando typedef se usa solo dentro de la propia clase (es decir, se declara como privado), creo que es una buena idea. Sin embargo, por exactamente las razones que usted da, no lo usaría si los typedef necesitan ser conocidos fuera de la clase. En ese caso, recomiendo moverlos fuera de la clase.

Stefan Rådström
fuente