Что такое Агрегаты и POD и как/почему они являются особенными?

Этот FAQ относится к агрегатам и POD и охватывает следующий материал:

  • Что такое Агрегаты?
  • Что такое POD (Обычные старые данные)?
  • Как они связаны?
  • Как и почему они особенные?
  • Какие изменения для С++ 11?

Ответ 1

Как читать:

Эта статья довольно длинная. Если вы хотите узнать об агрегатах и POD (простые старые данные), найдите время и прочитайте его. Если вас интересуют только агрегаты, прочитайте только первую часть. Если вас интересуют только POD, вы должны сначала прочитать определения, последствия и примеры агрегатов, а затем перейти к POD, но я все равно рекомендую прочитать первую часть целиком. Понятие агрегатов необходимо для определения POD. Если вы обнаружите какие-либо ошибки (даже незначительные, включая грамматику, стилистику, форматирование, синтаксис и т.д.), Пожалуйста, оставьте комментарий, я буду редактировать.

Этот ответ относится к C++ 03. Для других стандартов C++ см.:

Что такое агрегаты и почему они особенные

Формальное определение из стандарта C++ (C++ 03 8.5.1 §1):

Агрегат - это массив или класс (раздел 9) без объявленных пользователем конструкторов (12.1), без закрытых или защищенных нестатических элементов данных (пункт 11), без базовых классов (раздел 10) и без виртуальных функций (10.3).

Итак, хорошо, давайте разберем это определение. Прежде всего, любой массив является агрегатом. Класс также может быть совокупным, если... подождите! ничего не сказано о структурах или союзах, разве они не могут быть совокупностями? Да, они могут. В C++ термин class относится ко всем классам, структурам и объединениям. Таким образом, класс (или структура, или объединение) является совокупностью тогда и только тогда, когда он удовлетворяет критериям из приведенных выше определений. Что подразумевают эти критерии?

  • Это не означает, что агрегатный класс не может иметь конструкторов, фактически он может иметь конструктор по умолчанию и/или конструктор копирования, если они неявно объявлены компилятором, а не явно пользователем.

  • Нет частных или защищенных нестатических членов данных. Вы можете иметь столько приватных и защищенных функций-членов (но не конструкторов), сколько столько приватных или защищенных статических членов-данных и функций-членов, сколько вам нравится, и не нарушать правила для агрегатных классов.

  • Агрегатный класс может иметь объявленный пользователем/определенный пользователем оператор копирования и/или деструктор

  • Массив является агрегатом, даже если это массив неагрегированного типа класса.

Теперь давайте посмотрим на некоторые примеры:

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
};

Вы поняли идею. Теперь давайте посмотрим, как агрегаты особенные. Они, в отличие от неагрегированных классов, могут быть инициализированы с помощью фигурных скобок {}. Этот синтаксис инициализации обычно известен для массивов, и мы только что узнали, что это агрегаты. Итак, начнем с них.

Type array_name[n] = {a 1, a 2, …, a m };

если (m == n)
i- й элемент массива инициализируется с помощью i
иначе если (m <n)
первые m элементов массива инициализируются с помощью a 1, a 2 ,…, a m, а остальные n - m элементов, если возможно, инициализируются значением (объяснение термина см. ниже)
иначе, если (m> n)
компилятор выдаст ошибку
иначе (это тот случай, когда n вообще не указывается, как int a[] = {1, 2, 3};)
размер массива (n) предполагается равным m, поэтому int a[] = {1, 2, 3}; эквивалентно int a[3] = {1, 2, 3};

Когда объект скалярного типа (bool, int, char, double, указатели и т.д.) Инициализируется значением, это означает, что он инициализируется с 0 для этого типа (false для bool, 0.0 для double и т.д.). Когда объект класса с объявленным пользователем конструктором по умолчанию инициализируется значением, вызывается его конструктор по умолчанию. Если конструктор по умолчанию определен неявно, то все нестатические члены рекурсивно инициализируются значением. Это определение неточное и немного неправильное, но оно должно дать вам основную идею. Ссылка не может быть инициализирована значением. Инициализация значения для неагрегированного класса может завершиться неудачей, если, например, у класса нет соответствующего конструктора по умолчанию.

Примеры инициализации массива:

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
}

Теперь давайте посмотрим, как агрегатные классы могут быть инициализированы с помощью фигурных скобок. Примерно так же. Вместо элементов массива мы будем инициализировать нестатические элементы данных в порядке их появления в определении класса (все они по определению общедоступны). Если инициализаторов меньше, чем членов, остальные инициализируются значением. Если невозможно инициализировать значение одним из членов, которые не были явно инициализированы, мы получаем ошибку времени компиляции. Если инициализаторов больше, чем необходимо, мы также получаем ошибку во время компиляции.

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}};

