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:
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:
Definición formal del estándar C ++ ( C ++ 03 8.5.1 §1 ) :
Un agregado es una matriz o una clase (cláusula 9) sin constructores declarados por el usuario (12.1), sin miembros de datos no estáticos privados o protegidos (cláusula 11), sin clases base (cláusula 10) y sin funciones virtuales (10.3 )
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:
class NotAggregate1
{
virtual void f() {} //remember? no virtual functions
};
class NotAggregate2
{
int x; //x is private by default and non-static
};
class NotAggregate3
{
public:
NotAggregate3(int) {} //oops, user-defined constructor
};
class Aggregate1
{
public:
NotAggregate1 member1; //ok, public member
Aggregate1& operator=(Aggregate1 const & rhs) {/* */} //ok, copy-assignment
private:
void f() {} // ok, just a private function
};
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 otrosn - m
elementos son, si es posible, inicializados en valor (vea a continuación la explicación del término) de lo
contrario 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 queint 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 con 0
para ese tipo ( false
para bool
, 0.0
para double
, 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:
class A
{
public:
A(int) {} //no default constructor
};
class B
{
public:
B() {} //default constructor available
};
int main()
{
A a1[3] = {A(2), A(1), A(14)}; //OK n == m
A a2[3] = {A(2)}; //ERROR A has no default constructor. Unable to value-initialize a2[1] and a2[2]
B b1[3] = {B()}; //OK b1[1] and b1[2] are value initialized, in this case with the default-ctor
int Array1[1000] = {0}; //All elements are initialized with 0;
int Array2[1000] = {1}; //Attention: only the first element is 1, the rest are 0;
bool Array3[1000] = {}; //the braces can be empty too. All elements initialized with false
int Array4[1000]; //no initializer. This is different from an empty {} initializer in that
//the elements in this case are not value-initialized, but have indeterminate values
//(unless, of course, Array4 is a global array)
int array[2] = {1, 2, 3, 4}; //ERROR, too many initializers
}
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.
struct X
{
int i1;
int i2;
};
struct Y
{
char c;
X x;
int i[2];
float f;
protected:
static double d;
private:
void g(){}
};
Y y = {'a', {10, 20}, {20, 30}};
En el ejemplo anterior y.c
se inicializa con 'a'
, y.x.i1
con 10
, y.x.i2
con 20
, y.i[0]
con 20
, y.i[1]
con 30
y y.f
se inicializa el valor, es decir, se inicializa con 0.0
. El miembro estático protegido d
no se inicializa en absoluto, porque lo es static
.
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
Definición formal del estándar C ++ ( C ++ 03 9 §4 ) :
Un POD-struct es una clase agregada que no tiene miembros de datos no estáticos de tipo non-POD-struct, non-POD-union (o array de tales tipos) o referencia, y no tiene operador de asignación de copia definido por el usuario y no destructor definido por el usuario. Del mismo modo, una unión POD es una unión agregada que no tiene miembros de datos no estáticos de tipo non-POD-struct, non-POD-union (o matriz de tales tipos) o referencia, y no tiene operador de asignación de copia definido por el usuario y sin destructor definido por el usuario. Una clase POD es una clase que es una estructura POD o una unión POD.
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:
Una clase agregada se denomina POD si no tiene un operador y destructor de asignación de copias definido por el usuario y ninguno de sus miembros no estáticos es una clase no POD, una matriz de no POD o una referencia.
¿Qué implica esta definición? (¿Mencioné POD significa Plain Old Data ?)
Ejemplos:
struct POD
{
int x;
char y;
void f() {} //no harm if there's a function
static std::vector<char> v; //static members do not matter
};
struct AggregateButNotPOD1
{
int x;
~AggregateButNotPOD1() {} //user-defined destructor
};
struct AggregateButNotPOD2
{
AggregateButNotPOD1 arrOfNonPod[3]; //array of non-POD class
};
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 luego memcpy
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 con memcpy
. El siguiente ejemplo supone que T es un tipo POD:
#define N sizeof(T)
char buf[N];
T obj; // obj initialized to its original value
memcpy(buf, &obj, N); // between these two calls to memcpy,
// obj might be modified
memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type
// holds its original value
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 que g()
está bien formado. Tenga en cuenta que el compilador de Microsoft es demasiado liberal con esta regla: solo emite una advertencia en ambos casos.
int f()
{
struct NonPOD {NonPOD() {}};
goto label;
NonPOD x;
label:
return 0;
}
int g()
{
struct POD {int i; char c;};
goto label;
POD x;
label:
return 0;
}
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 partir A*
de T*
y obtener el puntero al primer elemento y viceversa.
La lista sigue y sigue…
Es importante entender qué es exactamente un POD porque muchas características del lenguaje, como puede ver, se comportan de manera diferente para ellos.
private:
según corresponda): struct A { int const a; };
entonces A()
está bien formado, incluso si A
la definición de constructor por defecto estaría mal formada.
La definición estándar de un agregado ha cambiado ligeramente, pero sigue siendo más o menos lo mismo:
Un agregado es una matriz o una clase (Cláusula 9) sin constructores proporcionados por el usuario (12.1), sin inicializadores de llaves o iguales para miembros de datos no estáticos (9.2), sin miembros de datos no estáticos privados o protegidos ( Cláusula 11), sin clases base (Cláusula 10) y sin funciones virtuales (10.3).
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 :
struct Aggregate {
Aggregate() = default; // asks the compiler to generate the default implementation
};
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:
struct NotAggregate {
int x = 5; // valid in C++11
std::vector<int> s{1,2,3}; // also valid
};
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.
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:
Una estructura POD es una clase sin unión que es a la vez una clase trivial y una clase de diseño estándar, y no tiene miembros de datos no estáticos de tipo estructura no POD, unión no POD (o matriz de tales tipos). Del mismo modo, una unión POD es una unión que es a la vez una clase trivial y una clase de diseño estándar, y no tiene miembros de datos no estáticos de tipo estructura no POD, unión no POD (o matriz de tales tipos). Una clase POD es una clase que es una estructura POD o una unión POD.
Repasemos cada una de estas dos propiedades en detalle por separado.
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:
Una clase trivialmente copiable es una clase que:
- no tiene constructores de copia no triviales (12.8),
- no tiene constructores de movimientos no triviales (12.8),
- no tiene operadores de asignación de copias no triviales (13.5.3, 12.8),
- no tiene operadores de asignación de movimientos no triviales (13.5.3, 12.8), y
- tiene un destructor trivial (12.4).
Una clase trivial es una clase que tiene un constructor trivial predeterminado (12.1) y es trivialmente copiable.
[ Nota: en particular, una clase trivialmente copiable o trivial no tiene funciones virtuales o clases base virtuales. —Final nota ]
Entonces, ¿qué son todas esas cosas triviales y no triviales?
Un constructor de copia / movimiento para la clase X es trivial si no es proporcionado por el usuario y si
- la clase X no tiene funciones virtuales (10.3) ni clases base virtuales (10.1), y
- el constructor seleccionado para copiar / mover cada subobjeto de clase base directa es trivial, y
- para cada miembro de datos no estático de X que es de tipo de clase (o matriz del mismo), el constructor seleccionado para copiar / mover ese miembro es trivial;
de lo contrario, el constructor copiar / mover no es trivial.
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:
// empty classes are trivial
struct Trivial1 {};
// all special members are implicit
struct Trivial2 {
int x;
};
struct Trivial3 : Trivial2 { // base class is trivial
Trivial3() = default; // not a user-provided ctor
int y;
};
struct Trivial4 {
public:
int a;
private: // no restrictions on access modifiers
int b;
};
struct Trivial5 {
Trivial1 a;
Trivial2 b;
Trivial3 c;
Trivial4 d;
};
struct Trivial6 {
Trivial2 a[23];
};
struct Trivial7 {
Trivial6 c;
void f(); // it's okay to have non-virtual functions
};
struct Trivial8 {
int x;
static NonTrivial1 y; // no restrictions on static members
};
struct Trivial9 {
Trivial9() = default; // not user-provided
// a regular constructor is okay because we still have default ctor
Trivial9(int x) : x(x) {};
int x;
};
struct NonTrivial1 : Trivial3 {
virtual void f(); // virtual members make non-trivial ctors
};
struct NonTrivial2 {
NonTrivial2() : z(42) {} // user-provided ctor
int z;
};
struct NonTrivial3 {
NonTrivial3(); // user-provided ctor
int w;
};
NonTrivial3::NonTrivial3() = default; // defaulted but not on first declaration
// still counts as user-provided
struct NonTrivial5 {
virtual ~NonTrivial5(); // virtual destructors are not trivial
};
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:
Una clase de diseño estándar es una clase que:
- no tiene miembros de datos no estáticos de tipo clase de diseño no estándar (o matriz de tales tipos) o referencia,
- no tiene funciones virtuales (10.3) ni clases base virtuales (10.1),
- tiene el mismo control de acceso (Cláusula 11) para todos los miembros de datos no estáticos,
- no tiene clases base de diseño no estándar,
- o no tiene miembros de datos no estáticos en la clase más derivada y como máximo una clase base con miembros de datos no estáticos, o no tiene clases base con miembros de datos no estáticos, y
- no tiene clases base del mismo tipo que el primer miembro de datos no estático.
Una estructura de diseño estándar es una clase de diseño estándar definida con la estructura de clave de clase o la clase de clave de clase.
Una unión de diseño estándar es una clase de diseño estándar definida con la unión de clave de clase.
[ Nota: las clases de diseño estándar son útiles para comunicarse con código escrito en otros lenguajes de programación. Su diseño se especifica en 9.2. —Final nota ]
Y veamos algunos ejemplos.
// empty classes have standard-layout
struct StandardLayout1 {};
struct StandardLayout2 {
int x;
};
struct StandardLayout3 {
private: // both are private, so it's ok
int x;
int y;
};
struct StandardLayout4 : StandardLayout1 {
int x;
int y;
void f(); // perfectly fine to have non-virtual functions
};
struct StandardLayout5 : StandardLayout1 {
int x;
StandardLayout1 y; // can have members of base type if they're not the first
};
struct StandardLayout6 : StandardLayout1, StandardLayout5 {
// can use multiple inheritance as long only
// one class in the hierarchy has non-static data members
};
struct StandardLayout7 {
int x;
int y;
StandardLayout7(int x, int y) : x(x), y(y) {} // user-provided ctors are ok
};
struct StandardLayout8 {
public:
StandardLayout8(int x) : x(x) {} // user-provided ctors are ok
// ok to have non-static data members and other members with different access
private:
int x;
};
struct StandardLayout9 {
int x;
static NonStandardLayout1 y; // no restrictions on static members
};
struct NonStandardLayout1 {
virtual f(); // cannot have virtual functions
};
struct NonStandardLayout2 {
NonStandardLayout1 X; // has non-standard-layout member
};
struct NonStandardLayout3 : StandardLayout1 {
StandardLayout1 x; // first member cannot be of the same type as base
};
struct NonStandardLayout4 : StandardLayout3 {
int z; // more than one class has non-static data members
};
struct NonStandardLayout5 : NonStandardLayout3 {}; // has a non-standard-layout base class
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>
:
template <typename T>
struct std::is_pod;
template <typename T>
struct std::is_trivial;
template <typename T>
struct std::is_trivially_copyable;
template <typename T>
struct std::is_standard_layout;
Podemos consultar el estándar Draft C ++ 14 como referencia.
Esto está cubierto en la sección 8.5.1
Agregados que nos da la siguiente definición:
Un agregado es una matriz o una clase (Cláusula 9) sin constructores proporcionados por el usuario (12.1), sin miembros de datos privados o protegidos no estáticos (Cláusula 11), sin clases base (Cláusula 10) y sin funciones virtuales (10.3 )
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 :
struct A
{
int a = 3;
int b = 3;
};
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:
Bjarne Stroustrup y Richard Smith plantearon un problema sobre la inicialización agregada y los inicializadores de miembros que no funcionan juntos. Este documento propone solucionar el problema adoptando la redacción propuesta por Smith que elimina la restricción de que los agregados no pueden tener inicializadores de miembros.
La definición de estructura POD ( datos antiguos simples ) se trata en la sección 9
Clases que dice:
Un POD struct 110 es una clase no sindical que es a la vez una clase trivial y una clase de diseño estándar, y no tiene miembros de datos no estáticos de tipo no POD struct, no POD union (o matriz de tales tipos). Del mismo modo, una unión POD es una unión que es tanto una clase trivial como una clase de diseño estándar, y no tiene miembros de datos no estáticos de tipo estructura no POD, unión no POD (o matriz de tales tipos). Una clase POD es una clase que es una estructura POD o una unión POD.
que es la misma redacción que C ++ 11.
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:
Una clase de diseño estándar es una clase que:
- (7.1) no tiene miembros de datos no estáticos de tipo clase de diseño no estándar (o conjunto de tales tipos) o referencia,
- (7.2) no tiene funciones virtuales ([class.virtual]) ni clases base virtuales ([class.mi]),
- (7.3) tiene el mismo control de acceso (Cláusula [class.access]) para todos los miembros de datos no estáticos,
- (7.4) no tiene clases base de diseño no estándar,
- (7.5) o no tiene miembros de datos no estáticos en la clase más derivada y como máximo una clase base con miembros de datos no estáticos, o no tiene clases base con miembros de datos no estáticos, y
- (7.6) no tiene clases base del mismo tipo que el primer miembro de datos no estático.109
Para esto en C ++ 14 :
Una clase S es una clase de diseño estándar si:
- (3.1) no tiene miembros de datos no estáticos de tipo clase de diseño no estándar (o conjunto de tales tipos) o referencia,
- (3.2) no tiene funciones virtuales ni clases base virtuales,
- (3.3) tiene el mismo control de acceso para todos los miembros de datos no estáticos,
- (3.4) no tiene clases base de diseño no estándar,
- (3.5) tiene como máximo un subobjeto de clase base de cualquier tipo dado,
- (3.6) tiene todos los miembros de datos no estáticos y campos de bits en la clase y sus clases base declaradas por primera vez en la misma clase, y
- (3.7) no tiene ningún elemento del conjunto M (S) de tipos como una clase base, donde para cualquier tipo X, M (X) se define de la siguiente manera.104 [Nota: M (X) es el conjunto de los tipos de todos los subobjetos de clase no base que pueden estar en un desplazamiento cero en X. - nota final]
- (3.7.1) Si X es un tipo de clase sin unión sin miembros de datos no estáticos (posiblemente heredados), el conjunto M (X) está vacío.
- (3.7.2) Si X es un tipo de clase sin unión con un miembro de datos no estático del tipo X0 que es de tamaño cero o es el primer miembro de datos no estático de X (donde dicho miembro puede ser una unión anónima ), el conjunto M (X) consta de X0 y los elementos de M (X0).
- (3.7.3) Si X es un tipo de unión, el conjunto M (X) es la unión de todos los M (Ui) y el conjunto que contiene todos los Ui, donde cada Ui es el tipo del i-ésimo miembro de datos no estático de X .
- (3.7.4) Si X es un tipo de matriz con el tipo de elemento Xe, el conjunto M (X) consta de Xe y los elementos de M (Xe).
- (3.7.5) Si X es un tipo que no es de clase ni de matriz, el conjunto M (X) está vacío.
¿Puedes por favor elaborar las siguientes reglas:
Lo intentaré:
a) las clases de diseño estándar deben tener todos los miembros de datos no estáticos con el mismo control de acceso
Eso es simple: todos los miembros de datos no estáticos deben todo ser public
, private
o protected
. No puedes tener algo public
y algo private
.
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 son public
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.
b) solo una clase en todo el árbol de herencia puede tener miembros de datos no estáticos,
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.
y el primer miembro de datos no estático no puede ser de un tipo de clase base (esto podría romper las reglas de alias).
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:
struct Base {};
struct Derived : Base { Base b; };
Derived d;
static_cast<Base*>(&d) == &d.b;
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 un Base
como miembro). Por Base
lo tanto, debe estar vacío (de datos). Y si Base
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 su this
puntero.
Así que de nuevo: sin grandes pérdidas.
static_cast<Base*>(&d)
y &d.b
son del mismo Base*
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.
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):
Un agregado es una matriz o una clase con
- sin constructores proporcionados por el usuario, explícitos o heredados,
- sin miembros de datos no estáticos privados o protegidos,
- sin funciones virtuales y
- sin clases base virtuales, privadas o protegidas.
[Nota: La inicialización agregada no permite acceder a miembros o constructores de clases base protegidas y privadas. —Nota final]
Los elementos de un agregado son:
- para una matriz, los elementos de la matriz en orden de subíndice creciente, o
- para una clase, las clases base directas en orden de declaración, seguidas de los miembros de datos directos no estáticos que no son miembros de un sindicato anónimo, en orden de declaración.
¿Qué cambió?
struct B1 // not a aggregate
{
int i1;
B1(int a) : i1(a) { }
};
struct B2
{
int i2;
B2() = default;
};
struct M // not an aggregate
{
int m;
M(int a) : m(a) { }
};
struct C : B1, B2
{
int j;
M m;
C() = default;
};
C c { { 1 }, { 2 }, 3, { 4 } };
cout
<< "is C aggregate?: " << (std::is_aggregate<C>::value ? 'Y' : 'N')
<< " i1: " << c.i1 << " i2: " << c.i2
<< " j: " << c.j << " m.m: " << c.m.m << endl;
//stdout: is C aggregate?: Y, i1=1 i2=2 j=3 m.m=4
struct D // not an aggregate
{
int i = 0;
D() = default;
explicit D(D const&) = default;
};
struct B1
{
int i1;
B1() : i1(0) { }
};
struct C : B1 // not an aggregate
{
using B1::B1;
};
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):
Una clase copiable trivialmente es una clase:
- donde cada constructor de copia, constructor de movimiento, operador de asignación de copia y operador de asignación de movimiento es eliminado o trivial,
- que tiene al menos un constructor de copia, constructor de movimiento, operador de asignación de copia no eliminado, o mover operador de asignación, y
- que tiene un destructor trivial, no eliminado.
Una clase trivial es una clase que se puede copiar trivialmente y tiene uno o más constructores predeterminados, todos los cuales son triviales o eliminados y al menos uno de los cuales no se elimina. [Nota: en particular, una clase trivialmente copiable o trivial no tiene funciones virtuales o clases base virtuales. Nota final]
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:
Una clase S es una clase de diseño estándar si:
- no tiene miembros de datos no estáticos de tipo clase de diseño no estándar (o matriz de tales tipos) o referencia,
- no tiene funciones virtuales ni clases base virtuales,
- tiene el mismo control de acceso para todos los miembros de datos no estáticos,
- no tiene clases base de diseño no estándar,
- tiene como máximo un subobjeto de clase base de cualquier tipo dado,
- tiene todos los miembros de datos no estáticos y campos de bits en la clase y sus clases base declaradas por primera vez en la misma clase, y - Si X es un tipo de clase sin unión cuyo primer miembro de datos no estático tiene tipo X0 (donde dicho miembro puede ser una unión anónima), el conjunto M (X ) consta de X0 y los elementos de M (X0).
- no tiene ningún elemento del conjunto M (S) de tipos (definidos a continuación) como una clase base.108 - Si X es un tipo de clase sin unión sin ( posiblemente heredados) miembros de datos no estáticos, el conjunto M (X) está vacío.
M (X) se define de la siguiente manera:
- Si X es un tipo de unión, el conjunto M (X) es la unión de todas las M (Ui) y el conjunto que contiene todas las Ui, donde cada Ui es el tipo del miembro de datos no estático i-ésimo de X.
- Si X es un tipo de matriz con el tipo de elemento Xe, el conjunto M (X) consta de Xe y los elementos de M (Xe). [Nota: M (X) es el conjunto de los tipos de todos los subobjetos que no son de clase base que están garantizados en una clase de diseño estándar para estar en un desplazamiento cero en X. —final nota] [Ejemplo:
- Si X es un tipo que no es de clase ni de matriz, el conjunto M (X) está vacío.
—Ejemplo final]struct B { int i; }; // standard-layout class struct C : B { }; // standard-layout class struct D : C { }; // standard-layout class struct E : D { char : 4; }; // not a standard-layout class struct Q {}; struct S : Q { }; struct T : Q { }; struct U : S, T { }; // not a standard-layout class
108) Esto asegura que dos subobjetos que tienen el mismo tipo de clase y que pertenecen al mismo objeto más derivado no se asignan a la misma dirección.
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.
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.
En C ++ 17, este tipo sigue siendo un agregado:
struct X {
X() = delete;
};
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:
sin
explicit
constructores heredados o proporcionados por el usuario
a
sin constructores declarados por el usuario o heredados
Esto ha sido adoptado en el borrador de trabajo de C ++ 20 . Ni elX
aquí ni el C
en la pregunta vinculada serán agregados en C ++ 20.
Esto también produce un efecto de yoyo con el siguiente ejemplo:
class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};
En C ++ 11/14, noB
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 que B{}
la inicialización del agregado. Esto requiere una lista de inicialización de copia A
desde {}
, pero desde fuera del contexto de B
, 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 revisar B
el constructor, y este fragmento se forma bien.
Un problema común que surge es querer usar emplace()
constructores de estilo con agregados:
struct X { int a, b; };
std::vector<X> xs;
xs.emplace_back(1, 2); // error
Esto no funciona, porque emplace
intentará realizar de manera efectiva la inicialización X(1, 2)
, que no es válida. La solución típica es agregar un constructor X
, 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.
En C ++ 17, esto no compila:
template <typename T>
struct Point {
T x, y;
};
Point p{1, 2}; // error
Los usuarios tendrían que escribir su propia guía de deducción para todas las plantillas agregadas:
template <typename T> Point(T, T) -> Point<T>;
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).