考虑以下具有两个编译单元的程序。
// 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) 的匹配,但名称可以引用非易失性 具有内部链接或无链接的 const 对象,如果该对象在D
的所有定义中具有相同的文本类型, 并且对象使用常量表达式 (5.19) 初始化,并且对象未使用 ODR,并且 对象在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)、运算符函数 ID (13.5)、文本运算符 ID (13.5.8)、转换 函数 ID (12.3.2) 或模板 ID (14.2),表示实体或标签 (6.6.4、6.1)。
标识符是
[lex.name][2.11]
标识符是任意长的字母和数字序列。
所以字符串文字似乎不是名称。
另一方面在第5节
[expr.prim.general][5.1.1.1]
字符串文字是一个左值;所有其他 文字是代理值。
一般来说,我认为lvalues
有名字。
你的最后一个论点是无稽之谈。"foo"
在语法上甚至不是一个名称,而是一个字符串字面。字符串文字是左值,一些左值有名称并不意味着字符串文字是或有名称。代码中使用的字符串文本不违反 ODR。
实际上,直到 C++11 年,它要求跨 TU 的内联函数的多个定义的字符串文字指定同一个实体,但 CWG 1823 删除了这个多余且大部分未实现的规则。
因为,名称
"foo"
可能与 两个编译单元 - 它可能是一个"不同"的字符串文字 在每个中,不是吗?
正确,但这无关紧要。因为 ODR 不关心特定的参数值。如果您确实设法以某种方式在两个 TU 中调用不同的函数模板专用化,那将是有问题的,但幸运的是字符串文字是无效的模板参数,因此您必须聪明。