В приведенном выше примере yc инициализируется с помощью 'a', yxi1 с 10, yxi2 с 20, yi[0] с 20, yi[1] с 30 и yf инициализируется значением, то есть инициализируется с 0.0. Защищенный статический элемент d вообще не инициализируется, поскольку он является static.

Совокупные союзы отличаются тем, что вы можете инициализировать только их первого члена скобками. Я думаю, что если вы достаточно продвинуты в C++, чтобы даже рассмотреть возможность использования союзов (их использование может быть очень опасным и должно быть тщательно продумано), вы можете сами найти правила для союзов в стандарте :).

Теперь, когда мы знаем, что особенного в агрегатах, давайте попробуем понять ограничения на классы; вот почему они там. Мы должны понимать, что инициализация для членов с фигурными скобками подразумевает, что класс является не чем иным, как суммой его членов. Если присутствует определяемый пользователем конструктор, это означает, что пользователю необходимо проделать дополнительную работу для инициализации членов, поэтому инициализация фигурной скобки будет неправильной. Если виртуальные функции присутствуют, это означает, что объекты этого класса имеют (в большинстве реализаций) указатель на так называемую vtable класса, который устанавливается в конструкторе, поэтому инициализация скобок будет недостаточной. Вы можете выяснить остальные ограничения аналогично упражнению :).

Так что хватит о агрегатах. Теперь мы можем определить более строгий набор типов, то есть POD

Что такое POD и почему они особенные

Формальное определение из стандарта C++ (C++ 03 9 §4):

POD-структура - это агрегатный класс, который не имеет нестатических членов-данных типа non-POD-struct, non-POD-union (или массива таких типов) или ссылки, и не имеет никакого пользовательского оператора назначения копирования и не имеет пользовательский деструктор. Аналогично, POD-объединение - это совокупное объединение, которое не имеет нестатических членов-данных типа non-POD-struct, non-POD-union (или массива таких типов) или ссылки, и не имеет пользовательского оператора назначения копирования и нет пользовательского деструктора. POD-класс - это класс, который является POD-структурой или POD-объединением.

Вау, это сложнее разобрать, не так ли? :) Давайте оставим союзы (на тех же основаниях, что и выше) и перефразируем немного яснее:

Агрегатный класс называется POD, если у него нет определяемого пользователем оператора и деструктора копирования, и ни один из его нестатических членов не является классом без POD, массивом без POD или ссылкой.

Что означает это определение? (Я упоминал, что POD обозначает Обычные Старые Данные?)

  • Все классы POD являются агрегатами, или, другими словами, если класс не является агрегатом, то он, безусловно, не POD
  • Классы, как и структуры, могут быть POD, даже если в обоих случаях стандартным термином является POD-структура.
  • Как и в случае агрегатов, не имеет значения, какие статические члены в классе

Примеры:

struct POD
{
  int x;
  char y;
  void f() {} //no harm if there 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
};

POD-классы, POD-объединения, скалярные типы и массивы таких типов вместе называются POD-типами.
POD являются особенными во многих отношениях. Я приведу только несколько примеров.

  • POD-классы наиболее близки к C-структурам. В отличие от них, POD могут иметь функции-члены и произвольные статические члены, но ни один из этих двух не изменяет структуру памяти объекта. Поэтому, если вы хотите написать более или менее переносимую динамическую библиотеку, которую можно использовать из C и даже .NET, вы должны попытаться заставить все экспортируемые функции принимать и возвращать только параметры POD-типов.

  • Время жизни объектов класса не POD начинается, когда конструктор завершается, и заканчивается, когда деструктор завершает работу. Для классов POD время жизни начинается, когда хранилище для объекта занято, и заканчивается, когда это хранилище освобождается или используется повторно.

  • Для объектов типов POD по стандарту гарантируется, что когда вы memcpy содержимое вашего объекта в массив char или unsigned char, а затем memcpy содержимое обратно в ваш объект, объект будет сохранять свое первоначальное значение. Обратите внимание, что нет такой гарантии для объектов не POD-типов. Также вы можете безопасно копировать POD-объекты с помощью memcpy. В следующем примере предполагается, что T является 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
    
  • Перейти к заявлению. Как вы, возможно, знаете, это незаконно (компилятор должен выдать ошибку), чтобы выполнить переход через goto из точки, где некоторая переменная еще не была в области видимости, до точки, где она уже находится в области видимости. Это ограничение применяется только в том случае, если переменная имеет тип не POD. В следующем примере f() плохо сформирован, тогда как g() хорошо сформирован. Обратите внимание, что компилятор Microsoft слишком либерален с этим правилом - он просто выдает предупреждение в обоих случаях.

    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;
    }
    
  • Гарантируется, что в начале объекта POD не будет заполнения. Другими словами, если первый член POD-класса A имеет тип T, вы можете безопасно reinterpret_cast от A* до T* и получить указатель на первый член и наоборот.

