为什么我不能将课内初始化的 `const std::string` 声明为静态成员?

9

我有以下可用代码:

#include <string>
#include <iostream>

class A {
public:
  const std::string test = "42";
  //static const std::string test = "42"; // fails
};

int main(void){
  A a;
  std::cout << a.test << '\n';
}

有没有一个很好的理由,为什么不能将测试设置为static const?我理解在c++11之前它受标准限制。我认为c++11引入了类内初始化,使其更加友好。我也知道对于整数类型已经有这样的语义存在了相当长的时间。
当然,它可以通过类外初始化的形式const std::string A::test = "42";来工作。
我猜,如果你可以把它变成非静态的,那么问题就出在其中之一。在类外范围进行初始化(通常const是在对象实例化时创建的)。但是,如果您正在创建与类的任何其他成员无关的对象,则我不认为这是问题。第二个问题是静态成员有多个定义。例如,如果它包含在几个.cpp文件中,落入几个对象文件中,那么链接器在将这些对象链接在一起(例如链接到一个可执行文件)时会遇到问题,因为它们将包含相同符号的副本。据我所知,这正好等于当一个人在头文件中提供类声明下面的类外定义,然后在多个地方包含这个公共头文件时的情况。我记得,这会导致链接器错误。
然而,现在处理这个问题的责任转移到用户/程序员身上。如果想要一个static库,需要提供一个类外定义,将其编译成单独的对象文件,然后将所有其他对象链接到这个对象文件中,因此只有一个二进制符号的定义。
我阅读了Do we still need to separately define static members, even if they are initialised inside the class definition?Why can't I initialize non-const static member or static array in class?中的答案。
我仍然想知道:
  1. 这只是一个标准问题,还是有更深层次的原因?
  2. 是否可以通过constexpr和用户定义的文字机制解决这个问题。clang和g++都说变量不能具有非文字类型。也许我可以创造一个。(也许由于某种原因这也是一个坏主意)
  3. 对于链接器来说,只包含一个符号的副本真的是一个很大的问题吗?由于它是static const,所以所有副本应该是二进制完全相同的不可变的副本。
如果我漏掉或误解了什么,请也请评论。

当涉及到静态变量时,你只能在声明的时候初始化const整型和枚举类型。 - juanchopanza
@juanchopanza 我知道。我以为在C++11中它可以被克服。 - luk32
我认为你在第三点指出了其中的一个问题。是的,这是可行的,但它需要将此情况特殊处理,否则链接器就不会“喜欢”多个定义。 - ondrejdee
我从未理解这些限制的合理性。多次定义相同符号的问题也适用于模板实例化,因此链接器已经知道如何处理它(通过丢弃除一个之外的所有副本)。 - Nemo
如果它是const static,真的是“multiple”吗?难道不只是副本吗?为什么不选择一个?我对连接器的内部和外部、地址转换等方面不是很熟悉。也许有人知道为什么这么困难或被弃用的想法。坦率地说,以前我认为这是与类内初始化问题有关而不是多个符号,但似乎情况并非如此。 - luk32
1个回答

8
您的问题有两个方面。标准规定了什么?为什么会这样?
对于静态成员类型为const std::string,需要在类说明符之外定义,并且在一个翻译单元中有一个定义。这是C++标准第3条款中规定的“一个定义规则”的一部分。
但是,为什么呢?
问题在于具有静态存储期的对象需要在最终程序映像中具有唯一的静态存储,因此需要从一个特定的翻译单元链接。类说明符没有一个翻译单元的主页,它只定义类型(在使用它的所有翻译单元中需要相同的定义)。
常量整数不需要存储的原因是,编译器将其用作常量表达式,并在使用点处内联。它永远不会到达程序映像。
然而,具有静态存储期的复杂类型,例如std :: string,即使它们是const,也需要存储,因为它们可能需要动态初始化(在进入main之前调用其构造函数)。
您可以认为编译器应该在每个使用它们的翻译单元中存储具有静态存储期的对象的信息,然后链接器应该在链接时将这些定义合并为程序映像中的一个对象。我猜测为什么不这样做的原因是,这需要链接器具有过多的智能。

1
一个常量整数不需要存储的原因是,编译器将其用作常量表达式并在使用点处内联。它永远不会到达程序映像。除非我写&const_int_member,这需要存储。而我从未引用过的静态常量整数不需要存储。最后,模板要求链接器具有完全相同的“过度智能”。因此,就我所知,这整个论点在每个细节上都是无意义的。(并不是你一个人在提出这个论点...) - Nemo
@Nemo:执行间接引用(&const_int_member)会导致其成为odr-used,因此需要满足9.4.2p4中所述的要求:"如果程序中odr-used(3.2)了该成员,则该成员仍应在名称空间范围内定义,并且名称空间范围定义不应包含初始化器。"(即现在需要一个定义)。至于模板,它们有特殊的"实例化单元"instantiation units,在第8翻译阶段使用特殊规则。 - Andrew Tomazos
好的,但是一旦你将它“odr-used”,它就会有与任何用户定义类型相同的问题。因此,理由仍然站不住脚;即使您仍然需要在命名空间范围内的某个地方要求单个定义,也没有理由不允许在头文件中初始化任何类型。据我所知,“const int”和“const whatever”之间的区别在思考时真的毫无意义... - Nemo
@Nemo:不,它没有相同的问题。正如我之前所说,常量整数类型可以内联(无论是否odr-used,以及是否具有存储),而用户定义的类型在所有情况下都不能内联,因为它们需要动态初始化和存储。 - Andrew Tomazos
所以,设计一个漂亮的用户定义字面量可能没有解决方法。我已经知道,整数是一个非常特殊的例外,只因为它将被内联到汇编调用中,并且不需要存储在最终对象中。因此,您无法对其进行取消引用,而如果它在类外定义,则可以取消引用它。我认为其他答案甚至有例子。我也知道9.4.2。我只是想在c++11中以某种方式实现它。我的问题现在似乎有点多余了。也许有人会解释为什么编译器不能执行看似简单的工作,如果没有,我就接受这个事实。 - luk32

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