Esta pregunta frecuente trata sobre los agregados y los POD y cubre el siguiente material:
- ¿Qué son los agregados ?
- ¿Qué son los POD (datos simples)?
- ¿Como están relacionados?
- ¿Cómo y por qué son especiales?
- ¿Qué cambia para C ++ 11?
Esta pregunta frecuente trata sobre los agregados y los POD y cubre el siguiente material:
Respuestas:
Cómo leer:
Este artículo es bastante largo. Si desea saber acerca de los agregados y los POD (datos antiguos simples), tómese el tiempo y léalo. Si solo le interesan los agregados, lea solo la primera parte. Si solo le interesan los POD, primero debe leer la definición, las implicaciones y los ejemplos de agregados y luego puede saltar a los POD, pero aún así le recomendaría leer la primera parte en su totalidad. La noción de agregados es esencial para definir los POD. Si encuentra algún error (incluso menor, incluyendo gramática, estilística, formato, sintaxis, etc.), deje un comentario, lo editaré.
Esta respuesta se aplica a C ++ 03. Para otros estándares de C ++ ver:
¿Qué son los agregados y por qué son especiales?
Definición formal del estándar C ++ ( C ++ 03 8.5.1 §1 ) :
Entonces, OK, analicemos esta definición. En primer lugar, cualquier matriz es un agregado. Una clase también puede ser un agregado si ... ¡espera! no se dice nada sobre estructuras o sindicatos, ¿no pueden ser agregados? Sí pueden. En C ++, el término se
class
refiere a todas las clases, estructuras y uniones. Entonces, una clase (o estructura, o unión) es un agregado si y solo si cumple los criterios de las definiciones anteriores. ¿Qué implican estos criterios?Esto no significa que una clase agregada no pueda tener constructores, de hecho puede tener un constructor predeterminado y / o un constructor de copia siempre que el compilador los declare implícitamente y no explícitamente el usuario
No hay miembros de datos privados o protegidos no estáticos . Puede tener tantas funciones miembro privadas y protegidas (pero no constructores), así como tantos miembros privados o protegidos de datos estáticos y funciones miembro como desee y no violar las reglas para las clases agregadas
Una clase agregada puede tener un operador y / o destructor de asignación de copia definido / definido por el usuario
Una matriz es un agregado, incluso si es una matriz de tipo de clase no agregada.
Ahora veamos algunos ejemplos:
Tienes la idea. Ahora veamos cómo los agregados son especiales. A diferencia de las clases no agregadas, se pueden inicializar con llaves
{}
. Esta sintaxis de inicialización se conoce comúnmente para las matrices, y acabamos de enterarnos de que son agregados. Entonces, comencemos con ellos.Type array_name[n] = {a1, a2, …, am};
if (m == n)
el i- ésimo elemento de la matriz se inicializa con un i
else if (m <n)
los primeros m elementos de la matriz se inicializan con 1 , a 2 , ..., a my los otros
n - m
elementos son, si es posible, inicializados en valor (vea a continuación la explicación del término) de locontrario si (m> n)
el compilador emitirá un error
más (este es el caso cuando n no se especifica del todo
int a[] = {1, 2, 3};
)el tamaño de se supone que la matriz (n) es igual a m, por lo que
int a[] = {1, 2, 3};
es equivalente aint a[3] = {1, 2, 3};
Cuando un objeto de tipo escalar (
bool
,int
,char
,double
, punteros, etc.) es de valor inicializado que significa que se inicializa con0
para ese tipo (false
parabool
,0.0
paradouble
, etc.). Cuando un objeto de tipo de clase con un constructor predeterminado declarado por el usuario tiene un valor inicializado, se llama a su constructor predeterminado. Si el constructor predeterminado se define implícitamente, todos los miembros no estáticos se inicializan de forma recursiva. Esta definición es imprecisa y un poco incorrecta, pero debería darle la idea básica. Una referencia no puede ser inicializada en valor. La inicialización del valor para una clase no agregada puede fallar si, por ejemplo, la clase no tiene un constructor predeterminado apropiado.Ejemplos de inicialización de matriz:
Ahora veamos cómo se pueden inicializar las clases agregadas con llaves. Más o menos de la misma manera. En lugar de los elementos de la matriz, inicializaremos los miembros de datos no estáticos en el orden de su aparición en la definición de clase (todos son públicos por definición). Si hay menos inicializadores que miembros, el resto tiene valor inicializado. Si es imposible inicializar con valor uno de los miembros que no se inicializaron explícitamente, obtenemos un error en tiempo de compilación. Si hay más inicializadores de los necesarios, también obtenemos un error en tiempo de compilación.
En el ejemplo anterior
y.c
se inicializa con'a'
,y.x.i1
con10
,y.x.i2
con20
,y.i[0]
con20
,y.i[1]
con30
yy.f
se inicializa el valor, es decir, se inicializa con0.0
. El miembro estático protegidod
no se inicializa en absoluto, porque lo esstatic
.Las uniones agregadas son diferentes en que puede inicializar solo su primer miembro con llaves. Creo que si está lo suficientemente avanzado en C ++ como para considerar el uso de uniones (su uso puede ser muy peligroso y debe pensarse cuidadosamente), puede buscar las reglas para las uniones en el estándar usted mismo :).
Ahora que sabemos qué tienen de especial los agregados, intentemos comprender las restricciones sobre las clases; Por eso están ahí. Debemos entender que la inicialización de los miembros con llaves implica que la clase no es más que la suma de sus miembros. Si está presente un constructor definido por el usuario, significa que el usuario necesita hacer un trabajo adicional para inicializar los miembros, por lo tanto, la inicialización de llaves sería incorrecta. Si las funciones virtuales están presentes, significa que los objetos de esta clase tienen (en la mayoría de las implementaciones) un puntero a la llamada vtable de la clase, que se establece en el constructor, por lo que la inicialización de llaves sería insuficiente. Podrías resolver el resto de las restricciones de manera similar a un ejercicio :).
Tan suficiente sobre los agregados. Ahora podemos definir un conjunto más estricto de tipos, a saber, POD
¿Qué son los POD y por qué son especiales?
Definición formal del estándar C ++ ( C ++ 03 9 §4 ) :
Wow, este es más difícil de analizar, ¿no? :) Dejemos de lado las uniones (por los mismos motivos que antes) y reformulemos de una manera más clara:
¿Qué implica esta definición? (¿Mencioné POD significa Plain Old Data ?)
Ejemplos:
Las clases POD, las uniones POD, los tipos escalares y las matrices de tales tipos se denominan colectivamente tipos POD.
Los POD son especiales en muchos sentidos. Proporcionaré solo algunos ejemplos.
Las clases POD son las más cercanas a las estructuras C. A diferencia de ellos, los POD pueden tener funciones miembro y miembros estáticos arbitrarios, pero ninguno de estos dos cambia la disposición de la memoria del objeto. Por lo tanto, si desea escribir una biblioteca dinámica más o menos portátil que se pueda usar desde C e incluso .NET, debe intentar hacer que todas sus funciones exportadas tomen y devuelvan solo parámetros de tipos POD.
La vida útil de los objetos de tipo de clase que no es POD comienza cuando el constructor ha terminado y termina cuando el destructor ha terminado. Para las clases POD, la vida útil comienza cuando el almacenamiento del objeto está ocupado y termina cuando ese almacenamiento se libera o se reutiliza.
Para los objetos de tipo POD, el estándar garantiza que cuando
memcpy
el contenido de su objeto se convierta en una matriz de caracteres char o unsigned char, y luegomemcpy
el contenido vuelva a su objeto, el objeto mantendrá su valor original. Tenga en cuenta que no existe tal garantía para objetos de tipos que no sean POD. Además, puede copiar objetos POD de forma segura conmemcpy
. El siguiente ejemplo supone que T es un tipo POD:Ir a la declaración. Como sabrán, es ilegal (el compilador debe emitir un error) realizar un salto a través de goto desde un punto donde alguna variable aún no estaba dentro del alcance hasta un punto donde ya está dentro del alcance. Esta restricción se aplica solo si la variable es de tipo no POD. En el siguiente ejemplo
f()
está mal formado, mientras queg()
está bien formado. Tenga en cuenta que el compilador de Microsoft es demasiado liberal con esta regla: solo emite una advertencia en ambos casos.Se garantiza que no habrá relleno al comienzo de un objeto POD. En otras palabras, si el primer miembro de una clase de POD A es de tipo T, que puede de manera segura
reinterpret_cast
a partirA*
deT*
y obtener el puntero al primer elemento y viceversa.La lista sigue y sigue…
Conclusión
Es importante entender qué es exactamente un POD porque muchas características del lenguaje, como puede ver, se comportan de manera diferente para ellos.
fuente
private:
según corresponda):struct A { int const a; };
entoncesA()
está bien formado, incluso siA
la definición de constructor por defecto estaría mal formada.¿Qué cambia para C ++ 11?
Agregados
La definición estándar de un agregado ha cambiado ligeramente, pero sigue siendo más o menos lo mismo:
Ok, que ha cambiado?
Anteriormente, un agregado no podía tener constructores declarados por el usuario , pero ahora no puede tener constructores proporcionados por el usuario . ¿Hay una diferencia? Sí, lo hay, porque ahora puedes declarar constructores y predeterminarlos :
Esto sigue siendo un agregado porque un constructor (o cualquier función miembro especial) que está predeterminado en la primera declaración no es proporcionado por el usuario.
Ahora un agregado no puede tener ningún inicializador de paréntesis o igual para miembros de datos no estáticos. ¿Qué significa esto? Bueno, esto es solo porque con este nuevo estándar, podemos inicializar miembros directamente en la clase de esta manera:
El uso de esta característica hace que la clase ya no sea un agregado porque es básicamente equivalente a proporcionar su propio constructor predeterminado.
Entonces, lo que es un agregado no cambió mucho en absoluto. Sigue siendo la misma idea básica, adaptada a las nuevas características.
¿Qué pasa con los POD?
Los POD pasaron por muchos cambios. Muchas de las reglas anteriores sobre los POD se relajaron en este nuevo estándar, y la forma en que se proporciona la definición en el estándar cambió radicalmente.
La idea de un POD es capturar básicamente dos propiedades distintas:
Debido a esto, la definición se ha dividido en dos conceptos distintos: clases triviales y clases de diseño estándar , porque son más útiles que POD. El estándar ahora raramente usa el término POD, prefiriendo los conceptos triviales y de diseño estándar más específicos .
La nueva definición básicamente dice que un POD es una clase que es trivial y tiene un diseño estándar, y esta propiedad debe ser recursiva para todos los miembros de datos no estáticos:
Repasemos cada una de estas dos propiedades en detalle por separado.
Clases triviales
Trivial es la primera propiedad mencionada anteriormente: las clases triviales admiten la inicialización estática. Si una clase es trivialmente copiable (un superconjunto de clases triviales), está bien copiar su representación sobre el lugar con cosas como
memcpy
y esperar que el resultado sea el mismo.El estándar define una clase trivial de la siguiente manera:
Entonces, ¿qué son todas esas cosas triviales y no triviales?
Básicamente, esto significa que un constructor de copia o movimiento es trivial si no es proporcionado por el usuario, la clase no tiene nada virtual y esta propiedad es recursiva para todos los miembros de la clase y para la clase base.
La definición de un operador de asignación de copia / movimiento trivial es muy similar, simplemente reemplazando la palabra "constructor" con "operador de asignación".
Un destructor trivial también tiene una definición similar, con la restricción añadida de que no puede ser virtual.
Y existe otra regla similar para los constructores predeterminados triviales, con la adición de que un constructor predeterminado no es trivial si la clase tiene miembros de datos no estáticos con inicializadores de llaves o iguales , que hemos visto anteriormente.
Aquí hay algunos ejemplos para aclarar todo:
Diseño estándar
El diseño estándar es la segunda propiedad. El estándar menciona que estos son útiles para comunicarse con otros lenguajes, y eso se debe a que una clase de diseño estándar tiene el mismo diseño de memoria que la estructura o unión C equivalente.
Esta es otra propiedad que debe ser recursiva para los miembros y todas las clases base. Y, como de costumbre, no se permiten funciones virtuales ni clases base virtuales. Eso haría que el diseño sea incompatible con C.
Una regla relajada aquí es que las clases de diseño estándar deben tener todos los miembros de datos no estáticos con el mismo control de acceso. Anteriormente éstos tuvieron que ser todo público , pero ahora puede que sean privadas o protegidas, siempre y cuando son todos privados o todo protegido.
Cuando se usa la herencia, solo una clase en todo el árbol de herencia puede tener miembros de datos no estáticos, y el primer miembro de datos no estáticos no puede ser de un tipo de clase base (esto podría romper las reglas de alias), de lo contrario, no es un estándar. clase de diseño
Así es como va la definición en el texto estándar:
Y veamos algunos ejemplos.
Conclusión
Con estas nuevas reglas, muchos más tipos pueden ser POD ahora. E incluso si un tipo no es POD, podemos aprovechar algunas de las propiedades de POD por separado (si es solo una de diseño trivial o estándar).
La biblioteca estándar tiene rasgos para probar estas propiedades en el encabezado
<type_traits>
:fuente
Lo que ha cambiado para C ++ 14
Podemos consultar el estándar Draft C ++ 14 como referencia.
Agregados
Esto está cubierto en la sección
8.5.1
Agregados que nos da la siguiente definición:El único cambio ahora es agregar inicializadores de miembros en clase no hace que una clase sea no agregada. Entonces, el siguiente ejemplo de la inicialización agregada de C ++ 11 para clases con inicializadores en ritmo de miembros :
no era un agregado en C ++ 11 pero está en C ++ 14. Este cambio está cubierto en N3605: Inicializadores y agregados de miembros , que tiene el siguiente resumen:
POD permanece igual
La definición de estructura POD ( datos antiguos simples ) se trata en la sección
9
Clases que dice:que es la misma redacción que C ++ 11.
Cambios de diseño estándar para C ++ 14
Como se señaló en el pod de comentarios, se basa en la definición de diseño estándar y eso sí cambió para C ++ 14, pero esto fue a través de informes de defectos que se aplicaron a C ++ 14 después del hecho.
Había tres DR:
Entonces , el diseño estándar pasó de este Pre C ++ 14:
Para esto en C ++ 14 :
fuente
Lo intentaré:
Eso es simple: todos los miembros de datos no estáticos deben todo ser
public
,private
oprotected
. No puedes tener algopublic
y algoprivate
.El razonamiento para ellos va al razonamiento para tener una distinción entre "diseño estándar" y "diseño no estándar". Es decir, dar al compilador la libertad de elegir cómo guardar las cosas en la memoria. No se trata solo de punteros vtable.
Cuando estandarizaron C ++ en 98, tuvieron que predecir básicamente cómo la gente lo implementaría. Si bien tenían bastante experiencia en la implementación con varios tipos de C ++, no estaban seguros de las cosas. Entonces decidieron ser cautelosos: dar a los compiladores tanta libertad como sea posible.
Es por eso que la definición de POD en C ++ 98 es tan estricta. Le dio a los compiladores de C ++ una gran libertad en el diseño de miembros para la mayoría de las clases. Básicamente, los tipos de POD estaban destinados a ser casos especiales, algo que escribiste específicamente por una razón.
Cuando se estaba trabajando en C ++ 11, tenían mucha más experiencia con compiladores. Y se dieron cuenta de que ... los escritores del compilador de C ++ son realmente vagos. Tenían toda esta libertad, pero no hacen nada con ella.
Las reglas del diseño estándar codifican más o menos la práctica común: la mayoría de los compiladores realmente no tuvieron que cambiar mucho para implementarlos (aparte de quizás algunas cosas para los rasgos de tipo correspondientes).
Ahora, cuando se trataba de
public
/private
, las cosas son diferentes. La libertad de reordenar qué miembros sonpublic
vs.private
realidad puede ser importante para el compilador, particularmente en las versiones de depuración. Y dado que el objetivo del diseño estándar es que hay compatibilidad con otros idiomas, no puede hacer que el diseño sea diferente en depuración frente a lanzamiento.Luego está el hecho de que realmente no hace daño al usuario. Si está haciendo una clase encapsulada, es muy probable que todos sus miembros de datos lo sean de
private
todos modos. Por lo general, no expone a los miembros de datos públicos en tipos totalmente encapsulados. Entonces, esto solo sería un problema para aquellos pocos usuarios que quieren hacer eso, que quieren esa división.Entonces no es una gran pérdida.
La razón de esto es por qué estandarizaron nuevamente el diseño estándar: práctica común.
No hay una práctica común cuando se trata de tener dos miembros de un árbol de herencia que realmente almacenan cosas. Algunos ponen la clase base antes que la derivada, otros lo hacen al revés. ¿De qué manera ordena a los miembros si provienen de dos clases base? Y así. Los compiladores divergen mucho en estas preguntas.
Además, gracias a la regla de cero / uno / infinito, una vez que dice que puede tener dos clases con miembros, puede decir tantas como desee. Esto requiere agregar muchas reglas de diseño sobre cómo manejar esto. Debe decir cómo funciona la herencia múltiple, qué clases colocan sus datos antes que otras clases, etc. Esas son muchas reglas, con muy poca ganancia de material.
No puede hacer todo lo que no tenga funciones virtuales y un diseño estándar de constructor predeterminado.
Realmente no puedo hablar con este. No estoy lo suficientemente educado en las reglas de alias de C ++ para entenderlo realmente. Pero tiene algo que ver con el hecho de que el miembro base compartirá la misma dirección que la clase base misma. Es decir:
Y eso probablemente va en contra de las reglas de alias de C ++. De alguna manera.
Sin embargo, considere esto: lo útil que podría tener la capacidad de hacer esto nunca realmente ser? Como solo una clase puede tener miembros de datos no estáticos,
Derived
debe ser esa clase (ya que tiene unBase
como miembro). PorBase
lo tanto, debe estar vacío (de datos). Y siBase
está vacío, así como una clase base ... ¿por qué tener un miembro de datos?Como
Base
está vacío, no tiene estado. Por lo tanto, cualquier función miembro no estática hará lo que hace en función de sus parámetros, no de suthis
puntero.Así que de nuevo: sin grandes pérdidas.
fuente
static_cast<Base*>(&d)
y&d.b
son del mismoBase*
tipo, apuntan a cosas diferentes, rompiendo así la regla de alias. Por favor corrigeme.Derived
debe ser esa clase?Derived
el primer miembro sea su clase base, debe tener dos cosas: una clase base y un miembro . Y dado que solo una clase en la jerarquía puede tener miembros (y aún ser un diseño estándar), esto significa que su clase base no puede tener miembros.Cambios en C ++ 17
Descargue el borrador final del estándar internacional C ++ 17 aquí .
Agregados
C ++ 17 expande y mejora los agregados y la inicialización de agregados. La biblioteca estándar ahora también incluye una
std::is_aggregate
clase de rasgo de tipo. Aquí está la definición formal de la sección 11.6.1.1 y 11.6.1.2 (referencias internas eliminadas):¿Qué cambió?
Clases triviales
La definición de clase trivial se modificó en C ++ 17 para abordar varios defectos que no se abordaron en C ++ 14. Los cambios fueron de naturaleza técnica. Aquí está la nueva definición en 12.0.6 (referencias internas eliminadas):
Cambios:
std::memcpy
. Esta fue una contradicción semántica, porque, al definir como eliminados todos los operadores de construcción / asignación, el creador de la clase claramente tenía la intención de que la clase no se pudiera copiar / mover, pero la clase aún cumplía con la definición de una clase trivialmente copiable. Por lo tanto, en C ++ 17 tenemos una nueva cláusula que establece que la clase trivialmente copiable debe tener al menos un constructor / operador de copia / movimiento trivial, no eliminado (aunque no necesariamente accesible públicamente). Ver N4148 , DR1734Clases de diseño estándar
La definición de diseño estándar también se modificó para abordar los informes de defectos. Nuevamente, los cambios fueron de naturaleza técnica. Aquí está el texto del estándar (12.0.7). Como antes, se eluyen las referencias internas:
Cambios:
Nota: El comité de estándares de C ++ pretendía que los cambios anteriores basados en informes de defectos se aplicaran a C ++ 14, aunque el nuevo lenguaje no está en el estándar publicado de C ++ 14. Está en el estándar C ++ 17.
fuente
¿Qué cambia en c ++ 20
Siguiendo el resto del tema claro de esta pregunta, el significado y el uso de los agregados continúa cambiando con cada estándar. Hay varios cambios clave en el horizonte.
Tipos con constructores declarados por el usuario P1008
En C ++ 17, este tipo sigue siendo un agregado:
Y, por lo tanto,
X{}
todavía se compila porque eso es una inicialización agregada, no una invocación de constructor. Ver también: ¿ Cuándo un constructor privado no es un constructor privado?En C ++ 20, la restricción cambiará de requerir:
a
Esto ha sido adoptado en el borrador de trabajo de C ++ 20 . Ni el
X
aquí ni elC
en la pregunta vinculada serán agregados en C ++ 20.Esto también produce un efecto de yoyo con el siguiente ejemplo:
En C ++ 11/14, no
B
era un agregado debido a la clase base, por lo que realiza una inicialización de valor que llama a qué llamadas , en un punto donde es accesible. Esto estaba bien formado.B{}
B::B()
A::A()
En C ++ 17, se
B
convirtió en un agregado porque las clases base estaban permitidas, lo que hizo queB{}
la inicialización del agregado. Esto requiere una lista de inicialización de copiaA
desde{}
, pero desde fuera del contexto deB
, donde no es accesible. En C ++ 17, esto está mal formado (auto x = B();
aunque estaría bien).En C ++ 20 ahora, debido al cambio de regla anterior,
B
una vez más deja de ser un agregado (no por la clase base, sino por el constructor predeterminado declarado por el usuario, a pesar de que está predeterminado). Así que volvemos a revisarB
el constructor, y este fragmento se forma bien.Inicializar agregados de una lista de valores entre paréntesis P960
Un problema común que surge es querer usar
emplace()
constructores de estilo con agregados:Esto no funciona, porque
emplace
intentará realizar de manera efectiva la inicializaciónX(1, 2)
, que no es válida. La solución típica es agregar un constructorX
, pero con esta propuesta (actualmente trabajando en Core), los agregados tendrán sintetizados constructores que hagan lo correcto y se comporten como constructores regulares. El código anterior se compilará tal cual en C ++ 20.Deducción de argumentos de plantilla de clase (CTAD) para agregados P1021 (específicamente P1816 )
En C ++ 17, esto no compila:
Los usuarios tendrían que escribir su propia guía de deducción para todas las plantillas agregadas:
Pero como esto es, en cierto sentido, "lo más obvio" que hacer, y básicamente es solo un ejemplo, el lenguaje lo hará por usted. Este ejemplo se compilará en C ++ 20 (sin la necesidad de la guía de deducciones proporcionada por el usuario).
fuente