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


548

Этот FAQ о Агрегатах и ​​POD и содержит следующие материалы:

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


Можно ли сказать, что мотивация этих определений примерно такая: POD == memcpy'able, Aggregate == aggregate-initializable?
Офек Шилон

Ответы:


572

Как читать:

Эта статья довольно длинная. Если вы хотите узнать об агрегатах и ​​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] = {a1, a2, …, am};

если (m == n)
i- й элемент массива инициализируется с помощью a 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}};

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


3
Хороший ответ. Комментарии: «Если конструктор по умолчанию определен неявно, то все нестатические члены инициализируются рекурсивно-значениями». и «Инициализация значения для неагрегированного класса может завершиться неудачей, если, например, у класса нет соответствующего конструктора по умолчанию». не правильно: Инициализация значения класса с неявно объявленным конструктором по умолчанию не требует неявно определенного конструктора по умолчанию. Таким образом, задано (вставьте private:соответствующим образом): struct A { int const a; };тогда A()правильно сформировано, даже если Aопределение конструктора по умолчанию будет некорректно сформировано.
Йоханнес Шауб -

4
@Kev: Если вам удастся упаковать ту же информацию в более короткий ответ, мы все с удовольствием проголосуем за нее!
СБИ

3
@Armen также обратите внимание, что вы можете сделать несколько ответов на один и тот же вопрос. Каждый ответ может содержать часть решения вопроса. Винт, что, по-моему, признанная вещь :)
Йоханнес Шауб -

3
Ответ отличный. Я до сих пор посещаю этот пост несколько раз. Кстати о предупреждениях для Visual Studio. «оператор goto» для pod приходит с незнанием компилятора MSVC, как вы упомянули. Но для оператора switch / case генерируется ошибка компиляции. Основываясь на этой концепции, я сделал несколько тестовых
модулей

2
В пуле, который начинается с «Время жизни объектов не-POD-класса начинается с завершения конструктора и заканчивается после завершения деструктора». последняя часть должна вместо этого сказать «когда деструктор запускается».
Quokka

458

Какие изменения для 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;

2
не могли бы вы разработать следующие правила: a) классы стандартной компоновки должны иметь все нестатические элементы данных с одинаковым контролем доступа; b) только один класс во всем дереве наследования может иметь нестатические элементы данных, а первый нестатический элемент данных не может иметь тип базового класса (это может нарушить правила псевдонимов). Особенно каковы причины для них? Для более позднего правила, можете ли вы привести пример прерывания псевдонимов?
Андрей Тиличко

@AndyT: Смотри мой ответ. Я пытался ответить, насколько мне известно.
Никол Болас

5
Возможно, потребуется обновить это для C ++ 14, в котором убрано требование «без скобок или равных инициализаторов» для агрегатов.
TC

@TC спасибо за хедз-ап. Я скоро посмотрю эти изменения и обновлю их.
Р. Мартиньо Фернандес

1
Что касается псевдонимов: существует правило компоновки C ++, которое гласит, что если класс C имеет (пустую) базовую X, а первый член данных C имеет тип X, то этот первый член не может иметь то же смещение, что и базовая X; он получает фиктивный байт заполнения перед ним, если необходимо избежать этого. Наличие двух экземпляров X (или подкласса) по одному и тому же адресу может сломать вещи, которые должны различать разные экземпляры по их адресам (пустому экземпляру больше нечего различать ...). В любом случае, необходимость вставлять в этот отступ байты прерывает «совместимость макета».
Грегго

106

Что изменилось для 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 ++ 14

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

Было три ДР:

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

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

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

Для этого в C ++ 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
Существует предложение разрешить агрегатам иметь базовый класс, если он является конструируемым по умолчанию, см. N4404
Шафик Ягмур

в то время как POD может остаться прежним, C ++ 14 StandardLayoutType, который является требованием для POD, изменился в соответствии с cppref: en.cppreference.com/w/cpp/named_req/StandardLayoutType
Ciro Santilli 法轮功 冠状 病 六四 事件 法轮功

1
@CiroSantilli 法轮功 改造 中心 六四 事件 法轮功 спасибо, я не знаю, как я пропустил это, я постараюсь обновить в течение следующих нескольких дней.
Шафик Ягмур

Дайте мне знать, если вы можете придумать пример, который является POD в C ++ 14, но не в C ++ 11 :-) Я начал подробный список примеров по адресу: stackoverflow.com/questions/146452/what- are-pod-types-in-c /…
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

