Почему я не могу инициализировать неконстантный статический член или статический массив в классе?

116

Почему я не могу инициализировать неконстантный staticчлен или staticмассив в классе?

class A
{
    static const int a = 3;
    static int b = 3;
    static const int c[2] = { 1, 2 };
    static int d[2] = { 1, 2 };
};

int main()
{
    A a;

    return 0;
}

компилятор выдает следующие ошибки:

g++ main.cpp
main.cpp:4:17: error: ISO C++ forbids in-class initialization of non-const static member b
main.cpp:5:26: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:5:33: error: invalid in-class initialization of static data member of non-integral type const int [2]’
main.cpp:6:20: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:6:27: error: invalid in-class initialization of static data member of non-integral type int [2]’

У меня два вопроса:

  1. Почему я не могу инициализировать staticэлементы данных в классе?
  2. Почему я не могу инициализировать staticмассивы в классе, даже constмассив?
Ишу Фанг
источник
1
Я думаю, главная причина в том, что это сложно сделать правильно. В принципе, вы, вероятно, могли бы сделать то, о чем говорите, но будут некоторые странные побочные эффекты. Как если бы ваш пример массива был разрешен, тогда вы могли бы получить значение A :: c [0], но не смогли бы передать A :: c функции, поскольку для этого потребуется адрес и время компиляции у констант нет адреса. В C ++ 11 некоторые из этих возможностей реализованы с помощью constexpr.
Vaughn Cato
Отличный вопрос и правильный ответ. Ссылка, которая мне помогла: msdn.microsoft.com/en-us/library/0e5kx78b.aspx
ETFovac

Ответы:

144

Почему я не могу инициализировать staticэлементы данных в классе?

Стандарт C ++ позволяет инициализировать внутри класса только статические постоянные целочисленные или перечисляемые типы. Это причина, по aкоторой разрешена инициализация, а другим нет.

Ссылка:
C ++ 03 9.4.2 Статические элементы данных
§4

Если статический член данных имеет тип константного интегрального или константного перечисления, его объявление в определении класса может указывать константный инициализатор, который должен быть интегральным константным выражением (5.19). В этом случае член может появляться в целочисленных постоянных выражениях. Этот член по-прежнему должен быть определен в области пространства имен, если он используется в программе, а определение области пространства имен не должно содержать инициализатор.

Что такое целочисленные типы?

C ++ 03 3.9.1 Основные типы
§7

Типы bool, char, wchar_t, а также целочисленные типы со знаком и без знака вместе называются целочисленными типами. 43) Синонимом целочисленного типа является целочисленный тип.

Сноска:

43) Следовательно, перечисления (7.2) не являются целыми; однако перечисления могут быть повышены до int, unsigned int, long или unsigned long, как указано в 4.5.

Обходной путь:

Вы можете использовать трюк с перечислением для инициализации массива внутри определения вашего класса.

class A 
{
    static const int a = 3;
    enum { arrsize = 2 };

    static const int c[arrsize] = { 1, 2 };

};

Почему Стандарт не допускает этого?

Бьярн хорошо объясняет это здесь :

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

Почему static constразрешены только интегральные типы и перечисления в инициализации класса?

Ответ скрыт в цитате Бьярна, прочтите ее внимательно:
«C ++ требует, чтобы каждый объект имел уникальное определение. Это правило было бы нарушено, если бы C ++ разрешил определение сущностей внутри класса, которые необходимо хранить в памяти как объекты».

Обратите внимание, что только static constцелые числа можно рассматривать как константы времени компиляции. Компилятор знает, что целочисленное значение не изменится в любое время, и, следовательно, он может применять свою собственную магию и применять оптимизацию, компилятор просто встраивает такие члены класса, то есть они больше не хранятся в памяти, поскольку необходимость хранения в памяти устранена , это дает таким переменным исключение из правила, упомянутого Бьярном.

Здесь следует отметить, что даже если static constцелочисленные значения могут иметь инициализацию класса, получение адреса таких переменных не допускается. Можно взять адрес статического члена, если (и только если) он имеет внеклассовое определение. Это дополнительно подтверждает рассуждение выше.

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


Как это изменилось в C ++ 11?

C ++ 11 до некоторой степени ослабляет ограничение.

C ++ 11 9.4.2 Статические элементы данных
§3

Если статический член данных имеет тип константного литерала, его объявление в определении класса может указывать фигурную скобку или равный инициализатор, в котором каждое предложение инициализатора, которое является выражением присваивания, является постоянным выражением. Статический член данных литерального типа может быть объявлен в определении класса с помощью, constexpr specifier;если это так, его объявление должно указывать на фигурную скобку или равный инициализатор, в котором каждое предложение инициализатора, которое является выражением присваиванияпостоянное выражение. [Примечание: в обоих этих случаях член может появляться в постоянных выражениях. - конец примечания] Этот член должен быть определен в области пространства имен, если он используется в программе, а определение области пространства имен не должно содержать инициализатор.

Кроме того , C ++ 11 будет разрешать (§12.6.2.8) нестатический член данных должны быть инициализированы , где она объявлена (в своем классе). Это будет означать очень простую пользовательскую семантику.

Обратите внимание, что эти функции еще не реализованы в последней версии gcc 4.7, поэтому вы все равно можете получать ошибки компиляции.

