You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++14下内联函数使用constexpr变量的ODR违规判定问题

注:本问题仅针对C14标准,即C17引入inline变量特性之前的规则

核心问题
  • 内联函数定义中使用的constexpr变量,满足何种情况会被判定为odr-used,从而导致该函数的多定义违反[basic.def.odr]/6?

(相关规则大概率涉及[basic.def.odr]/3;如果在内联函数定义上下文内取这类constexpr变量的地址,是否会静默引入程序UB?)

示例问题:若doMath()定义如下:

// some_math.h
#pragma once

// 编码规范要求避免直接使用字面量
constexpr int kTwo{2};
inline int doMath(int arg) { return std::max(arg, kTwo); }
                                 // std::max(const int&, const int&)

当doMath()通过包含头文件的方式在两个不同翻译单元中定义并被使用时,程序是否存在未定义行为?

背景说明

参考如下示例代码:

// constants.h
#pragma once
constexpr int kFoo{42};

// foo.h
#pragma once
#include "constants.h"
inline int foo(int arg) { return arg * kFoo; }  // #1: kFoo未被odr-used

// a.cpp
#include "foo.h"
int a() { return foo(1); }  // foo被odr-used

// b.cpp
#include "foo.h"
int b() { return foo(2); }  // foo被odr-used

上述代码基于C++14标准编译,该版本中尚未引入inline变量特性,因此constexpr变量不会被隐式标记为inline。

具有外部链接的内联函数foo在a.cpp和b.cpp对应的两个翻译单元TU_a、TU_b中均被odr-used,因此需要在两个TU中都提供定义(参考[basic.def.odr]/4)。

[basic.def.odr]/6规定了这类多翻译单元多定义场景的合法要求,其中核心相关条款为/6.1和/6.2:

程序中可以存在多份[...]具有外部链接的内联函数[...]的定义,前提是每个定义出现在不同翻译单元中,且所有定义满足以下要求:若名为D的实体在多个翻译单元中定义,则:

  • /6.1 D的每个定义由相同的标记序列构成;
  • /6.2 D的每个定义中,按[basic.lookup]规则查找到的对应名称,在重载决议([over.match])和偏特化匹配([temp.over])后,应指向D定义内的实体,或指向同一个实体;例外情况:名称可以指向具有内部或无链接的非易失性const对象,前提是该对象在D的所有定义中字面类型相同、由常量表达式([expr.const])初始化、未被odr-used、且在D的所有定义中值相同;
  • ...

若D的定义不满足上述要求,则行为未定义。

上述示例中/6.1要求已满足。

/6.2的例外条款要求kFoo满足以下条件才能合规:

  1. [满足] 是具有内部链接的const对象
  2. [满足] 由常量表达式初始化
  3. [满足] 在foo的所有定义中字面类型一致
  4. [满足] 在foo的所有定义中值一致
  5. [待确认] 未被odr-used。

此处第5条应解读为“在foo的定义内未被odr-used”,标准措辞可以更明确。若kFoo(至少在foo定义内)被判定为odr-used,则违反[basic.def.odr]/6,触发ODR违规,导致程序未定义行为。

根据[basic.def.odr]/3的规则判定kFoo是否被odr-used:

变量x的名称出现在潜在求值表达式ex中时,x被ex odr-used,除非对x应用左值到右值转换([conv.lval])可以得到不调用任何非平凡函数的常量表达式([expr.const]);且如果x是对象,ex是表达式e的潜在结果集合的元素,其中e要么被应用了左值到右值转换([conv.lval]),要么是弃值表达式([expr]条款)。[...]

当前疑问点:如果在foo的定义内取kFoo的地址,或者在foo定义外取其地址,是否会影响[basic.def.odr]/6.2的合规性?


补充细节

若foo按如下方式定义:

// #2
inline int foo(int arg) { 
    std::cout << "&kFoo in foo() = " << &kFoo << "\n";
    return arg * kFoo; 
}

a()和b()定义如下:

int a() { 
    std::cout << "TU_a, &kFoo = " << &kFoo << "\n";
    return foo(1); 
}

int b() { 
    std::cout << "TU_b, &kFoo = " << &kFoo << "\n";
    return foo(2); 
}

依次调用a()和b()得到的输出如下:

TU_a, &kFoo    = 0x401db8
&kFoo in foo() = 0x401db8  // <-- TU_a中的foo()取到TU_a的kFoo地址

TU_b, &kFoo    = 0x401dbc
&kFoo in foo() = 0x401db8  // <-- TU_b中的foo()意外取到TU_a的kFoo地址

即在非内联函数a()、b()中访问kFoo时,得到的是对应TU本地的kFoo地址,但在内联函数foo()中访问时,得到的是同一个kFoo地址。

按本部分定义的foo、a、b编写的程序是否存在未定义行为?

实际开发中的常见场景:用constexpr变量定义数学常量,在内联函数定义内将其作为参数传递给std::max()这类按引用传参的工具函数。


内容的提问来源于stack exchange,提问作者dfrib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 00:18:03