У этого списка нет конца…

Заключение

Важно понимать, что такое POD, потому что многие языковые функции, как вы видите, ведут себя по-разному для них.

Ответ 2

Какие изменения для С++ 11?

Заполнители

Стандартное определение агрегата немного изменилось, но оно все же почти одинаково:

Агрегат - это массив или класс (раздел 9) без конструкторов, предоставляемых пользователем (12.1), нет элементов привязки или равных инициализаторов для нестатических элементов данных (9.2), нет личных или защищенных нестатические члены данных (раздел 11), базовые классы (раздел 10) и виртуальные функции (10.3).

Хорошо, что изменилось?

  • Ранее агрегат не мог иметь никаких объявленных пользователем конструкторов, но теперь он не может иметь созданных пользователем конструкторов. Есть ли разница? Да, есть, потому что теперь вы можете объявить конструкторы и по умолчанию:

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

    Это все еще агрегат, потому что конструктор (или любая специальная функция-член), который по умолчанию используется в первом объявлении, не предоставляется пользователям.

  • Теперь у агрегата не может быть каких-либо выравнивающих или равных инициализаторов для нестатических членов данных. Что это значит? Ну, это только потому, что с помощью этого нового стандарта мы можем инициализировать членов непосредственно в классе следующим образом:

    struct NotAggregate {
        int x = 5; // valid in C++11
        std::vector<int> s{1,2,3}; // also valid
    };
    

    Использование этой функции делает класс более не агрегированным, потому что он в основном эквивалентен предоставлению собственного конструктора по умолчанию.

Итак, что такое совокупность, совсем не изменилось. Это по-прежнему та же основная идея, адаптированная к новым функциям.

Как насчет POD?

POD прошли множество изменений. Многие новые правила о POD были ослаблены в этом новом стандарте, и способ определения в стандарте был радикально изменен.

Идея POD состоит в том, чтобы зафиксировать в основном два разных свойства:

  • Он поддерживает статическую инициализацию и
  • Компиляция POD в С++ дает вам тот же макет памяти, что и структура, скомпилированная в C.

Из-за этого определение было разделено на два разных понятия: тривиальные классы и классы стандартного макета, потому что они более полезны, чем POD. Стандарт теперь редко использует термин POD, предпочитая более конкретные тривиальные и стандартные макеты.

Новое определение в основном говорит о том, что POD - это класс, который является тривиальным и имеет стандартный макет, и это свойство должно содержать рекурсивно для всех нестатических членов данных:

Структура POD - это неединичный класс, который является как тривиальным классом, так и стандартным классом макета, и не имеет нестатических членов данных типа non-POD struct, non-POD union (или массив таких типов). Аналогичным образом, объединение POD представляет собой объединение, которое является тривиальным классом и стандартным классом макета, и имеет не нестатические члены данных типа non-POD struct, не-POD union (или массив таких типов). Класс POD - это класс, который представляет собой либо структуру POD, либо объединение POD.

Перейдем к каждому из этих двух свойств отдельно.

Тривиальные классы

Тривиальным является первое свойство, упомянутое выше: тривиальные классы поддерживают статическую инициализацию. Если класс тривиально-копируемый (надмножество тривиальных классов), можно скопировать его представление по месту такими вещами, как memcpy, и ожидать, что результат будет таким же.

Стандарт определяет тривиальный класс следующим образом:

Тривиально-скопируемый класс - это класс, который:

- нет нетривиальных конструкторов копирования (12.8),

- нет нетривиальных конструкторов перемещения (12.8),

- нет нетривиальных операторов присваивания копий (13.5.3, 12.8),

- нет нетривиальных операторов присваивания перемещения (13.5.3, 12.8) и

- имеет тривиальный деструктор (12.4).

Тривиальный класс - это класс, имеющий тривиальный конструктор по умолчанию (12.1) и тривиально копируемый.

[Примечание. В частности, тривиально-копируемый или тривиальный класс не имеет виртуальных функций или виртуальных базовых классов.-end note]

Итак, каковы все эти тривиальные и нетривиальные вещи?

Конструктор копирования/перемещения для класса X тривиален, если он не предоставляется пользователем и если

- класс X не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1) и

- конструктор, выбранный для копирования/перемещения каждого подобъекта прямого базового класса, тривиален и