Алок Сохранить
источник
7
В С ++ 11 все по-другому. Ответ может использовать обновление.
bames53 09
4
Это не кажется правдой: «Обратите внимание, что только статические целые числа const могут рассматриваться как константы времени компиляции. Компилятор знает, что целочисленное значение не будет изменяться в любое время, и, следовательно, он может применить свое собственное волшебство и применить оптимизацию, компилятор просто встраивает такие члены класса, т.е. они больше не хранятся в памяти : « Вы уверены, что они не обязательно хранятся в памяти? Что, если я дам определения для членов? Что &memberвернется?
Nawaz
2
@Als: Да. Вот в чем мой вопрос. Итак, почему C ++ допускает инициализацию в классе только для целых типов, ваш ответ не отвечает правильно. Подумайте, почему он не позволяет инициализировать static const char*член?
Nawaz
3
@Nawaz: Поскольку C ++ 03 допускает только постоянный инициализатор для статического и интегрального константного типа и типа перечисления const и никакой другой тип, C ++ 11 расширяет его до константного литерального типа, что ослабляет нормы для инициализации класса. в C ++ 03, возможно, был недосмотр, который потребовал изменения и, следовательно, был исправлен в C ++ 11, если есть какие-либо традиционные тактические причины для изменения, я о них не знаю. Если вы знаете о них, не стесняйтесь поделиться их.
Alok Save
4
Упомянутый вами «обходной путь» не работает с g ++.
iammilind
4

Кажется, это пережиток старых времен простых линкеров. Вы можете использовать статические переменные в статических методах как временное решение:

// header.hxx
#include <vector>

class Class {
public:
    static std::vector<int> & replacement_for_initialized_static_non_const_variable() {
        static std::vector<int> Static {42, 0, 1900, 1998};
        return Static;
    }
};

int compilation_unit_a();

и

// compilation_unit_a.cxx
#include "header.hxx"

int compilation_unit_a() {  
    return Class::replacement_for_initialized_static_non_const_variable()[1]++;
}

и

// main.cxx
#include "header.hxx"

#include <iostream>

int main() {
    std::cout
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << std::endl;
}

сборки:

g++ -std=gnu++0x -save-temps=obj -c compilation_unit_a.cxx 
g++ -std=gnu++0x -o main main.cxx compilation_unit_a.o

бегать:

./main

Тот факт, что это работает (последовательно, даже если определение класса включено в разные единицы компиляции), показывает, что современный компоновщик (gcc 4.9.2) на самом деле достаточно умен.

Забавно: Печать 0123на руке и 3210на x86.

не-пользователь
источник
1

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

user541686
источник
0

Это потому, что может быть только одно определение, A::aкоторое используют все единицы перевода.

Если вы выступали static int a = 3;в классе с заголовком, включенным во все единицы перевода, вы получите несколько определений. Таким образом, определение статического значения, не относящееся к внешнему, принудительно вызывает ошибку компилятора.

Использование static inlineили static constисправление этого. static inlineконкретизирует символ только в том случае, если он используется в единице трансляции, и гарантирует, что компоновщик выбирает и оставляет только одну копию, если она определена в нескольких единицах трансляции из-за того, что она находится в группе comdat. constв области видимости файла заставляет компилятор никогда не выдавать символ, потому что он всегда немедленно заменяется в коде, если он externне используется, что недопустимо в классе.

Следует отметить, что static inline int b;это рассматривается как определение, тогда как static const int bили static const A b;все еще рассматриваются как объявление и должны быть определены вне очереди, если вы не определяете его внутри класса. Интересно static constexpr A b;трактуется как определение, тогда static constexpr int b;как это ошибка и должна иметь инициализатор (это потому, что теперь они становятся определениями и, как любое определение const / constexpr в области файла, им требуется инициализатор, которого нет у int, но тип класса делает, потому что он имеет неявное значение, = A()когда является определением - clang позволяет это, но gcc требует, чтобы вы явно инициализировали, иначе это ошибка. Вместо этого это не проблема с inline). static const A b = A();не допускается и должно быть constexprилиinlineчтобы разрешить инициализатор для статического объекта с типом класса, то есть сделать статический член типа класса более чем декларацией. Итак, да в определенных ситуациях A a;- это не то же самое, что явная инициализация A a = A();(первое может быть объявлением, но если для этого типа разрешено только объявление, то второе является ошибкой. Второе может использоваться только в определении. constexprДелает его определением ). Если вы используете constexprи указываете конструктор по умолчанию, то конструктор должен бытьconstexpr

#include<iostream>

struct A
{
    int b =2;
    mutable int c = 3; //if this member is included in the class then const A will have a full .data symbol emitted for it on -O0 and so will B because it contains A.
    static const int a = 3;
};

struct B {
    A b;
    static constexpr A c; //needs to be constexpr or inline and doesn't emit a symbol for A a mutable member on any optimisation level
};

const A a;
const B b;

int main()
{
    std::cout << a.b << b.b.b;
    return 0;
}

Статический член - это прямая декларация области видимости файла extern int A::a;(которая может быть сделана только в классе, а определения вне линии должны относиться к статическому члену в классе и должны быть определениями и не могут содержать extern), тогда как нестатический член является частью полное определение типа класса и имеют те же правила, что и объявления области видимости файла без extern. Это неявные определения. Так int i[]; int i[5];что это переопределение, тогда как static int i[]; int A::i[5];это не так, но в отличие от двух externs, компилятор все равно обнаружит повторяющийся член, если вы это сделаете static int i[]; static int i[5];в классе.

Льюис Келси
источник
-3

статические переменные относятся к классу. Конструкторы инициализируют атрибуты ОСОБЕННО для экземпляра.


источник