Определение целочисленных членов static const в определении класса

109

Я понимаю, что C ++ позволяет определять статические константные члены внутри класса, если это целочисленный тип.

Почему же тогда следующий код выдает ошибку компоновщика?

#include <algorithm>
#include <iostream>

class test
{
public:
    static const int N = 10;
};

int main()
{
    std::cout << test::N << "\n";
    std::min(9, test::N);
}

Я получаю следующую ошибку:

test.cpp:(.text+0x130): undefined reference to `test::N'
collect2: ld returned 1 exit status

Интересно, что если я закомментирую вызов std :: min, код компилируется и связывается нормально (хотя test :: N также упоминается в предыдущей строке).

Есть идеи, что происходит?

Мой компилятор - gcc 4.4 для Linux.

HighCommander4
источник
3
Прекрасно работает на Visual Studio 2010.
Puppy
4
Эта точная ошибка объясняется на gcc.gnu.org/wiki/…
Джонатан Уэйкли
В частном случае charвы можете определить его как constexpr static const char &N = "n"[0];. Обратите внимание на расширение &. Я думаю, это работает, потому что буквальные строки определяются автоматически. Хотя меня это немного беспокоит - он может вести себя странно в файле заголовка среди разных единиц перевода, поскольку строка, вероятно, будет иметь несколько разных адресов.
Аарон МакДэйд
1
Этот вопрос является свидетельством того, насколько плохим остается ответ C ++ на «не использовать #defines для констант».
Johannes Overmann
1
@JohannesOvermann В этой связи я хочу упомянуть об использовании встроенных переменных для глобальных переменных начиная с C ++ 17 inline const int N = 10, где, насколько мне известно, все еще есть хранилище, определенное компоновщиком. В этом случае можно также использовать встроенное ключевое слово для определения статической переменной внутри теста определения класса.
Вормер

Ответы:

72

Я понимаю, что C ++ позволяет определять статические константные члены внутри класса, если это целочисленный тип.

Вы вроде как правы. Вы можете инициализировать статические константные интегралы в объявлении класса, но это не определение.

http://publib.boulder.ibm.com/infocenter/comphelp/v8v101/index.jsp?topic=/com.ibm.xlcpp8a.doc/language/ref/cplr038.htm

Интересно, что если я закомментирую вызов std :: min, код компилируется и связывается нормально (хотя test :: N также упоминается в предыдущей строке).

Есть идеи, что происходит?

std :: min принимает свои параметры по ссылке const. Если бы они брались по значению, у вас не было бы этой проблемы, но, поскольку вам нужна ссылка, вам также нужно определение.

Вот глава / стих:

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

См. Ответ Чу для возможного обходного пути.

Эдвард Стрэндж
источник
Понятно, это интересно. В этом случае, в чем разница между предоставлением значения в точке объявления и предоставлением значения в точке определения? Какой из них рекомендуется?
HighCommander4
Что ж, я считаю, что вы можете обойтись без определения до тех пор, пока вы фактически никогда не «используете» переменную. Если вы используете его только как часть константного выражения, переменная никогда не используется. В противном случае, похоже, не будет большой разницы, кроме возможности увидеть значение в заголовке - что может быть или не быть тем, что вы хотите.
Эдвард Стрэндж
2
Краткий ответ: static const x = 1; является rvalue, но не lvalue. Значение доступно как константа во время компиляции (с ее помощью можно измерить массив) static const y; [без инициализатора] должен быть определен в файле cpp и может использоваться как rvalue или lvalue.
Дейл Уилсон
2
Было бы хорошо, если бы они могли это расширить / улучшить. На мой взгляд, инициализированные, но не определенные объекты следует рассматривать как литералы. Например, нам разрешено привязать литерал 5к const int&. Так почему бы не рассматривать OP test::Nкак соответствующий литерал?
Аарон МакДэйд,
Интересное объяснение, спасибо! Это означает, что в C ++ static const int по-прежнему не заменяет целочисленные #defines. enum всегда имеет только знак int, поэтому для отдельных констант нужно использовать классы enum. Для меня было бы совершенно очевидно выродить объявление константы с константой и известными значениями в буквальную константу, чтобы это могло компилироваться без проблем. C ++ предстоит пройти долгий путь ...
Йоханнес Оверманн
51

Пример Бьярна Страуструпа в его FAQ по C ++ предполагает, что вы правы, и вам нужно определение, только если вы берете адрес.

class AE {
    // ...
public:
    static const int c6 = 7;
    static const int c7 = 31;
};

const int AE::c7;   // definition

int f()
{
    const int* p1 = &AE::c6;    // error: c6 not an lvalue
    const int* p2 = &AE::c7;    // ok
    // ...
}

Он говорит: «Вы можете взять адрес статического члена, если (и только если) он имеет внеклассовое определение» . Это говорит о том, что это сработало бы иначе. Может быть, ваша функция min как-то за кулисами вызывает адреса.

HostileFork говорит, что не доверяйте SE
источник
2
std::minпринимает свои параметры по ссылке, поэтому требуется определение.
Rakete1111
Как мне написать определение, если AE - это шаблонный класс AE <class T>, а c7 - не int, а T :: size_type? У меня есть значение, инициализированное как «-1» в заголовке, но clang говорит о неопределенном значении, и я не знаю, как написать определение.
Фабиан
@Fabian Я путешествую, разговариваю по телефону и немного занят ... но я думаю, что ваш комментарий звучит так, как будто его лучше всего написать как новый вопрос. Напишите MCVE, включая полученную ошибку, также, возможно, добавьте то, что говорит gcc. Бьюсь об заклад, люди быстро скажут вам, что к чему.
HostileFork говорит, что не доверяйте SE
@HostileFork: При написании MCVE вы иногда сами находите решение. В моем случае ответ заключается в том, что template<class K, class V, class C> const typename AE<K,V,C>::KeyContainer::size_type AE<K,V,C>::c7;KeyContainer - это typedef для std :: vector <K>. Необходимо перечислить все параметры шаблона и написать typename, потому что это зависимый тип. Может, кому-то этот комментарий будет полезен. Однако теперь мне интересно, как экспортировать это в DLL, потому что класс шаблона, конечно, находится в заголовке. Надо ли экспортировать c7 ???
Фабиан
24

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

class test
{
public:
    enum { N = 10 };
};
Стивен Чу
источник
2
И это, вероятно, решило бы проблему. Когда N используется в качестве параметра для min (), это вызовет создание временного объекта, а не попытку ссылки на предположительно существующую переменную.
Эдвард Стрэндж
Это имело то преимущество, что его можно было сделать приватным.
Агостино
11

Не только int. Но вы не можете определить значение в объявлении класса. Если у вас есть:

class classname
{
    public:
       static int const N;
}

в файле .h у вас должно быть:

int const classname::N = 10;

в файле .cpp.

Amardeep AC9MF
источник
2
Я знаю, что вы можете объявить переменную любого типа внутри объявления класса. Я сказал, что думал, что статические целочисленные константы также могут быть определены внутри объявления класса. Разве это не так? Если нет, то почему компилятор не выдает ошибку в строке, где я пытаюсь определить ее внутри класса? Более того, почему строка std :: cout не вызывает ошибку компоновщика, а строка std :: min вызывает?
HighCommander4
Нет, невозможно определить статические члены в объявлении класса, потому что инициализация генерирует код. В отличие от встроенной функции, которая также генерирует код, статическое определение глобально уникально.
Amardeep AC9MF,
@ HighCommander4: Вы можете указать инициализатор для static constинтегрального члена в определении класса. Но это все еще не определяет этого члена. Подробности см. В ответе Ноя Робертса.
AnT
9

Вот еще один способ обойти проблему:

std::min(9, int(test::N));

(Я думаю, что ответ Сумасшедшего Эдди правильно описывает, почему существует проблема.)

Карадок
источник
5
или дажеstd::min(9, +test::N);
Томилов Анатолий
Но вот большой вопрос: оптимально ли все это? Не знаю, как вы, ребята, но меня больше всего привлекает пропуск определения, поскольку оно не должно занимать память и накладные расходы при использовании const static.
Opux
6

Начиная с C ++ 11 вы можете использовать:

static constexpr int N = 10;

Теоретически это все еще требует, чтобы вы определяли константу в файле .cpp, но пока вы не берете ее адрес N, очень маловероятно, что какая-либо реализация компилятора выдаст ошибку;).

Карло Вуд
источник
А что, если вам нужно передать значение в качестве аргумента типа const int &, как в примере? :-)
Вормер
Это нормально работает. Вы не создаете экземпляр N таким образом, просто передавая константную ссылку на временный объект. wandbox.org/permlink/JWeyXwrVRvsn9cBj
Карло Вуд
Возможно, C ++ 17, а не C ++ 14 и даже не C ++ 17 в более ранних версиях gcc 6.3.0 и ниже, это не стандартная вещь. Но спасибо, что упомянули об этом.
Вормер
Ах да, ты прав. Я не пробовал C ++ 14 на wandbox. Хорошо, это та часть, где я сказал: «Теоретически это все еще требует от вас определения константы». Итак, вы правы, что это не «стандарт».
Карло Вуд
3

C ++ позволяет определять статические константные члены внутри класса

Нет, 3.1 §2 говорит:

Объявление - это определение, если оно не объявляет функцию без указания тела функции (8.4), оно содержит спецификатор extern (7.1.1) или спецификацию связи (7.5) и не содержит ни инициализатора, ни тела функции, оно объявляет статические данные член в определении класса (9.4), это объявление имени класса (9.1), это объявление непрозрачного перечисления (7.2) или объявление typedef (7.1.3), объявление-использование (7.3. 3) или директивой using (7.3.4).

fredoverflow
источник