- для каждого нестатического элемента данных X, относящегося к типу класса (или его массиву), конструктор выбранный для копирования/перемещения этого элемента тривиально,

в противном случае конструктор copy/move не является тривиальным.

В основном это означает, что конструктор копирования или перемещения тривиален, если он не предоставляется пользователям, класс не имеет ничего виртуального в нем, и это свойство выполняется рекурсивно для всех членов класса и базового класса.

Определение тривиального оператора присваивания копирования/перемещения очень похоже, просто заменив слово "конструктор" на "оператор присваивания".

Тривиальный деструктор также имеет аналогичное определение с добавленным ограничением, что он не может быть виртуальным.

И еще одно подобное правило существует для тривиальных конструкторов по умолчанию, с добавлением, что конструктор по умолчанию не является тривиальным, если класс имеет нестатические элементы данных с элементами скрепления или равенства, которые мы видели выше.

Вот несколько примеров, чтобы очистить все:

// 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 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
};

Стандарт-макет

Стандартная макет - это второе свойство. В стандарте упоминается, что они полезны для общения с другими языками и потому, что класс стандартного макета имеет тот же макет памяти эквивалентной структуры или объединения C.

Это другое свойство, которое должно содержать рекурсивно для членов и всех базовых классов. И, как обычно, виртуальные функции или виртуальные базовые классы не допускаются. Это сделало бы макет несовместимым с C.

В этом случае расслабленное правило состоит в том, что классы стандартного макета должны иметь все нестатические элементы данных с одним и тем же контролем доступа. Раньше они должны были быть общедоступными, но теперь вы можете сделать их частными или защищенными, если они все закрыты или защищены.

При использовании наследования только один класс во всем дереве наследования может иметь нестатические элементы данных, а первый нестатический член данных не может быть типа базового класса (это может нарушить правила псевдонимов), в противном случае оно не будет класс стандартного макета.

Это определение содержится в стандартном тексте:

Класс стандартного макета - это класс, который:

- не содержит нестатических элементов данных класса нестандартного макета (или массива таких типов) или ссылку,

- не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1),

- имеет тот же контроль доступа (раздел 11) для всех нестатических членов данных,

- не имеет базовых классов нестандартной компоновки,

- либо не имеет нестатических элементов данных в самом производном классе и не более одного базового класса с нестатические члены данных или не имеет базовых классов с нестатическими элементами данных и

- не имеет базовых классов того же типа, что и первый нестатический элемент данных.

Структура стандартного макета - это класс стандартного макета, определенный с помощью структуры класса или структуры класс класса.

Стандартное соединение макета - это класс стандартного макета, определенный с помощью объединения классов классов.

[Примечание. Стандартные классы макета полезны для общения с кодом, написанным на других языках программирования. Их макет указан в примечании 9.2.-end]

И рассмотрим несколько примеров.

// empty classes have standard-layout
struct StandardLayout1 {};

struct StandardLayout2 {
    int x;
};

struct StandardLayout3 {
private: // both are private, so it 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

Заключение

С этими новыми правилами намного больше типов могут быть POD сейчас. И даже если тип не POD, мы можем использовать некоторые из свойств POD отдельно (если это только один из тривиальных или стандартных макетов).

Стандартная библиотека имеет свойства для проверки этих свойств в заголовке <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;
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;

Ответ 3

Что изменилось для С++ 14

Мы можем обратиться к проекту стандарта С++ 14 для справки.

сводные показатели

Это описано в разделе 8.5.1 Агрегаты, который дает нам следующее определение:

Агрегат - это массив или класс (раздел 9) без предоставленных пользователем конструкторов (12.1), без закрытых или защищенных нестатических элементов данных (пункт 11), без базовых классов (пункт 10) и без виртуальных функций (10.3).

Единственное изменение - добавление инициализаторов членов класса не делает класс неагрегированным. Итак, следующий пример из С++ 11 агрегатной инициализации для классов с инициализированными членами-инициализаторами:

struct A
{
  int a = 3;
  int b = 3;
};

не был агрегатом в С++ 11, но в С++ 14. Это изменение описано в N3605: Инициализаторы и агрегаты элементов, которые имеют следующее резюме:

Бьярн Страуструп и Ричард Смит подняли вопрос о том, что инициализация агрегатов и инициализаторы членов не работают вместе. В этой статье предлагается решить эту проблему, приняв предложенную Смитом формулировку, которая снимает ограничение, согласно которому агрегаты не могут иметь инициализаторы элементов.

POD остается прежним

Определение структуры POD (обычные старые данные) рассматривается в разделе 9 Классы, в котором говорится:

Структура 110 POD является классом, не являющимся объединением, который является одновременно тривиальным классом и классом стандартной компоновки и не имеет нестатических членов-данных типа не-POD-структуры, не-POD-объединения (или массива таких типов). Аналогично, объединение POD - это объединение, которое является как тривиальным классом, так и классом стандартной компоновки и не имеет нестатических членов-данных типа non-POD struct, non-POD union (или массива таких типов). Класс POD - это класс, который является либо структурой POD, либо объединением POD.

это та же формулировка, что и в С++ 11.

Изменения стандартного макета для С++ 14

Как отмечалось в комментариях, модуль опирается на определение стандартного макета, и это изменилось для С++ 14, но это было с помощью отчетов о дефектах, которые были применены к С++ 14 после факта.

Было три ДР:

Итак, стандартная раскладка пошла из этого Pre С++ 14:

Класс стандартного макета - это класс, который:

