在头文件中使用lambda会违反ODR

Can using a lambda in header files violate the ODR?

本文关键字:ODR lambda 文件      更新时间:2023-10-16

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

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

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

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

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

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

然而,这并不是故意的。事实上,核心问题765不久前就已经触及了这个问题,它专门命名了具有外部链接的内联函数,例如f:

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

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

2009年7月会议的说明:

类型应相同。

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

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

(注意:微软风险投资公司还没有考虑到上述措辞,尽管可能会在下一个版本中)。

因此,此类函数体中的lambda是安全的,因为闭包类型的定义确实在块范围内([expr.prim.lambda]/3)。
因此CCD_ 5的多个定义一直是明确的。

这个解决方案当然不涵盖所有场景,因为有更多种类的具有外部链接的实体可以使用lambdas,尤其是函数模板——这应该由另一个核心问题来涵盖
同时,安腾已经包含了适当的规则,以确保在更多情况下此类Lambda的类型一致,因此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)。

更新

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

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

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