Неопределенная ссылка на статический constexpr char []

186

Я хочу иметь 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
}
Pubby
источник
1
Просто догадка, работает ли, например, если baz - это int? Можете ли вы получить к нему доступ? Это также может быть ошибкой.
FailedDev
1
@Pubby: Вопрос: в какой единице перевода он будет определен? Ответ: все, что включает в себя заголовок. Проблема: Нарушение правила одного определения. Исключение: постоянные интегралы времени компиляции могут быть «инициализированы» в заголовках.
Mooing Duck
Он отлично компилируется как int@MooingDuck. Он отлично работает как не член. Не нарушит ли это тоже правило?
Пабби
@ Pubby8: intс чит. Как неучастник, это не должно быть разрешено, если правила не изменены для C ++ 11 (возможно)
Mooing Duck
Учитывая мнения и возражения, этот вопрос требовал более подробного ответа, который я добавил ниже.
Шафик Ягмур

Ответы:

188

Добавьте в ваш файл cpp:

constexpr char foo::baz[];

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

Керрек С.Б.
источник
70
Это выглядит странно ... так как это , кажется, не обеспечивает компилятор с некоторой информацией , которую он не раньше ...
Vines
32
Это выглядит еще более странно, когда у вас есть объявление класса в файле .cpp! Вы инициализируете поле в объявлении класса, но вам все еще нужно « объявить » поле, написав constexpr char foo :: baz [] ниже класса. Кажется, что программисты, использующие constexpr, могут скомпилировать свои программы, следуя одному странному совету: объявить его снова.
Лукаш Червински
5
@LukaszCzerwinski: Слово, которое вы ищете, это «определить».
Kerrek SB
5
Правильно, никакой новой информации: объявите, что используетеdecltype(foo::baz) constexpr foo::baz;
не пользователь
6
как будет выглядеть выражение, если foo шаблонизирован? Спасибо.
Привет
80

C ++ 17 вводит встроенные переменные

C ++ 17 исправляет эту проблему для constexpr staticпеременных-членов, требующих внепланового определения, если оно было использовано odr. См. Вторую половину этого ответа для деталей до C ++ 17.

Предложение P0386 Inline Variables предоставляет возможность применять inlineспецификатор к переменным. В частности, к этому случаю constexprотносятся inlineстатические переменные-члены. Предложение говорит:

Встроенный спецификатор может применяться как к переменным, так и к функциям. Переменная, объявленная inline, имеет ту же семантику, что и функция, объявленная inline: она может быть определена идентично в нескольких единицах перевода, должна быть определена в каждой единице перевода, в которой она используется в odr, и поведение программы такое, как если бы есть ровно одна переменная.

и модифицировал [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 ++ 14 и более ранние

В C ++ 03 нам было разрешено предоставлять только инициализаторы в классе для константных интегралов или константных типов перечисления , в 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-to-rvalue применяется не сразу, так как оно неприменимо из-за того, bazчто является массивом. Это 4.1 описано в разделе [conv.lval], где говорится:

Glvalue (3.10) нефункционального типа, не являющегося массивом T, может быть преобразован в prvalue.53 [...]

Что применяется при преобразовании массива в указатель .

Эта формулировка [basic.def.odr] была изменена из-за Отчета о дефектах 712, поскольку некоторые случаи не были охвачены этой формулировкой, но эти изменения не изменяют результаты для этого случая.

Шафик Ягмур
источник
так ясно ли нам, что это не constexprимеет к этому никакого отношения? ( bazв любом случае это константное выражение)
ММ
@MattMcNabb хорошо constexpr требуется, если член не является, integral or enumeration typeно в противном случае, да, важно то, что это константное выражение .
Шафик Ягмур
Я считаю, что в первом абзаце слово «ord-used» следует читать как «odr-used», но я никогда не уверен с C ++
Егор Пасько
37

Это действительно недостаток в C ++ 11 - как объяснили другие, в C ++ 11 статическая переменная-член constexpr, в отличие от любого другого типа глобальной переменной constexpr, имеет внешнюю связь, поэтому должна где-то явно определяться.

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

Хорошая новость - этот недостаток исправлен в C ++ 17! Однако этот подход немного запутан: в C ++ 17 статические переменные-члены constexpr неявно встроены . Применение inline к переменным является новой концепцией в C ++ 17, но это фактически означает, что им нигде не требуется явное определение.

SethML
источник
4
Up for C ++ 17 информация. Вы можете добавить эту информацию к принятому ответу!
СР
5

Разве более элегантное решение не превращается char[]в:

static constexpr char * baz = "quz";

Таким образом, мы можем получить определение / объявление / инициализатор в 1 строке кода.

deddebme
источник
9
с помощью char[]вы можете использовать, sizeofчтобы получить длину строки во время компиляции, с char *вы не можете (она вернет ширину типа указателя, в данном случае 1).
gnzlbg
2
Это также генерирует предупреждение, если вы хотите быть строгим с ISO C ++ 11.
Шиталь Шах
Посмотрите мой ответ, который не показывает sizeofпроблему и может быть использован в решениях «только для заголовка»
Джош Грайфер
4

Мой обходной путь для внешней компоновки статических членов заключается в использовании constexprметодов получения ссылочных членов (что не приводит к проблеме @gnzlbg, возникшей в качестве комментария к ответу @deddebme).
Эта идиома важна для меня, потому что я ненавижу иметь несколько .cpp файлов в своих проектах и ​​пытаюсь ограничить число до одного, который состоит только из #includes и 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]'
Джош Грайфер
источник
-1

В моем окружении gcc vesion - 5.4.0. Добавление -O2 может исправить эту ошибку компиляции. Кажется, gcc может справиться с этим случаем при запросе оптимизации.

Хайшань Чжоу
источник