  • (7.1) не имеет нестатических членов-данных типа нестандартного класса макета (или массива таких типов) или ссылки,
  • (7.2) не имеет виртуальных функций ([class.virtual]) и виртуальных базовых классов ([class.mi]),
  • (7.3) имеет одинаковый контроль доступа (пункт [class.access]) для всех нестатических элементов данных,
  • (7.4) не имеет базовых классов нестандартной компоновки,
  • (7.5) либо не имеет нестатических членов данных в самом производном классе и не более одного базового класса с нестатическими членами данных, или не имеет базовых классов с нестатическими членами данных, и
  • (7.6) не имеет базовых классов того же типа, что и первый элемент нестатических данных .109

Для этого в С++ 14:

Класс S является классом стандартной компоновки, если он:

  • (3.1) не имеет нестатических членов-данных типа нестандартного класса макета (или массива таких типов) или ссылки,
  • (3.2) не имеет виртуальных функций и виртуальных базовых классов,
  • (3.3) имеет одинаковый контроль доступа для всех нестатических элементов данных,
  • (3.4) не имеет нестандартных макетов базовых классов,
  • (3.5) имеет не более одного подобъекта базового класса любого данного типа,
  • (3.6) имеет все нестатические члены-данные и битовые поля в классе, а его базовые классы сначала объявлены в одном и том же классе, и
  • (3.7) не имеет элемента набора типов M (S) в качестве базового класса, где для любого типа X M (X) определяется следующим образом .104 [Примечание: M (X) - это набор типов все подобъекты не базового класса, которые могут иметь нулевое смещение в X. - конец примечания]
    • (3.7.1) Если X является типом класса без объединения, в котором нет (возможно, унаследованных) нестатических членов-данных, множество M (X) пусто.
    • (3.7.2) Если X - это тип класса, не являющийся объединением, с нестатическим членом данных типа X0, который имеет нулевой размер или является первым нестатическим элементом данных в X (где указанный член может быть анонимным объединением), множество M (X) состоит из X0 и элементов из M (X0).
    • (3.7.3) Если X является типом объединения, множество M (X) является объединением всех M (Ui) и набора, содержащего все Ui, где каждый Ui является типом i-го элемента не статических данных X,
    • (3.7.4) Если X является типом массива с типом элемента Xe, множество M (X) состоит из Xe и элементов M (Xe).
    • (3.7.5) Если X не класс, не массив типа, множество M (X) пусто.

Ответ 4

Вы можете продумать следующие правила:

Я попробую:

a) классы стандартного макета должны иметь все нестатические элементы данных с одинаковым контролем доступа

Это просто: все нестатические члены данных должны быть public, private или protected. Вы не можете иметь некоторые public и некоторые private.

Причиной для них является рассуждение о том, чтобы различать "стандартную компоновку" и "не стандартную компоновку" вообще. А именно, дать компилятору свободу выбора, как поместить вещи в память. Это не только ссылки на vtable.

Назад, когда они стандартизировали С++ в 98, они должны были в основном предсказать, как люди будут его реализовывать. Хотя у них было довольно много опыта внедрения с различными вкусами С++, они не были уверены в вещах. Поэтому они решили быть осторожными: дать компиляторам как можно больше свободы.

Вот почему определение POD на С++ 98 настолько строгое. Это дало компиляторам С++ большую широту в макете участника для большинства классов. В принципе, типы POD были предназначены для особых случаев, что вы специально писали по какой-то причине.

Когда С++ 11 работал, у них было много опыта работы с компиляторами. И они поняли, что... С++ компиляторы очень ленивы. У них была вся эта свобода, но они ничего не делали с ней.

Правила стандартного макета более или менее кодируют обычную практику: большинству компиляторов действительно не нужно сильно менять, если что-либо вообще реализовать их (за пределами, возможно, некоторых вещей для соответствующих типов типов).

