【问题标题】:Strings in headers -- does this violate the ODR?标题中的字符串——这是否违反了 ODR?
【发布时间】: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


【解决方案1】:

你的最后一个论点是无稽之谈。 "foo" 在语法上甚至不是一个名字,而是一个 string-literal。并且字符串文字是左值并且某些具有名称的左值并不意味着字符串文字是或具有名称。代码中使用的字符串文字不违反 ODR。

实际上,直到 C++11 之前,都要求跨 TU 的内联函数的多个定义中的字符串文字指定相同的实体,但 CWG 1823 删除了该多余且大部分未实现的规则。

因为,名称"foo" 可能不对应于同一个对象 两个编译单元——它可能是一个“不同的”字符串文字 在每个,不是吗?

正确,但这无关紧要。因为 ODR 不关心特定的参数值。如果您确实设法以某种方式获得了不同的结果,例如要在两个 TU 中调用的函数模板特化,这将是有问题的,但幸运的是字符串文字是无效的模板参数,所以你必须很聪明。

【讨论】:

  • 我猜想成为一个名字最显着的特征是名字解析发生了。显然,命名空间、隐藏等不适用于"foo",所以这对我来说应该是一个很大的暗示,它不是一个名字。
猜你喜欢
  • 2021-06-19
  • 2021-12-08
  • 1970-01-01
  • 2014-02-15
  • 2010-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多