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

Этот FAQ посвящен Агрегатам и POD и содержит следующие материалы:

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

6 ответов

Как читать:

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

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

Формальное определение из стандарта 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] = {a1, a2, …, am};

если (m == n)
i-й элемент массива инициализируется с помощьюi
иначе если (m
первые m элементов массива инициализируются с помощью1, a2,…, am и других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}};

В приведенном выше примере y.c инициализируется с 'a', y.x.i1 с 10, y.x.i2 с 20, y.i[0] с 20, y.i[1] с 30 а также y.f инициализируется значением, то есть инициализируется 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'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
};

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, потому что многие языковые функции, как вы видите, ведут себя по-разному для них.

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

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

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

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

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

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

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

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

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

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

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

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

А как насчет POD?

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

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

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

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

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

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

Давайте подробно рассмотрим каждое из этих двух свойств.

Тривиальные занятия

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

в противном случае конструктор копирования / перемещения нетривиален.

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

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

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

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

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

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

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

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

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

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

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

Вот как идет определение в стандартном тексте:

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

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

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

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

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

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

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

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

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

[ Примечание: классы стандартной компоновки полезны для связи с кодом, написанным на других языках программирования. Их расположение указано в 9.2. —Конечная записка ]

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

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

Заключение

Благодаря этим новым правилам 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;

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

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

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

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

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

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

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

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

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

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

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

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

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

Изменения в 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
    << "is C aggregate?: " << (std::is_aggregate::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
  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 разрешил тривиально копируемому классу, а значит, тривиальному классу, иметь каждый конструктор / оператор копирования / перемещения, объявленный как удаленный. Если, например, класс также является стандартным макетом, его можно, однако, легально скопировать / переместить с 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.

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

Я буду стараться:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

И это, вероятно, противоречит правилам алиасинга в C++. Каким-то образом.

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

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

Итак, еще раз: нет большой потери.

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

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

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

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

struct X {
    X() = delete;
};

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

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

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

в

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

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

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

Общая проблема, которая возникает, это желание использовать emplace()в стиле конструкторов с агрегатами:

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

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

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

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

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

Point p{1, 2}; // error

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

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

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

Другие вопросы по тегам