До С++ 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.