静态 constexpr 字符串的引用未定义

236

我想在我的类中拥有一个static constchar数组。 GCC提示我应该使用constexpr,但现在它告诉我这是未定义的参考。如果我将数组设置为非成员,则可以编译。发生了什么?

// .hpp
struct foo {
  void bar();
  static constexpr char baz[] = "quz";
};

// .cpp
void foo::bar() {
  std::string str(baz); // undefined reference to baz
}

1
有一个猜想,如果baz是int类型的,它能工作吗?你能访问它吗?这也可能是一个bug。 - FailedDev
1
@Pubby:问题:它将在哪个翻译单元中定义?答案:包括头文件的所有内容。问题:违反了一个定义规则。例外:编译时常量整数可以在头文件中“初始化”。 - Mooing Duck
考虑到观点和赞同数,这个问题需要更详细的答案,我在下面添加了。 - Shafik Yaghmour
@Shafik:这是一个重复的问题,因为在C++11之前,静态成员如果被odr-used使用,则需要定义而不仅仅是声明。 - Ben Voigt
在类常量静态成员初始化方面的规则发生了很大变化,odr规则也是如此。在C++11之前,甚至不可能出现这种特殊情况,因此无法在C++03问题中找到这个问题的答案。 - Shafik Yaghmour
显示剩余5条评论
6个回答

236

在您的cpp文件中添加:

constexpr char foo::baz[];

原因:您必须提供静态成员的定义和声明。声明和初始化器在类定义中,但成员定义必须分开。


83
看起来很奇怪...因为它似乎没有提供编译器之前没有的一些信息... - vines
39
当类声明在.cpp文件中时,看起来会更加奇怪!你在类声明中初始化字段,但仍需要通过在类下方编写constexpr char foo :: baz[]来"声明"该字段。似乎使用constexpr的程序员可以通过遵循一个奇怪的技巧来编译他们的程序:再次声明它。 - Lukasz Czerwinski
5
你要找的词是“定义”。 - Kerrek SB
5
正确,没有新信息:使用decltype(foo::baz) constexpr foo::baz;进行声明。 - not-a-user
9
如果将 foo 转变为模板形式,那么表达式会是什么样子?谢谢。 - Hei
显示剩余13条评论

125

C++17引入了内联变量

对于constexpr static成员变量,C++17修复了需要在odr-used时进行外部定义的问题。有关C++17之前的详细信息,请参见此答案的后半部分。

提案P0386 Inline Variables引入了将inline限定符应用于变量的能力。特别是对于这种情况,constexpr意味着静态成员变量是inline的。该提案说:

内联限定符也可以应用于变量以及函数。声明为内联的变量具有与声明为内联的函数相同的语义:它可以在多个翻译单元中定义,必须在使用它的每个翻译单元中定义,并且程序的行为就像存在一个变量一样。

并修改[basic.def]p2:

一个声明就是一个定义,除非
  • 它在类定义之外声明了一个静态数据成员,并且该变量在类内部使用constexpr限定符进行了定义(此用法已被弃用,请参见[depr.static_constexpr]),

...

并添加[depr.static_constexpr]

For compatibility with prior C++ International Standards, a constexpr static data member may be redundantly redeclared outside the class with no initializer. This usage is deprecated. [ Example:

struct A {
  static constexpr int n = 5;  // definition (declaration in C++ 2014)
};

constexpr int A::n;  // redundant declaration (definition in C++ 2014)

 — end example ]


C++14及之前版本

在C++03中,我们只能为const integralsconst enumeration types提供类内初始值设定项,在C++11中使用constexpr将此扩展到literal types

在C++11中,如果静态constexpr成员没有被odr-used,则无需为其提供命名空间范围定义。这可以从C++11标准草案的第9.4.2[class.static.data]中看出(下文将强调我的重点):

[...] 可以在类定义中使用constexpr指定文字类型的静态数据成员;如果是这样,则其声明应指定一个大括号或等于初始化程序,在其中每个初始化项都是常量表达式的赋值表达式。[注:在这两种情况下,该成员可以出现在常量表达式中。——end note] 如果程序中使用了静态成员且该成员是odr-used(3.2),则仍然需要在命名空间范围内定义该成员,但该定义不应包含初始化项。

