我正在尝试深入研究inline
函数的含义,并偶然发现了这个问题。考虑这个小程序(演示(:
/* ---------- main.cpp ---------- */
void other();
constexpr int get()
{
return 3;
}
int main()
{
std::cout << get() << std::endl;
other();
}
/* ---------- other.cpp ---------- */
constexpr int get()
{
return 4;
}
void other()
{
std::cout << get() << std::endl;
}
在没有优化的情况下编译时,程序会产生以下输出:
3
3
这可能不是我们想要的,但至少我可以解释一下。
- 编译器不需要在编译时计算
constexpr
函数的结果,因此它决定将其推迟到运行时。 - 关于函数的
constexpr
意味着inline
- 我们的
get()
函数碰巧有不同的实现 - 我们没有声明
get()
函数是静态的 - 链接器必须仅选择
get()
函数的一个实现
碰巧的是,链接器从main.cpp
中选择了get()
,返回3。
现在到我没有得到的部分。我只是将get()
功能从constexpr
更改为consteval
。现在编译器需要在编译时计算值,即在链接时间之前(对吗?我希望get()
函数根本不存在于目标文件中。
但是当我运行它(演示(时,我的输出完全相同!这怎么可能?..我的意思是,是的,我知道这是未定义的行为,但这不是重点。为什么应该在编译时计算的值会干扰其他翻译单元?
UPD:我知道这个功能在 clang 中被列为未实现,但这个问题无论如何都是适用的。是否允许符合标准的编译器表现出这种行为?
具有相同内联函数的两个定义的程序是格式不正确的程序,无需诊断。
该标准对格式错误的程序的运行时或编译时行为没有要求。
现在,C++中没有您想象中的"编译时间"。 虽然几乎每个C++实现都会编译文件,链接它们,构建二进制文件,然后运行它,但C++标准都围绕这一事实。
它讨论了翻译单元,以及将它们组合到程序中时会发生什么,以及该程序的运行时行为是什么。
。
实际上,编译器可以构建从符号到某些内部结构的映射。 它正在编译您的第一个文件,然后在第二个文件中它仍在访问该地图。 同一内联函数的新定义? 跳过它。
其次,代码必须生成编译时常量表达式。 但是编译时常量表达式在您使用它的上下文中不是可观察的属性,并且在链接甚至运行时执行此操作没有任何副作用! 在好像没有什么可以阻止的。
consteval
说"如果我运行它并且违反了允许它成为常量表达式的规则,我应该出错而不是依靠非常量表达式"。 这类似于"它必须在编译时运行",但它并不相同。
要确定正在发生以下情况,请尝试以下操作:
template<auto x>
constexpr std::integral_constant< decltype(x), x > constant = {};
现在将您的打印行替换为:
std::cout << constant<get()> << std::endl;
这使得将评估推迟到运行/链接时间是不切实际的。
这将区分"编译器很聪明和缓存get
"和"编译器稍后在链接时评估它",因为确定要调用哪个ostream& <<
需要实例化constant<get()>
的类型,这反过来又需要评估get()
。
编译器往往不会将重载解析延迟到链接时间。
对consteval
函数的要求是每次调用它都必须产生一个常量表达式。
一旦编译器确信调用确实生成了常量表达式,就不需要它不得对函数进行编码并在运行时调用它。当然,对于某些consteval
函数(如那些设想用于反射的函数(,它最好不要这样做(至少如果它不想将所有内部数据结构放入目标文件中(,但这不是一般要求。
未定义的行为是未定义的。
答案是,无论函数是constexpr
还是consteval
,它仍然是ODR违规。也许使用特定的编译器和特定的代码,您可能会得到您期望的答案,但它仍然格式不正确,不需要诊断。
您可以做的是在匿名命名空间中定义它们:
/* ---------- main.cpp ---------- */
void other();
namespace {
constexpr int get()
{
return 3;
}
}
int main()
{
std::cout << get() << std::endl;
other();
}
/* ---------- other.cpp ---------- */
namespace {
constexpr int get()
{
return 4;
}
}
void other()
{
std::cout << get() << std::endl;
}
但更好的是,只需使用模块:
/* ---------- main.cpp ---------- */
import other;
constexpr int get()
{
return 3;
}
int main()
{
std::cout << get() << std::endl; // print 3
other();
}
/* ---------- other.cpp ---------- */
export module other;
constexpr int get() // okay, module linkage
{
return 4;
}
export void other()
{
std::cout << get() << std::endl; // print 4
}