Чому я не можу ініціалізувати non-const статичний член або статичний масив у класі?


116

Чому я не можу ініціалізувати non-const 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.
Вунь Катон

Відмінне запитання та макредна відповідь. Посилання, яке мені допомогло: msdn.microsoft.com/en-us/library/0e5kx78b.aspx
ETFovac

Відповіді:


144

Чому я не можу ініціалізувати staticчленів даних у класі?

Стандарт C ++ дозволяє ініціалізувати лише статичні постійні типи інтегралів чи перерахування всередині класу. Це причина a, яку дозволено ініціалізувати, а інші - ні.

Довідка:
C ++ 03 9.4.2 Статичні члени даних
§4

Якщо статичний член даних має інтегральний характер const або тип перерахування const, його декларація у визначенні класу може визначати константу-ініціалізатор, який повинен бути інтегральним постійним виразом (5.19). У цьому випадку член може з'являтися в цілісних постійних виразах. Член все ж повинен бути визначений в області простору імен, якщо він використовується в програмі, а визначення сфери простору імен не повинно містити ініціалізатор.

Що таке цілісні типи?

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

Типи bool, char, wchar_t, а також підписані та непідписані цілі цілі типи називаються інтегральними типами.43) Синонім цілісного типу - це цілочисельний тип.

Виноска:

43) Тому перерахування (7.2) не є цілісними; однак перерахування можуть бути підвищені до int, unsigned int, long або unsigned long, як зазначено в 4.5.

Обхід:

Ви можете використовувати фокус enum, щоб ініціалізувати масив усередині свого класу.

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

Якщо статичний член даних має const буквальний тип, його декларація у визначенні класу може визначати дужку або рівний ініціалізатор, у якому кожен ініціалізатор-застереження, яке є виразним призначенням, є постійним виразом. Статичний член даних буквального типу може бути оголошений у визначенні класу, constexpr specifier;якщо так, його декларація повинна вказувати дужку або рівний ініціалізатор, в якому кожен ініціалізатор-застереження, яке є призначенням-виразомє постійним виразом. [Примітка. В обох цих випадках член може відображатися в постійних виразах. —Закінчити примітку] Член все одно повинен бути визначений в області простору імен, якщо він використовується в програмі, а визначення області простору імен не повинно містити ініціалізатор.

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

Зауважте, що ці функції ще не були впроваджені в останній gcc 4.7, тому ви все одно можете отримати помилки компіляції.


7
У c ++ 11 речі різні. Відповідь може використовувати оновлення.
bames53

4
Це, мабуть, не відповідає дійсності: "Зауважте, що лише статичні цілі числа const можуть розглядатися як константи часу компіляції. Компілятор знає, що ціле значення не зміниться в будь-який час, і, отже, він може застосувати власну магію і застосувати оптимізацію, компілятор просто вказує таких членів класу, тобто вони більше не зберігаються в пам'яті : " Ви впевнені, що вони обов'язково не зберігаються в пам'яті? Що робити, якщо я надаю визначення членам? Що б &memberповернути?
Наваз

2
@Als: Так. Саме це і є моє питання. То чому C ++ допускає ініціалізацію в класі лише для інтегральних типів, не відповідає правильно Вашою відповіддю. Подумайте, чому це не дозволяє ініціалізацію для static const char*члена?
Наваз

3
@Nawaz: Оскільки C ++ 03 дозволений лише постійним ініціалізатором для статичного і const інтегрального типу та типу перерахування const, а не іншого типу, C ++ 11 поширює це на const буквальний тип, який послаблює норми для ініціалізації в класі. Обмеження в 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

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


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. Вони є неявними визначеннями. Таким чином int i[]; int i[5];, це переосмислення, тоді як static int i[]; int A::i[5];це не так, але на відміну від 2 екстернерів, компілятор все одно виявить дублікат члена, якщо ви робите це static int i[]; static int i[5];в класі.


-3

статичні змінні специфічні для класу. Конструктори ініціалізують атрибути ESPECIALY для екземпляра.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.