【问题标题】:Can consteval functions from different translation units interfere?来自不同翻译单元的 consteval 函数会干扰吗?
【发布时间】:2020-02-15 13:53:35
【问题描述】:

我试图深入研究 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

这可能不是我们想要的,但至少我可以解释。

  1. 编译器不需要在编译时计算constexpr 函数的结果,因此它决定将其推迟到运行时。
  2. constexpr 函数暗示 inline
  3. 我们的 get() 函数恰好有不同的实现
  4. 我们没有将 get() 函数声明为静态
  5. 链接器只能选择get() 函数的一种实现

碰巧链接器从main.cpp中选择了get(),返回3。

现在到我不明白的部分。我只是get() 函数从constexpr 更改为consteval。现在编译器需要在编译期间计算该值,即在链接时间之前(对吗?)。我希望 get() 函数根本不会出现在目标文件中。

但是当我运行它时 (demo),我的输出完全相同!这怎么可能?.. 我的意思是,我知道这是未定义的行为,但这不是重点。为什么应该在编译时计算的值会干扰其他翻译单元?

UPD:我知道这个功能is listed as unimplemented in clang,但问题仍然适用。是否允许符合标准的编译器表现出这种行为?

【问题讨论】:

  • UB 意味着 anything 可能发生在程序的 any 部分。在尝试推理任何事情之前,首先摆脱任何 UB。

标签: c++ constexpr c++20 one-definition-rule consteval


【解决方案1】:

具有相同内联函数的两个定义的程序是格式错误的程序,不需要诊断。

该标准对格式错误的程序的运行时或编译时行为没有任何要求。

现在,C++ 中没有您想象的“编译时间”。虽然几乎每个 C++ 实现都会编译文件、链接它们、构建二进制文件然后运行它,但 C++ 标准对这一事实采取了措施。

它讨论了翻译单元,以及将它们组合到一个程序中会发生什么,以及该程序的运行时行为是什么。

...

实际上,您的编译器可能正在构建从符号到某个内部结构的映射。它正在编译您的第一个文件,然后在第二个文件中它仍在访问该地图。同一个内联函数的新定义?跳过它。

其次,您的代码必须生成编译时常量表达式。但是编译时常量表达式在您使用它的上下文中不是可观察的属性,并且在链接甚至运行时执行它没有副作用!而且好像没有什么可以阻止它。

consteval 是说“如果我运行它并且违反了允许它成为常量表达式的规则,我应该出错而不是退回到非常量表达式”。这类似于“必须在编译时运行”,但又不一样。

要确定发生了哪些情况,请尝试以下操作:

template<auto x>
constexpr std::integral_constant< decltype(x), x > constant = {};

现在将您的打印行替换为:

std::cout << constant<get()> << std::endl;

这使得推迟运行/链接时间的评估变得不切实际。

这将区分“编译器很聪明并缓存get”和“编译器稍后在链接时对其进行评估”,因为确定要调用哪个ostream&amp; &lt;&lt;需要实例化constant&lt;get()&gt;的类型,其中转需要评估get()

编译器往往不会将重载解决方案推迟到链接时间。

【讨论】:

  • 这是一个很好的解释,但我仍然不相信,对不起。请参阅gcc.godbolt.org/z/rb_X2W,其中consteval f() 在运行时被明确调用。你是想说这是一个正确的行为吗?我认为这是一个错误,类似于这个:stackoverflow.com/q/58454657/261217
  • @Mikhail,Clang 列出了未实现的即时功能,正如您在错误报告中看到的那样。使用未实现的功能的结果并不是特别引人注目。
  • @chris 这就是我首先提出这个问题的原因,因为我没有可以依赖的实现。
【解决方案2】:

consteval 函数的要求是每次调用它都必须产生一个常量表达式。

一旦编译器确信调用确实产生了一个常量表达式,它就不必对函数进行代码生成并在运行时调用它。当然,对于某些consteval 函数(例如那些为反射而设想的函数)最好不要这样做(至少如果它不想将其所有内部数据结构放入目标文件中),但这不是一般要求.

未定义的行为是未定义的。

【讨论】:

【解决方案3】:

对此的答案是,无论函数是 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
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-02
    • 1970-01-01
    • 1970-01-01
    • 2012-08-08
    相关资源
    最近更新 更多