对静态constexpr char[]的未定义引用

Undefined reference to static constexpr char[]

本文关键字:未定义 引用 静态 constexpr char      更新时间:2023-10-16

我想在我的类中有一个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
}

添加到您的cpp文件:

constexpr char foo::baz[];
原因:必须提供静态成员的定义以及声明。声明和初始化器在类定义中,但成员定义必须分开。

c++ 17引入内联变量

c++ 17修复了constexpr static成员变量的这个问题,如果它被频繁使用,需要一个脱行定义。有关c++ 17之前版本的详细信息,请参阅本回答的后半部分。

提案P0386内联变量引入了将 inline说明符应用于变量的能力。特别是在这种情况下,constexpr意味着静态成员变量的inline。提案说:

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

和修改[basic.def]p2:

声明就是定义,除非

  • 它在类定义之外声明了一个静态数据成员,并且变量是在类中使用constexpr说明符定义的(这种用法已弃用;参见[depr.static_constexpr]),

and add [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)

- end示例]


c++ 14及更早版本

在c++ 03中,我们只允许为const整型const枚举类型提供类内初始化式,在c++ 11中使用 constexpr 扩展到文字类型

在c++ 11中,如果静态constexpr成员不是odr使用的,我们不需要为它提供命名空间作用域定义,我们可以从c++ 11标准草案9.4.2 [class.static]小节中看到这一点。数据]上面写着(强调我的前进):

[…可以在类中声明文字类型的静态数据成员带有constexpr说明符的定义;如果是这样,其声明应指定一个大括号或等号初始化项,其中每个初始化项子句这是一个赋值表达式,是一个常量表达式。[注:在在这两种情况下,成员都可以出现在常量表达式中。端请注意)如果成员在程序和命名空间作用域定义中被odr (3.2)使用,则该成员仍应定义在命名空间作用域中不能包含初始化式。

那么问题就变成了,baz 是否在这里使用

std::string str(baz); 

,答案是,所以我们还需要一个命名空间作用域定义。

那么我们如何确定一个变量是否是常用的呢?原始c++ 11在3.2 [basic.def.odr]章节中的措辞是:

除非表达式是未求值的,否则它可能被求值操作数(第5条)或其子表达式。其名称为是不常用的,除非它是满足出现在a中的要求的对象常量表达式(5.19)和左值到右值的转换(4.1)立即应用

因此baz确实产生常量表达式,但左值到右值转换不会立即应用,因为它不适用,因为baz是一个数组。这在4.1 [conv]小节中有介绍。表示:

非函数、非数组类型T的glvalue(3.10)可以是转换为右值。53[…]

数组到指针的转换应用了什么?

[basic.def.odr]的措辞是由于缺陷报告712而改变的,因为一些情况没有被这种措辞所覆盖,但是这些变化不会改变这种情况的结果。

这确实是c++ 11中的一个缺陷——正如其他人解释的那样,在c++ 11中,静态constexpr成员变量与其他类型的constexpr全局变量不同,具有外部链接,因此必须在某处显式定义。

同样值得注意的是,在使用优化进行编译时,您通常可以在实践中避免使用没有定义的静态constexpr成员变量,因为它们最终会在所有使用中被内联,但是如果您在不进行优化的情况下进行编译,您的程序通常会链接失败。这使得这成为一个非常常见的隐藏陷阱——你的程序在优化后编译得很好,但一旦你关闭优化(可能是为了调试),它就无法链接。

好消息-这个缺陷在c++ 17中被修复了!不过这种方法有点复杂:在c++ 17中,静态constexpr成员变量是隐式内联的。将内联应用于变量是c++ 17中的一个新概念,但它实际上意味着它们不需要在任何地方显式定义。

对于静态成员的外部链接,我的解决方法是使用constexpr引用成员getter(这不会遇到@gnzlbg作为对@deddebme答案的评论而提出的问题)。
这个习惯用法对我来说很重要,因为我讨厌在我的项目中有多个.cpp文件,并试图将数量限制在一个,该文件仅由#include s和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]'

更优雅的解决方案不是将char[]改为:

static constexpr char * baz = "quz";
通过这种方式,我们可以在一行代码中拥有定义/声明/初始化式。

在我的环境中,gcc版本是5.4.0。添加"-O2"可以修复此编译错误。在请求优化时,gcc似乎可以处理这种情况。