Теперь, когда дело дошло до public/private, все по-другому. Свобода переупорядочивать, какие члены public vs. private на самом деле может иметь значение для компилятора, особенно в отладочных сборках. И поскольку точка стандартного макета заключается в совместимости с другими языками, вы не можете иметь разную компоновку в debug vs. release.

Тогда возникает тот факт, что это не повредит пользователю. Если вы делаете инкапсулированный класс, вероятность того, что все ваши члены данных будут private в любом случае. Как правило, вы не публикуете публичные данные на полностью инкапсулированных типах. Таким образом, это будет проблемой только для тех немногих пользователей, которые хотят это сделать, кто хочет это разделение.

Так что это не большая потеря.

b) только один класс во всем дереве наследования может иметь нестатические члены данных,

Причина этого возвращается к тому, почему они снова стандартизировали стандартный макет: обычная практика.

Нет обычной практики, когда речь идет о наличии двух элементов дерева наследования, которые фактически хранят вещи. Некоторые кладут базовый класс перед производным, другие делают это по-другому. В каком порядке вы заказываете участников, если они происходят из двух базовых классов? И так далее. Компиляторы сильно расходятся по этим вопросам.

Кроме того, благодаря правилу zero/one/infinity, как только вы скажете, что у вас есть два класса с членами, вы можете сказать столько, сколько хотите. Это требует добавления большого количества правил компоновки для того, как справиться с этим. Вы должны сказать, как многократное наследование работает, какие классы помещают свои данные перед другими классами и т.д. Это много правил, при очень небольшом материальном выигрыше.

Вы не можете сделать все, что не имеет виртуальных функций, и стандартный макет конструктора по умолчанию.

и первый нестатический элемент данных не может быть типа базового класса (это может нарушить правила псевдонимов).

Я не могу разговаривать с этим. Я недостаточно образован в правилах псевдонимов С++, чтобы действительно понять это. Но это как-то связано с тем, что базовый член будет иметь тот же адрес, что и базовый класс. То есть:

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

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

И это, вероятно, против правил псевдонимов С++. В некотором роде.

Однако учтите это: насколько полезной могла быть способность делать это когда-либо на самом деле? Поскольку только один класс может иметь нестатические элементы данных, тогда Derived должен быть этим классом (поскольку он имеет Base как член). Поэтому Base должен быть пустым (данных). И если Base пуст, а также базовый класс... зачем вообще его член данных?

Так как Base пуст, он не имеет состояния. Таким образом, любые нестатические функции-члены будут делать то, что они делают, основываясь на своих параметрах, а не на указателе this.

Так снова: никаких больших потерь.

Ответ 5

Изменения в C++ 17

Загрузите окончательный вариант C++ 17 Международного стандарта здесь.

сводные показатели

C++ 17 расширяет и улучшает агрегаты и инициализацию агрегатов. Стандартная библиотека также теперь включает класс черты типа std::is_aggregate. Вот формальное определение из разделов 11.6.1.1 и 11.6.1.2 (исключены внутренние ссылки):

Агрегат - это массив или класс с
- нет пользовательских, явных или унаследованных конструкторов,
- нет личных или защищенных нестатических данных членов,
- нет виртуальных функций, и
- нет виртуальных, частных или защищенных базовых классов.
[Примечание: Агрегированная инициализация не позволяет получить доступ к защищенным и закрытым членам или конструкторам базового класса. —Конечная записка]
Элементы совокупности:
- для массива элементы массива в порядке возрастания индекса или
- для класса - прямые базовые классы в порядке объявления, за которыми следуют прямые нестатические члены данных, которые не являются членами анонимного объединения, в порядке объявления.

Что изменилось?

  1. Агрегаты теперь могут иметь публичные, не виртуальные базовые классы. Кроме того, не требуется, чтобы базовые классы были агрегатами. Если они не являются агрегатами, они инициализируются списком.
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
    &lt&lt "is C aggregate?: " &lt&lt (std::is_aggregate&ltC&gt::value ? 'Y' : 'N')
    &lt&lt " i1: " &lt&lt c.i1 &lt&lt " i2: " &lt&lt c.i2
    &lt&lt " j: " &lt&lt c.j &lt&lt " m.m: " &lt&lt c.m.m &lt&lt endl;

//stdout: is C aggregate?: Y, i1=1 i2=2 j=3 m.m=4
  1. Явные конструкторы по умолчанию запрещены
struct D // not an aggregate
{
    int i = 0;
    D() = default;
    explicit D(D const&) = default;
};
  1. Наследование конструкторов запрещено
struct B1
{
    int i1;
    B1() : i1(0) { }
};
struct C : B1 // not an aggregate
{
    using B1::B1;
};