1
@CiroSantilli 法轮功 改造 中心 六四 事件 法轮功, так что здесь произошло, если мы посмотрим на описание стандартной компоновки в C ++ 11 и C ++ 14, они совпадают. Эти изменения, где они были применены через дефекты, возвращаются в C ++ 14. Поэтому, когда я писал это изначально, это было правильно :-p
Шафик Ягмур

47

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

Я попытаюсь:

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

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

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

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

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

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

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

Теперь, когда дело дошло до public/ private, все по-другому. Свобода для изменения порядка , которые члены publicvs.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указателе.

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


Спасибо за объяснение, это очень помогает. Вероятно , несмотря на static_cast<Base*>(&d)и &d.bте же Base*типа, что указывает на разные вещи , таким образом , нарушая правила наложения спектров. Пожалуйста, поправьте меня.
Андрей Тыличко

1
и почему, если только один класс может иметь нестатические члены-данные, тогда Derivedэтот класс должен быть?
Андрей Тыличко

3
@AndyT: чтобы Derivedпервый член был его базовым классом, он должен иметь две вещи: базовый класс и член . А так как только один класс в иерархии может иметь членов (и при этом иметь стандартную компоновку), это означает, что его базовый класс не может иметь членов.
Николь Болас

3
@AndyT, да, по сути, вы правы, IME, насчет правила наложения имен. Два разных экземпляра одного типа должны иметь разные адреса памяти. (Это позволяет отслеживать идентичность объекта с помощью адресов памяти.) Базовый объект и первый производный член - это разные экземпляры, поэтому они должны иметь разные адреса, что вызывает добавление заполнения, влияющее на макет класса. Если бы они были разных типов, это не имело бы значения; объектам разных типов разрешается иметь один и тот же адрес (например, класс и его первый элемент данных).
Адам Петерсон

46

Изменения в 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<C>::value ? 'Y' : 'N')
    << " i1: " << c.i1 << " i2: " << c.i2
    << " j: " << c.j << " m.m: " << c.m.m << endl;

//stdout: is C aggregate?: Y, i1=1 i2=2 j=3 m.m=4
  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 является типом класса, не являющимся объединением, без no ( возможно унаследованные) нестатические члены-данные, множество 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.


Примечание. Я только что обновил свой ответ: стандартные дефекты изменений макета имеют статус CD4, что означает, что они фактически применяются к C ++ 14. Вот почему мой ответ не включал их, потому что это произошло после того, как я написал свой ответ.
Шафик Ягмур

Обратите внимание, я начал щедрость по этому вопросу.
Шафик Ягмур

Спасибо @ShafikYaghmour. Я проверю статус сообщений об ошибках и соответствующим образом изменю свой ответ.
ThomasMcLeod

@ShafikYaghmour, После некоторого обзора процесса C ++ 14, и мне кажется, что, хотя эти DRs были «приняты» на встрече Рапперс в июне 2014 года, язык с собрания Issaquah в феврале 2014 года стал тем, что стало C ++ 14. См. Isocpp.org/blog/2014/07/trip-report-summer-iso-c-meeting «в соответствии с правилами ISO мы официально не одобрили никаких изменений в рабочем документе C ++». Я что-то пропустил?
ThomasMcLeod

Они имеют статус «CD4», что означает, что они должны применяться в режиме C ++ 14.
Шафик Ягмур

14

Что меняется в

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

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

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

struct X {
    X() = delete;
};

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

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

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

в

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

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

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

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

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

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

В C ++ 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) агрегаты будут эффективно иметь синтезированные конструкторы, которые делают правильные вещи - и ведут себя как обычные конструкторы. Приведенный выше код будет скомпилирован как есть в C ++ 20.

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

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

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

Point p{1, 2}; // error

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

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

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


Несмотря на то, что я буду высказываться, добавление этого будет немного преждевременным, но я не знаю ничего значительного, что могло бы изменить это до того, как будет сделан C ++ 2x.
Шафик Ягмур

@ShafikYaghmour Да, наверное, слишком рано. Но, учитывая, что SD был крайним сроком для новых языковых функций, это единственные два в полете, о которых я знаю - в худшем случае я просто заблокирую удалить один из этих разделов позже? Я только что увидел вопрос, который был активен с щедростью, и подумал, что сейчас самое время принять участие, прежде чем я забуду.
Барри

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

@ShafikYaghmour Похоже, здесь ничего не изменится :)
Барри

Я надеюсь, что это будет обновлено сейчас, с C ++ 20 уже выпущен
AtAll
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.