那么问题就变成了,这里是否 bazodr-used

std::string str(baz); 

答案是,因此我们还需要一个命名空间范围定义。

那么如何确定变量是否odr-used?原始的C++11措辞在第3.2[basic.def.odr]中说:

除非它是未评估的操作数(第5条),或其子表达式,否则表达式是可能被评估的。出现为可能被评估的表达式的变量是odr-used,除非它是一个满足出现在常量表达式中的要求的对象(5.19),并且立即应用lvalue-to-rvalue转换(4.1)。

因此,baz确实产生了常量表达式,但由于baz是一个数组,因此lvalue-to-rvalue转换不适用,因此没有立即应用。这在第4.1[conv.lval]中有所涵盖,其中写道:

一个非函数、非数组类型 T 的 glvalue(3.10)可以被转换为 prvalue.53[...].
array-to-pointer conversion 中应用了什么。
由于 Defect Report 712[basic.def.odr] 的措辞已更改,因为这些措辞没有涵盖某些情况,但这些更改不会改变此情况的结果。

我们清楚了吗,constexpr 和此无关吗? (baz 无论如何都是一个常量表达式) - M.M
@MattMcNabb 好的,如果成员不是“整数或枚举类型”,则需要使用constexpr,但除此之外,重要的是它是一个常量表达式 - Shafik Yaghmour
在第一段中,“ord-used”应该是“odr-used”,我相信,但我对C ++从来不确定。 - Egor Pasko

55

这真的是C++11中的一个缺陷 - 如其他人所解释,在C++11中,静态constexpr成员变量与每种其他类型的constexpr全局变量不同,具有外部链接性,因此必须在某处明确定义。

值得注意的是,当使用优化编译时,通常可以轻松地使用静态constexpr成员变量而不需要定义,因为它们可以在所有用法中内联,但如果没有启用优化编译,则您的程序通常会无法链接。这使得这成为一个非常常见的隐藏陷阱 - 您的程序在优化的情况下编译正常,但是一旦关闭优化(例如调试),就无法链接。

好消息是 - 在C++17中,这个缺陷已经得到修复!虽然方法有点繁琐:在C++17中,静态constexpr成员变量隐式内联。将内联应用于变量是C++17中的一个新概念,但它有效地意味着它们不需要在任何地方显式定义。


4
可以为C++17信息提供更新。您可以将此信息添加到已接受的答案中! - S.R

6

我对静态成员变量的外部链接的解决方法是使用 constexpr 引用成员的 getter(这不会遇到 @gnzlbg 评论中提到的问题,而那个问题会影响 @deddebme 的答案)。
这个习语对我来说很重要,因为我讨厌在我的项目中有多个 .cpp 文件,并尽量将数量限制为一个,其中仅包含 #includemain() 函数。

// 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]'

不错的想法。我在另一篇帖子中也看到,整数字面量的替代方法是在类中定义一个未作用域的枚举,主要缺点是它定义了一个类作用域值,无法作为成员访问。请参见stackoverflow.com/a/3026072/937363。 - mattgately

5
难道更优雅的解决方案不是将char[]改为:
static constexpr char * baz = "quz";

这样我们就可以在一行代码中定义/声明/初始化。

12
使用 char[],您可以在编译时使用 sizeof 来获取字符串的长度,但是对于 char*,您不能这样做(它将返回指针类型的宽度,在此情况下为1)。 - gnzlbg
3
如果您希望严格遵循ISO C++11标准,这也会产生警告。 - Shital Shah
请查看我的答案,它不会出现 sizeof 问题,并且可以用于“仅头文件”的解决方案。 - Josh Greifer
1
添加const以修复ISO警告: static constexpr const char * baz = "quz"; - mentalmushroom
是的,您需要添加constchar/侧以将指向的值标记为const(这是一个字符串文字,如果您尝试更改它,它将产生未定义的行为)。在您的情况下,constexpr仅会使指针const。这是我更喜欢constexpr char s[]而不是constexpr const char* sconstexpr char const* s的另一个原因。 - codentary

-2

在我的环境中,gcc版本是5.4.0。添加“-O2”可以解决这个编译错误。当要求优化时,似乎gcc可以处理这种情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接