Неопределенная ссылка на статический constexpr char[]
Я хочу иметь static const
char
массив в моем классе. GCC пожаловался и сказал мне, что я должен использовать constexpr
, хотя теперь он говорит мне, что это неопределенная ссылка. Если я сделаю массив не членом, он будет скомпилирован. Что здесь происходит?
// .hpp
struct foo {
void bar();
static constexpr char baz[] = "quz";
};
// .cpp
void foo::bar() {
std::string str(baz); // undefined reference to baz
}
5 ответов
Добавьте в ваш файл cpp:
constexpr char foo::baz[];
Причина: вы должны предоставить определение статического члена, а также объявление. Объявление и инициализатор находятся внутри определения класса, но определение члена должно быть отдельным.
C++17 вводит встроенные переменные
C++17 исправляет эту проблему для статических переменных-членов constexpr, требующих внешнего определения, если оно было использовано. Смотрите оригинальный ответ ниже для деталей до C++17.
Предложение P0386 Inline Variables предоставляет возможность применять встроенный спецификатор к переменным. В частности, в этом случае constexpr подразумевает inline для статических переменных-членов. Предложение говорит:
Встроенный спецификатор может применяться как к переменным, так и к функциям. Переменная, объявленная inline, имеет ту же семантику, что и функция, объявленная inline: она может быть определена идентично в нескольких единицах перевода, должна быть определена в каждой единице перевода, в которой она выделяется, и поведение программы такое, как если бы ровно одна переменная.
и модифицировал [basic.def]p2:
Декларация является определением, если
...
- он объявляет член статических данных вне определения класса, и переменная была определена внутри класса с помощью спецификатора constexpr (это использование устарело; см. [depr.static_constexpr]),
...
и добавьте [depr.static_constexpr]:
Для совместимости с предыдущими международными стандартами C++ член статических данных constexpr может быть избыточно объявлен вне класса без инициализатора. Это использование не рекомендуется. [ Пример:
struct A { static constexpr int n = 5; // definition (declaration in C++ 2014) }; constexpr int A::n; // redundant declaration (definition in C++ 2014)
- конец примера]
Оригинальный ответ
В C++03 нам было разрешено предоставлять intializer класса только для константных интегралов или константных типов перечисления, в C++11 с использованием constexpr это было распространено на литеральные типы.
В C++11 нам не нужно предоставлять определение области имен для статического члена constexpr, если он не используется odr, это можно увидеть из черновика стандартного раздела C++11 9.4.2
[class.static.data], который говорит (акцент мой вперед):
[...] Статический член данных литерального типа может быть объявлен в определении класса с помощью спецификатора constexpr; если это так, его объявление должно указывать инициализатор скобок или равных, в котором каждое предложение инициализатора, являющееся выражением присваивания, является константным выражением. [Примечание: в обоих этих случаях член может появляться в константных выражениях. - примечание конца] Элемент все еще должен быть определен в области пространства имен, если он используется в программе с помощью odr (3.2), и определение области пространства имен не должно содержать инициализатор.
Тогда возникает вопрос: baz
odr-используется здесь:
std::string str(baz);
и ответ - да, и поэтому нам также требуется определение области имен.
Так как же определить, используется ли переменная odr? Оригинальная формулировка C++11 в разделе 3.2
[basic.def.odr] говорит:
Выражение потенциально оценивается, если оно не является неоцененным операндом (раздел 5) или его подвыражением. Переменная, имя которой появляется в качестве потенциально оцениваемого выражения, используется odr, если только она не является объектом, удовлетворяющим требованиям для появления в константном выражении (5.19), и преобразование lvalue-в-значение (4.1) применяется немедленно.
Так baz
дает константное выражение, но преобразование lvalue-в-значение не применяется немедленно, так как оно не применимо из-за baz
будучи массивом. Это описано в разделе 4.1
[conv.lval] который говорит:
Glvalue (3.10) нефункционального типа, не являющегося массивом T, может быть преобразован в prvalue.53 [...]
Что применяется при преобразовании массива в указатель.
Эта формулировка [basic.def.odr] была изменена из-за Отчета о дефектах 712, поскольку некоторые случаи не были охвачены этой формулировкой, но эти изменения не изменяют результаты для этого случая.
Это действительно недостаток в C++11 - как объяснили другие, в C++ 11 статическая переменная-член constexpr, в отличие от любого другого типа глобальной переменной constexpr, имеет внешнюю связь, поэтому должна где-то явно определяться.
Стоит также отметить, что на практике вы часто можете обходиться без статических переменных-членов constexpr без определений при компиляции с оптимизацией, поскольку они могут оказаться встроенными во всех случаях, но если вы компилируете без оптимизации, часто ваша программа не сможет связать. Это делает это очень распространенной скрытой ловушкой - ваша программа прекрасно компилируется с оптимизацией, но, как только вы выключаете оптимизацию (возможно, для отладки), она не может установить связь.
Хорошая новость - этот недостаток исправлен в C++17! Однако этот подход немного запутан: в C++ 17 статические переменные-члены constexpr неявно встроены. Применение inline к переменным является новым понятием в C++17, но это фактически означает, что они нигде не нуждаются в явном определении.
Разве не более элегантное решение изменить char[]
в:
static constexpr char * baz = "quz";
Таким образом, мы можем получить определение / объявление / инициализатор в 1 строке кода.
Мой обходной путь для внешней привязки статических членов заключается в использовании constexpr
получатели ссылочных членов (которые не сталкиваются с проблемой @gnzlbg, поднятой в качестве комментария к ответу @deddebme).
Эта идиома важна для меня, потому что я не люблю иметь несколько.cpp файлов в своих проектах, и пытаюсь ограничить число до одного, который состоит только из #include
с и main()
функция.
// foo.hpp
struct foo {
static constexpr auto& baz() { return "quz"; }
};
// some.cpp
auto sz = sizeof(foo::baz()); // sz == 4
auto& foo_baz = foo::baz(); // note auto& not auto
auto sz2 = sizeof(foo_baz); // 4
auto name = typeid(foo_baz).name(); // something like 'char const[4]'
В моем окружении gcc vesion - 5.4.0. Добавление -O2 может исправить эту ошибку компиляции. Кажется, gcc может справиться с этим случаем при запросе оптимизации.