来自不同翻译单位的constval函数会相互干扰吗?
Can consteval functions from different translation units interfere?
我试图深入研究一个函数的含义 inline
并偶然发现了这个问题。考虑这个小程序(demo):
/* ---------- 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()
静态函数
- linker 必须只选择
get()
函数的一种实现方式
碰巧 linker 从 main.cpp
中选择了 get()
,返回了 3。
现在说到我没听懂的部分。我只是 将 get()
函数从 constexpr
更改为 consteval
。现在编译器需要在编译时计算值,即在 link 时间之前(对吗?)。我希望 get()
函数根本不会出现在目标文件中。
但是当我运行它(demo)时,我有完全相同的输出!这怎么可能?..我的意思是,我知道这是未定义的行为,但这不是重点。为什么应该在编译时计算的值会干扰其他翻译单元?
UPD: 我知道这个功能 is listed as unimplemented in clang,但这个问题仍然适用。是否允许符合标准的编译器表现出这种行为?
consteval
函数的要求是每次调用它都必须生成常量表达式。
一旦编译器确定调用确实会产生一个常量表达式,就没有要求它不得在 运行 时间对函数进行代码生成并调用它。当然,对于某些 consteval
函数(比如那些为反射而设想的函数),最好不要这样做(至少如果它不想将其所有内部数据结构放入目标文件),但这不是一般要求。
未定义的行为是未定义的。
具有相同内联函数的两个定义的程序是格式错误的程序,不需要诊断。
该标准对格式错误程序的运行时间或编译时行为没有任何要求。
现在,C++ 中没有您想象的 "compile time"。虽然几乎每个 C++ 实现都编译文件,links 它们,构建一个二进制文件,然后 运行s 它,C++ 标准小心翼翼地围绕这个事实。
它讨论翻译单元,将它们放在一个程序中时会发生什么,以及该程序的运行时间行为是什么。
...
实际上,您的编译器可能正在构建从符号到某个内部结构的映射。它正在编译您的第一个文件,然后在第二个文件中它仍在访问该地图。同一个内联函数的新定义?跳过它。
其次,您的代码必须生成编译时常量表达式。但是编译时常量表达式 在您使用它的上下文中不是可观察的 属性 ,并且在 link 甚至没有副作用运行时间!并且好像没有什么可以阻止它。
consteval
表示 "if I run this and the rules that permit it being a constant expression are violated, I should error rather than fall back on non-constant expression"。这与"it must be run at compile time"相似,但不一样。
要确定发生了哪些情况,请尝试以下操作:
template<auto x>
constexpr std::integral_constant< decltype(x), x > constant = {};
现在将您的打印行替换为:
std::cout << constant<get()> << std::endl;
这使得将评估推迟到 run/link 时间变得不切实际。
这将区分 "compiler is being clever and caching get
" 和 "compiler is evaluating it later at link time",因为确定要调用哪个 ostream& <<
需要实例化 constant<get()>
的类型,而这又需要评估 get()
.
编译器往往不会将重载决策推迟到 link 时间。
答案是它仍然是 ODR 违规,无论函数是 constexpr
还是 consteval
。也许使用特定的编译器和特定的代码你可能会得到你期望的答案,但它仍然是错误的,不需要诊断。
您可以做的是在匿名命名空间中定义它们:
/* ---------- 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
}
我试图深入研究一个函数的含义 inline
并偶然发现了这个问题。考虑这个小程序(demo):
/* ---------- 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()
静态函数 - linker 必须只选择
get()
函数的一种实现方式
碰巧 linker 从 main.cpp
中选择了 get()
,返回了 3。
现在说到我没听懂的部分。我只是 将 get()
函数从 constexpr
更改为 consteval
。现在编译器需要在编译时计算值,即在 link 时间之前(对吗?)。我希望 get()
函数根本不会出现在目标文件中。
但是当我运行它(demo)时,我有完全相同的输出!这怎么可能?..我的意思是,我知道这是未定义的行为,但这不是重点。为什么应该在编译时计算的值会干扰其他翻译单元?
UPD: 我知道这个功能 is listed as unimplemented in clang,但这个问题仍然适用。是否允许符合标准的编译器表现出这种行为?
consteval
函数的要求是每次调用它都必须生成常量表达式。
一旦编译器确定调用确实会产生一个常量表达式,就没有要求它不得在 运行 时间对函数进行代码生成并调用它。当然,对于某些 consteval
函数(比如那些为反射而设想的函数),最好不要这样做(至少如果它不想将其所有内部数据结构放入目标文件),但这不是一般要求。
未定义的行为是未定义的。
具有相同内联函数的两个定义的程序是格式错误的程序,不需要诊断。
该标准对格式错误程序的运行时间或编译时行为没有任何要求。
现在,C++ 中没有您想象的 "compile time"。虽然几乎每个 C++ 实现都编译文件,links 它们,构建一个二进制文件,然后 运行s 它,C++ 标准小心翼翼地围绕这个事实。
它讨论翻译单元,将它们放在一个程序中时会发生什么,以及该程序的运行时间行为是什么。
...
实际上,您的编译器可能正在构建从符号到某个内部结构的映射。它正在编译您的第一个文件,然后在第二个文件中它仍在访问该地图。同一个内联函数的新定义?跳过它。
其次,您的代码必须生成编译时常量表达式。但是编译时常量表达式 在您使用它的上下文中不是可观察的 属性 ,并且在 link 甚至没有副作用运行时间!并且好像没有什么可以阻止它。
consteval
表示 "if I run this and the rules that permit it being a constant expression are violated, I should error rather than fall back on non-constant expression"。这与"it must be run at compile time"相似,但不一样。
要确定发生了哪些情况,请尝试以下操作:
template<auto x>
constexpr std::integral_constant< decltype(x), x > constant = {};
现在将您的打印行替换为:
std::cout << constant<get()> << std::endl;
这使得将评估推迟到 run/link 时间变得不切实际。
这将区分 "compiler is being clever and caching get
" 和 "compiler is evaluating it later at link time",因为确定要调用哪个 ostream& <<
需要实例化 constant<get()>
的类型,而这又需要评估 get()
.
编译器往往不会将重载决策推迟到 link 时间。
答案是它仍然是 ODR 违规,无论函数是 constexpr
还是 consteval
。也许使用特定的编译器和特定的代码你可能会得到你期望的答案,但它仍然是错误的,不需要诊断。
您可以做的是在匿名命名空间中定义它们:
/* ---------- 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
}