C ++ 11 позволяет в классе инициализировать нестатические и неконстантные члены. Что изменилось?

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

class Y {
const int c3 = 7;           // error: not static
static int c4 = 7;          // error: not const
static const float c5 = 7;  // error: not integral
};

И следующие рассуждения:

Так почему же существуют эти неудобные ограничения? Класс обычно объявляется в файле заголовка, а файл заголовка обычно включается во многие единицы перевода. Однако, чтобы избежать сложных правил компоновщика, C ++ требует, чтобы у каждого объекта было уникальное определение. Это правило было бы нарушено, если бы C ++ допускал определение сущностей в классе, которые должны были храниться в памяти как объекты.

Однако C ++ 11 ослабляет эти ограничения, разрешая инициализацию в классе нестатических членов (§12.6.2 / 8):

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

  • если объект является нестатическим членом данных, который имеет скобки или равно-инициализатор, объект инициализируется, как указано в 8.5;
  • в противном случае, если объект является вариантом члена (9.5), инициализация не выполняется;
  • в противном случае объект инициализируется по умолчанию (8.5).

Раздел 9.4.2 также разрешает инициализацию в классе неконстантных статических членов, если они помечены constexpr спецификатор.

Так что же случилось с причинами ограничений, которые были у нас в C ++ 03? Должны ли мы просто принять «сложные правила компоновщика» или что-то еще изменилось, что облегчает реализацию?

70

Решение

Короткий ответ: они сохранили компоновщик примерно таким же, за счет того, что компилятор стал еще сложнее, чем раньше.

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

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

class X {
int a = 1234;
public:
X() = default;
X(int z) : a(z) {}
};

Теперь дополнительные правила в этой точке имеют дело с тем, какое значение используется для инициализации a когда вы используете конструктор не по умолчанию. Ответ на этот вопрос довольно прост: если вы используете конструктор, который не указывает никакого другого значения, тогда 1234 будет использоваться для инициализации a — но если вы используете конструктор, который задает какое-то другое значение, то 1234 в основном игнорируется.

Например:

#include <iostream>

class X {
int a = 1234;
public:
X() = default;
X(int z) : a(z) {}

friend std::ostream &operator<<(std::ostream &os, X const &x) {
return os << x.a;
}
};

int main() {
X x;
X y{5678};

std::cout << x << "\n" << y;
return 0;
}

Результат:

1234
5678
57

Другие решения

Я думаю, что аргументация могла быть написана до того, как шаблоны были доработаны. В конце концов, «сложные правила компоновщика», необходимые для инициализаторов в классе статических членов, были / были необходимы C ++ 11 для поддержки статических членов шаблонов.

Рассматривать

struct A { static int s = ::ComputeSomething(); }; // NOTE: This isn't even allowed,
// thanks @Kapil for pointing that out

// vs.

template <class T>
struct B { static int s; }

template <class T>
int B<T>::s = ::ComputeSomething();

// or

template <class T>
void Foo()
{
static int s = ::ComputeSomething();
s++;
std::cout << s << "\n";
}

Проблема для компилятора одинакова во всех трех случаях: в каком блоке перевода он должен давать определение s и код, необходимый для его инициализации? Простое решение — испустить его везде и позволить компоновщику разобраться. Вот почему линкеры уже поддерживали такие вещи, как __declspec(selectany), Просто было бы невозможно реализовать C ++ 03 без него. И именно поэтому не было необходимости расширять компоновщик.

Проще говоря: я думаю, что аргументация, приведенная в старом стандарте, просто неверна.


ОБНОВИТЬ

Как отметил Капил, мой первый пример даже не разрешен в текущем стандарте (C ++ 14). Я все равно оставил его, потому что это IMO — самый сложный случай для реализации (компилятор, компоновщик). Моя точка зрения: даже тот дело не сложнее, чем то, что уже разрешено, например при использовании шаблонов.

6

Теоретически So why do these inconvenient restrictions exist?... причина верна, но ее можно легко обойти, и это именно то, что делает C ++ 11.

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

Другими словами, инициализация по-прежнему связана с конструктором, просто обозначения отличаются и более удобны. Если конструктор не вызывается, значения не инициализируются.

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

Это видно из собственного Страуструпа Часто задаваемые вопросы на C ++ 11.

4