70

是否可以在头文件中写入以下内容:

inline void f () { std::function<void ()> func = [] {}; }

或者

class C { std::function<void ()> func = [] {}; C () {} };

我猜在每个源文件中,lambda 的类型可能不同,因此std::function(target_type的结果中包含的类型会有所不同)。

这是否违反了 ODR(单一定义规则),尽管看起来像是一种常见的模式和合理的做法?第二个样本是否每次都违反 ODR 或仅当至少一个构造函数位于头文件中时?

4

2 回答 2

41

这归结为 lambda 的类型是否因翻译单元而异。如果是这样,它可能会影响模板参数推导,并可能导致调用不同的函数——这意味着定义一致。这将违反 ODR(见下文)。

然而,这不是故意的。事实上,这个问题已经被核心问题 765提到过,它专门命名了具有外部链接的内联函数 - 例如f

7.1.2 [dcl.fct.spec] 第 4 段指定出现在具有外部链接的内联函数主体中的局部静态变量和字符串文字必须是程序中每个翻译单元中的相同实体。然而,关于本地类型是否同样需要相同,并没有说什么。

尽管符合标准的程序总是可以通过使用 typeid 来确定这一点,但最近对 C++ 的更改(允许本地类型作为模板类型参数,lambda 表达式闭包类)使这个问题更加紧迫。

2009 年 7 月会议记录:

类型应相同。

现在,该决议将以下措辞纳入[dcl.fct.spec]/4

在函数体中定义的extern inline类型在每个翻译单元中都是相同的类型。

(注意:MSVC 尚未考虑上述措辞,尽管它可能会在下一个版本中出现)。

因此,此类函数体内的 Lambda 是安全的,因为闭包类型的定义确实在块范围内([expr.prim.lambda]/3)。
因此,对的多个定义f曾经被很好地定义过。

这个解决方案当然不会涵盖所有场景,因为有更多种类的具有外部链接的实体可以使用 lambda,特别是函数模板 - 这应该由另一个核心问题来解决。
同时,Itanium 已经包含适当的规则来确保此类 lambdas 的类型在更多情况下一致,因此 Clang 和 GCC 应该已经按预期运行。


以下是关于为什么不同的闭包类型违反 ODR 的标准规范。考虑[basic.def.odr]/6中的要点 (6.2) 和 (6.4) :

[…] 的定义可能不止一个。给定一个D在多个翻译单元中定义的实体,则每个定义D应由相同的标记序列组成;和

(6.2) - 在 的每个定义中D对应的名称,根据 [basic.lookup] 查找,应指在 的定义中定义的实体D,或应指同一实体,在重载决议后 ([over.match] )和部分模板特化匹配后 ([temp.over]), […]; 和

(6.4) - 在 的每个定义中D,所指的重载运算符、对转换函数、构造函数、运算符新函数和运算符删除函数的隐式调用,应指同一函数,或在定义中定义的函数 ;[…]D

这实际上意味着在实体定义中调用的任何函数在所有翻译单元中都应相同 -或者已在其定义中定义,如本地类及其成员。即使用 lambda 本身没有问题,但将其传递给函数模板显然是有问题的,因为这些是在定义之外定义的。

在您的示例中C,闭包类型是在类中定义的(其范围是最小的封闭类)。如果闭包类型在两个 TU 中不同,标准可能无意中暗示了闭包类型的唯一性,则构造函数实例化并调用function的构造函数模板的不同特化,违反上述引用中的 (6.4)。

于 2016-01-11T12:16:31.083 回答
8

更新

毕竟我同意@Columbo 的回答,但想加上实用的五美分 :)

尽管违反 ODR 听起来很危险,但在这种特殊情况下,这并不是一个真正的严重问题。在不同的 TU 中创建的 lambda 类是等价的,除了它们的 typeid。因此,除非您必须处理标头定义的 lambda(或取决于 lambda 的类型)的 typeid,否则您是安全的。

现在,当 ODR 违规被报告为错误时,它很有可能会在有问题的编译器中得到修复,例如 MSVC 以及可能不遵循 Itanium ABI 的其他一些编译器。请注意,符合 Itanium ABI 的编译器(例如 gcc 和 clang)已经在为头文件定义的 lambda 生成 ODR 正确的代码。

于 2016-01-12T15:09:05.750 回答