С++ 11 допускает инициализацию в классе нестатических и неконстантных элементов. Что изменилось?

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

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

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

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

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

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

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

В разделе 9.4.2 также допускается инициализация нестационарных статических элементов в классе, если они помечены спецификатором constexpr.

Итак, что случилось с причинами ограничений, которые мы имели в С++ 03? Мы просто принимаем "сложные правила компоновщика" или что-то еще изменилось, что упрощает его реализацию?

Ответ 1

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

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

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

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

Ответ 2

Я предполагаю, что аргументация могла быть написана до того, как шаблоны были доработаны. В конце концов, для "статических" элементов шаблонов уже необходимо было/было обязано "сложное правило (-е) компоновщика, необходимое для инициализаторов в статичных элементах класса.

Рассмотрим

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). Было бы невозможно реализовать С++ 03 без него. И поэтому не нужно было расширять компоновщик.

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


UPDATE

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

Ответ 3

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

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

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

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

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