Тривиальные Классы

Определение тривиального класса было переработано в C++ 17 для устранения нескольких дефектов, которые не были рассмотрены в C++ 14. Изменения носили технический характер. Вот новое определение в 12.0.6 (внутренние ссылки исключены):

Тривиально копируемый класс - это класс:
- где каждый конструктор копирования, конструктор перемещения, оператор назначения копирования и оператор назначения перемещения либо удалены, либо тривиальны,
- который имеет по крайней мере один не удаленный конструктор копирования, конструктор перемещения, оператор назначения копирования или оператор назначения перемещения, и
- это имеет тривиальный, не удаленный деструктор.
Тривиальный класс - это класс, который является тривиально копируемым и имеет один или несколько конструкторов по умолчанию, каждый из которых либо тривиален, либо удален, и по крайней мере один из которых не удален. [Примечание: В частности, тривиально копируемый или тривиальный класс не имеет виртуальных функций или виртуальных базовых классов.

Изменения:

  1. Согласно C++ 14, для того, чтобы класс был тривиальным, в классе не должно быть никаких операторов копирования/перемещения/назначения, которые были бы нетривиальными. Однако тогда неявно объявленный как конструктор/оператор по умолчанию может быть нетривиальным и все же определенным как удаленный, потому что, например, класс содержит подобъект типа класса, который не может быть скопирован/перемещен. Наличие такого нетривиального конструктора/оператора, определенного как удаленный, приведет к тому, что весь класс будет нетривиальным. Аналогичная проблема существовала с деструкторами. C++ 17 поясняет, что наличие такого конструктора/операторов не приводит к тому, что класс будет нетривиально копируемым, а следовательно, нетривиальным, и что тривиально копируемый класс должен иметь тривиальный не удаляемый деструктор. DR1734, DR1928
  2. C++ 14 позволил тривиально копируемому классу, а значит, тривиальному классу, иметь каждый оператор конструктора/присваивания копирования/перемещения, объявленный как удаленный. Если, например, class также является стандартным макетом, его можно легально скопировать/переместить с помощью std::memcpy. Это было семантическим противоречием, потому что, определяя как удаленные все операторы конструктора/присваивания, создатель класса явно предполагал, что класс не может быть скопирован/перемещен, однако класс все еще удовлетворяет определению тривиально копируемого класса. Следовательно, в C++ 17 у нас есть новое предложение о том, что тривиально копируемый класс должен иметь хотя бы один тривиальный, не удаленный (хотя и не обязательно общедоступный) оператор конструктора/присваивания копирования/перемещения. См. N4148, DR1734
  3. Третье техническое изменение касается аналогичной проблемы с конструкторами по умолчанию. Согласно C++ 14, класс может иметь тривиальные конструкторы по умолчанию, которые неявно определены как удаленные, но все же остаются тривиальным классом. Новое определение поясняет, что у тривиального класса должен быть хотя бы один тривиальный не удаленный конструктор по умолчанию. См DR1496

Стандартные классы

Определение стандартного макета также было переработано с учетом отчетов о дефектах. Опять же изменения носили технический характер. Вот текст из стандарта (12.0.7). Как и прежде, внутренние ссылки исключены:

Класс S является классом стандартной компоновки, если он:
- не имеет нестатических членов-данных типа нестандартного класса макета (или массива таких типов) или ссылки,
- не имеет виртуальных функций и виртуальных базовых классов,
- имеет одинаковый контроль доступа для всех нестатических элементов данных,
- не имеет нестандартных макетов базовых классов,
- имеет не более одного подобъекта базового класса любого данного типа,
- имеет все нестатические члены-данные и битовые поля в классе и его базовые классы, впервые объявленные в том же классе, и
- не имеет элемента набора M (S) типов (определенных ниже) в качестве базового класса .108
M (X) определяется следующим образом:
- Если X является типом класса, не являющимся объединением, без (возможно, унаследованных) нестатических членов-данных, набор M (X) пуст.
- Если X является типом класса, не являющимся объединением, первый нестатический член данных которого имеет тип X0 (где указанный член может быть анонимным объединением), множество M (X) состоит из X0 и элементов M (X0).
- Если X является типом объединения, множество M (X) является объединением всех M (Ui) и набора, содержащего все Ui, где каждый Ui является типом i-го элемента нестатических данных X.
- Если X является типом массива с типом элемента Xe, множество M (X) состоит из Xe и элементов M (Xe).
- Если X не класс, не массив типа, множество M (X) пусто.
[Примечание: M (X) - это набор типов всех подобъектов не базового класса, для которых в классе стандартной компоновки гарантируется нулевое смещение в X. - конец примечания]
[ Пример:

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) Это гарантирует, что два подобъекта, которые имеют один и тот же тип класса и принадлежат к одному и тому же самому производному объекту, не будут размещены по одному и тому же адресу.

Изменения:

  1. Уточнено, что требование о том, что только один класс в дереве деривации "имеет" нестатические элементы данных, относится к классу, в котором такие элементы данных впервые объявлены, а не к классам, где они могут быть унаследованы, и расширило это требование до нестатических битовых полей, Также поясняется, что класс стандартной компоновки "имеет не более одного подобъекта базового класса любого данного типа". Смотри DR1813, DR1881
  2. Определение стандартного макета никогда не позволяло типу любого базового класса быть того же типа, что и первый нестатический элемент данных. Это сделано для того, чтобы избежать ситуации, когда элемент данных с нулевым смещением имеет тот же тип, что и любой базовый класс. Стандарт C++ 17 предоставляет более строгое, рекурсивное определение "набора типов всех подобъектов не базового класса, которые гарантированно находятся в классе стандартной компоновки с нулевым смещением", чтобы запретить такие типы от того, чтобы быть типом любого базового класса. Смотри DR1672, DR2120.

Примечание. Комитет по стандартам C++ предполагал, что вышеуказанные изменения, основанные на отчетах о дефектах, будут применяться к C++ 14, хотя новый язык отсутствует в опубликованном стандарте C++ 14. Это в стандарте C++ 17.

Ответ 6

Что изменится для С++ 20

Это еще рано, поэтому некоторые ответы могут измениться в будущем. Следуя остальной ясной теме этого вопроса, значение и использование агрегатов продолжает меняться с каждым стандартом. На горизонте есть несколько ключевых изменений.

Типы с конструкторами, объявленными пользователем P1008

В С++ 17 этот тип все еще является агрегатом:

struct X {
    X() = delete;
};

И, следовательно, X{} все еще компилируется, потому что это агрегатная инициализация, а не вызов конструктора. Смотрите также: Когда приватный конструктор не приватный конструктор?

В С++ 20 ограничение изменится с требования:

нет пользовательских, explicit или унаследованных конструкторов

в

нет пользовательских или унаследованных конструкторов

Это было принято в рабочий проект С++ 20. Ни X здесь, ни C в связанном вопросе не будут агрегатами в С++ 20.

Это также обеспечивает эффект йо-йо в следующем примере:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

В С++ 11/14, B не был агрегатом из-за базового класса, поэтому B{} выполняет инициализацию значения, которая вызывает B::B() которая вызывает A::A(), в точке, где это доступны. Это было правильно сформировано.

В С++ 17 B стал агрегатом, потому что были разрешены базовые классы, что сделало B{} агрегатной инициализацией. Это требует инициализации copy-list A от {}, но вне контекста B, где он недоступен. В С++ 17 это плохо сформировано (auto x = B(); хотя все будет в порядке).

В С++ 20 теперь, из-за вышеуказанного изменения правила, B снова перестает быть агрегатом (не из-за базового класса, а из-за объявленного пользователем конструктора по умолчанию - даже если он по умолчанию). Итак, мы вернулись к рассмотрению конструктора B И этот фрагмент становится правильно сформированным.

Инициализация агрегатов из заключенного в скобки списка значений P960

Распространенной проблемой является желание использовать конструкторы emplace() -style с агрегатами:

struct X { int a, b; };
std::vector<X> xs;
xs.emplace_back(1, 2); // error

Это не работает, потому что emplace попытается эффективно выполнить инициализацию X(1, 2), что недопустимо. Типичным решением является добавление конструктора к X, но с этим предложением (в настоящее время работающим через Core) агрегаты будут эффективно иметь синтезированные конструкторы, которые делают правильные вещи - и ведут себя как обычные конструкторы. Приведенный выше код будет скомпилирован как есть в С++ 20 (при условии, что эта функция будет одобрена, что кажется вероятным).

Удержание аргумента шаблона класса (CTAD) для агрегатов P1021

В С++ 17 это не компилируется:

template <typename T>
struct Point {
    T x, y;
};

Point p{1, 2}; // error

Пользователи должны будут написать свое собственное руководство по выводу для всех совокупных шаблонов:

template <typename T> Point(T, T) -> Point<T>;

Но поскольку это в некотором смысле "очевидная вещь", и в основном это просто шаблон, язык сделает это за вас. Это изменение было одобрено Evolution в ноябре 2018 года, поэтому приведенный выше пример, скорее всего, скомпилируется в С++ 20 (без необходимости предоставления руководства по выводам, предоставленного пользователем).