【发布时间】:2016-10-20 11:30:42
【问题描述】:
考虑以下具有两个编译单元的程序。
// a.hpp
class A {
static const char * get() { return "foo"; }
};
void f();
// a.cpp
#include "a.hpp"
#include <iostream>
void f() {
std::cout << A::get() << std::endl;
}
// main.cpp
#include "a.hpp"
#include <iostream>
void g() {
std::cout << A::get() << std::endl;
}
int main() {
f();
g();
}
出于某种原因需要创建全局字符串常量是很常见的。以完全幼稚的方式执行此操作会导致链接器问题。通常,人们将声明放在头文件中,将定义放在单个编译单元中,或者使用宏。
我一直认为用函数执行此操作的方式(如上所示)是“可以的”,因为它是一个 inline 函数,并且链接器消除了生成的任何重复副本,并且使用编写的程序这种模式似乎工作正常。但是,现在我怀疑它是否真的合法。
函数A::get 在两个不同的翻译单元中使用,但它是隐式内联的,因为它是一个类成员。
在[basic.def.odr.6] 中声明:
... 内联函数可以有多个定义 外部链接(7.1.2)...在程序中提供每个定义 出现在不同的翻译单元中,并且只要定义满足以下要求。给定 这样一个名为
D的实体在多个翻译单元中定义,则
-D的每个定义应由相同的令牌序列组成;和
- 在D的每个定义中,对应的名字,根据3.4查找,应该是指定义的实体 在D的定义中,或在重载决议 (13.3) 之后和之后指代同一实体 部分模板特化 (14.8.3) 的匹配,除了名称可以引用非易失性 如果对象在D的所有定义中具有相同的文字类型,则具有内部链接或没有链接的 const 对象, 并且该对象使用常量表达式(5.19)进行初始化,并且该对象不是odr-used,并且 对象在D的所有定义中具有相同的值;和
- 在D的每个定义中,对应的实体应具有相同的语言链接;和
- ...(更多似乎不相关的条件)如果
D的定义满足所有这些要求, 那么程序的行为就好像有一个D的定义。如果D的定义不满足 这些要求,则行为未定义。
在我的示例程序中,两个定义(每个翻译单元中的一个)分别对应于相同的标记序列。 (这就是为什么我最初认为它还可以。)
但是,不清楚是否满足第二个条件。因为,名称 "foo" 可能不对应两个编译单元中的同一个对象——它可能是每个“不同”的字符串文字,不是吗?
我尝试更改程序:
static const void * get() { return static_cast<const void*>("foo"); }
以便它打印字符串文字的地址,我得到相同的地址,但是我不确定这是否会发生。
它是否属于“...应引用在D 的定义中定义的实体”? "foo" 是否被认为是在 A::get 中定义的?看起来可能是这样,但正如我非正式地理解的那样,字符串文字最终会导致编译器发出某种全局 const char[] ,它位于可执行文件的特殊段中。该“实体”是否被视为在 A::get 内,还是不相关?
"foo" 是否甚至被视为“名称”,还是“名称”一词仅指有效的 C++“标识符”,例如可用于变量或函数?一方面它说:
[basic][3.4]
名称是标识符 (2.11)、operator-function-id (13.5)、literal-operator-id (13.5.8)、conversion- function-id (12.3.2) 或 template-id (14.2),表示实体或标签 (6.6.4, 6.1)。
一个标识符是
[lex.name][2.11]
标识符是任意长的字母和数字序列。
所以看起来字符串文字不是名称。
另一方面,在第 5 节中
[expr.prim.general][5.1.1.1]
字符串文字是左值;所有其他 文字是纯右值。
一般来说,我认为lvalues 有名字。
【问题讨论】:
-
由于这被标记为 [language-lawyer],我将留给跟踪相关规范引用的人来回答;但是 FWIW,“名称”基本上意味着“可以声明的东西”——比如,变量名、函数名、类名等——字符串文字当然 not i> 名称。 (有关更正式的枚举,请参阅 en.cppreference.com/w/cpp/language/identifiers#Names。)
-
这是一个学术问题。因此,目前创建新标准的人对此非常感兴趣,他们显然想不出如果有一个简单的解决方案来堵住漏洞(我认为这个过程在 C++11 和 C++14 之间的某个地方被犯规了) )。在实践中,
inline函数会产生一个可丢弃的链接器记录,并且链接器只选择一个定义,这在实践中保证了字符串的唯一性——至少如果机器代码内联优化尊重该行为,就像它所做的那样。 -
如果你想查看考虑因素,我记得我上次在关于
inline数据的提案中看到了这个讨论(他们没有考虑堵住漏洞)。 -
inline函数的替代方法是使用模板化常量技巧。它依赖于模板类中静态常量的 ODR 豁免。而且它需要 machinery 用于内联数据,只是不能直接使用。 -
"foo"不是名称。并非所有左值都有名称。
标签: c++ c++11 c++14 language-lawyer one-definition-rule