C++:将 constexpr lambda 初始化为 constexpr/non-constexpr 变量

C++: initializing constexpr lambda to constexpr/non-constexpr variable

我正在阅读 Nicolai M. Josuttis 的《C++ 17 完整指南》一书,无法理解以下示例

auto squared1 = [](auto val) constexpr { // example 1. compile-time lambda calls
  return val * val;
};

及其声明

If (only) the lambda is constexpr it can be used at compile time, but squared1 might be initialized at run time, which means that some problems might occur if the static initialization order matters (e.g, causing the static initialization order fiasco).

作者建议考虑以下方案

constexpr auto squared = [](auto val) constexpr {
  return val * val;
};

意味着它会以某种方式避免前面的问题。

我不明白第一个例子关于初始化顺序的问题,因此无法理解作者的解决方案如何改进它。您能否解释一下并举一个例子来说明第一个例子的缺点?

语句auto square1 = 初始化一个全局变量,甚至不是常量。这是一个运行时变量,您可以对其进行变异、分配以及可能的其他内容。

你可以像这样初始化这样的变量:

auto returnMeALambda() {
    int capture = rand() % 2;

    return [capture](auto val) {
        return val * val + capture;
    }
}

auto square1 = returnMeALambda();

可以看到,returnMeALambda的代码是严格运行时的,所以square1是在运行时强制初始化的。

这些变量没有在编译时可用的值。编译器可能会在运行时很好地初始化它,即使不是被迫的。这会在运行时产生成本,并且使用 static initialization order fiasco,您可以在技术上在初始化之前使用 lambda,甚至在初始化之前使用另一个全局变量:

extern int baseval;

auto returnMeALambda() {
    int capture = baseval + rand() % 2;

    return [capture](auto val) {
        return val * val + capture;
    }
}

auto square1 = returnMeALambda();

int baseval = square1(2);

此代码将始终以未定义行为结束,因为它始终使用未初始化的变量,无论初始化顺序如何。

作者提出的解决方案是将变量初始化为constexpr。在这种情况下这会做三件事:

  • 使变量const。你不能在运行时改变它的值。
  • 现在强制执行常量初始化。 square1 的值编码在可执行二进制代码中。
  • 使变量值在编译时可用。您现在可以在 constexpr 上下文中使用该变量。

第二点是属性作者寻求的解决方案。该变量保证它永远不会在运行时初始化,因为该值在编译时可用并保证由编译器进行常量初始化。


请注意,在 C++20 中,您可以单独应用第二点,而无需使用 constinit.

将值强制为 const
constinit int value = 9;

现在编译器被迫使用常量初始化来初始化值,而且变量在运行时是可变的。这有效地解决了初始化顺序失败的问题,因为您可以不断地初始化变量。