[c++] ¿Qué son los agregados y POD y cómo / por qué son especiales?



Answers

¿Qué cambios para C ++ 11?

Agregados

La definición estándar de un agregado ha cambiado ligeramente, pero sigue siendo más o menos el mismo:

Un agregado es una matriz o una clase (Cláusula 9) sin constructores proporcionados por el usuario (12.1), sin inicializadores ortográficos para miembros de datos no estáticos (9.2), sin miembros de datos no estáticos protegidos o privados ( Cláusula 11), sin clases base (Cláusula 10) y sin funciones virtuales (10.3).

Ok, ¿qué cambió?

  1. 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 establecerlos por defecto :

    struct Aggregate {
        Aggregate() = default; // asks the compiler to generate the default implementation
    };
    

    Esto todavía es un agregado porque un constructor (o cualquier función de miembro especial) que está predeterminado en la primera declaración no es proporcionado por el usuario.

  2. Ahora, un agregado no puede tener ningún inicializador ortogonal o igual para los 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, qué es un agregado no cambió mucho en absoluto. Sigue siendo la misma idea básica, adaptada a las nuevas características.

¿Qué hay de los POD?

Los POD sufrieron 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:

  1. Es compatible con la inicialización estática, y
  2. Compilar un POD en C ++ le da el mismo diseño de memoria que una estructura compilada en C.

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 más triviales y de diseño estándar .

La nueva definición básicamente dice que un POD es una clase que es a la vez trivial y tiene un diseño estándar, y esta propiedad debe mantenerse recursivamente para todos los miembros de datos no estáticos:

Una estructura POD es una clase no sindical 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). De manera similar, 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.

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:

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 movimiento no triviales (13.5.3, 12.8), y

- tiene un destructor trivial (12.4).

Una clase trivial es una clase que tiene un constructor predeterminado trivial (12.1) y se puede copiar trivialmente.

[ Nota: en particular, una clase trivialmente copiable o trivial no tiene funciones virtuales o clases base virtuales. -finalizar 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 directo de clase base es trivial, y

- para cada miembro de datos no estáticos de X que sea del 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 lo proporciona el usuario, la clase no tiene nada virtual, y esta propiedad se mantiene recursivamente 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" por "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 aún existe otra regla similar para los constructores triviales por defecto, con la adición de que un constructor predeterminado no es trivial si la clase tiene miembros de datos no estáticos con llaves o igual inicializadores , 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
};

Diseño estándar

El diseño estándar es la segunda propiedad. La norma menciona que estos son útiles para comunicarse con otros idiomas, y eso se debe a que una clase de diseño estándar tiene el mismo diseño de memoria de 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 siempre, 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, estos tenían que ser todos públicos , pero ahora puedes hacerlos privados o protegidos, siempre y cuando sean privados o estén todos protegidos.

Al usar herencia, solo una clase en el árbol de herencia completo 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 aliasing); 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,

- 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áticos.

Una estructura de disposición 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 el código escrito en otros lenguajes de programación. Su diseño se especifica en 9.2. -finalizar 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

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 POD por separado (si es solo una de diseño trivial o estándar).

La biblioteca estándar tiene características 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;
Question

Esta pregunta frecuente es sobre agregados y POD y cubre el siguiente material:

  • ¿Qué son agregados ?
  • ¿Qué son los POD (datos antiguos simples)?
  • ¿Como están relacionados?
  • ¿Cómo y por qué son especiales?
  • ¿Qué cambios para C ++ 11?



puede 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 ser public , private o protected . No puedes tener algunos public y otros private .

El razonamiento para ellos va al razonamiento de tener una distinción entre "diseño estándar" y "diseño no estándar" en absoluto. A saber, para dar al compilador la libertad de elegir cómo poner cosas en la memoria. No se trata solo de los indicadores de Vtable.

Cuando estandarizaron C ++ en 98, tenían que predecir básicamente cómo las personas lo implementarían. Si bien tenían bastante experiencia en la implementación con varios sabores de C ++, no estaban seguros de las cosas. Entonces decidieron ser cautos: dar a los compiladores la mayor libertad posible.

Es por eso que la definición de POD en C ++ 98 es muy estricta. Dio a los compiladores de C ++ una gran latitud 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 específicamente escribiste por una razón.

Cuando se estaba trabajando en C ++ 11, tenían mucha más experiencia con los compiladores. Y se dieron cuenta de que ... los escritores de compiladores de C ++ son realmente flojos. Tenían toda esta libertad, pero no hicieron nada con eso.

Las reglas del diseño estándar son una práctica común más o menos codificadora: la mayoría de los compiladores en realidad no tenían que cambiar mucho o implementarlas en absoluto (fuera de tal vez algunas cosas para los rasgos de tipo correspondientes).

Ahora, cuando se trata de public / private , las cosas son diferentes. La libertad de reordenar qué miembros son public frente a private realmente puede importarle al compilador, particularmente en las compilaciones de depuración. Y dado que el punto del diseño estándar es que hay compatibilidad con otros lenguajes, no puede tener el diseño diferente en depuración frente a versión.

Luego está el hecho de que realmente no lastima al usuario. Si está haciendo una clase encapsulada, es probable que todos sus miembros de datos sean private todos modos. Por lo general, no expones miembros de datos públicos en tipos totalmente encapsulados. Así que esto solo sería un problema para los pocos usuarios que sí quieren hacer eso, que quieren esa división.

Entonces no es una gran pérdida.

b) solo una clase en el árbol de herencia completo puede tener miembros de datos no estáticos,

El motivo de esto vuelve a explicar por qué estandarizaron el diseño estándar nuevamente: 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 de otra manera. ¿De qué manera ordena a los miembros si provienen de dos clases base? Y así. Los compiladores difieren mucho en estas preguntas.

Además, gracias a la regla cero / uno / infinito, una vez que diga que puede tener dos clases con miembros, puede decir todas las que quiera. Esto requiere agregar muchas reglas de diseño sobre cómo manejar esto. Tienes que decir cómo funciona la herencia múltiple, qué clases ponen sus datos antes que otras clases, etc. Son muchas reglas, con muy poca ganancia material.

No puede hacer todo lo que no tiene funciones virtuales y un diseño estándar de constructor por defecto.

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 aliasing).

Realmente no puedo hablar de esto. No estoy lo suficientemente educado en las reglas de aliasing de C ++ para realmente entenderlo. Pero tiene algo que ver con el hecho de que el miembro base compartirá la misma dirección que la clase base. Es decir:

struct Base {};
struct Derived : Base { Base b; };

Derived d;
static_cast<Base*>(&d) == &d.b;

Y eso es probablemente en contra de las reglas de aliasing de C ++. De alguna manera.

Sin embargo, considere esto: ¿qué tan útil podría ser realmente la capacidad de hacer esto? Como solo una clase puede tener miembros de datos no estáticos, Derived debe ser esa clase (ya que tiene una Base como miembro). Entonces Base debe estar vacía (de datos). Y si Base está vacía, así como una clase base ... ¿por qué tener un miembro de datos en absoluto?

Como Base está vacía, no tiene estado. Entonces, cualquier función miembro no estática hará lo que haga basándose en sus parámetros, no en su puntero.

Entonces otra vez: no hay gran pérdida.




Related