问题

我无法理解此错误的实质,所以赦免我,如果标题可能更好.此代码无法编译:

 template <auto v>
struct value_as_type {
    using type = decltype(v);    
    static constexpr type value {v};
    constexpr operator type() const {
        return v;
    }
};

template <int First, int Last, typename Functor>
constexpr void static_for([[maybe_unused]] Functor&& f)
{
    if constexpr (First < Last)
    {
        f(value_as_type<First>{});
        static_for<First + 1, Last, Functor>(std::forward<Functor>(f));
    }
}

template <class... FieldsSequence>
struct DbRecord
{
private:
    static constexpr bool checkAssertions()
    {
        static_assert(sizeof...(FieldsSequence) > 0);
        static_for<1, sizeof...(FieldsSequence)>([](auto&& index) {
            constexpr int i = index;
            static_assert(i > 0 && i < sizeof...(FieldsSequence));
        });

        return true;
    }

private:
    static_assert(checkAssertions());
};
 

断层线是constexpr int i = index;,错误是“表达式不评估常数”.

为什么会这样?我希望调用value_as_type<int>对象的转换运算符.最让人困惑的是,如果lambda采用auto index而不是auto&& index,它确实工作正常.

在线演示: https://godbolt.org/z/TffIIn

  最佳答案

这是一个较短的复制,考虑使用ACCEPT编译的程序与没有以下内容的程序之间的区别:

 struct One { constexpr operator int() const { return 1; } };

template <typename T>
constexpr int foo(T&& t) {
#ifdef ACCEPT
    return t;
#else
    constexpr int i = t;
    return i;
#endif
}

constexpr int i = foo(One{});
 

正如我对宏的选择可能建议的那样,ACCEPT案例是可以的,另一个案例是ill-framed.为什么?有关的规则是 [expr.const] / 4.12 :

表达式 e 是一个核心常量表达式,除非按照抽象机器的规则对 e 进行评估,否则将评估以下一个: [...] 一个引用参考类型的变量或数据成员的 id-expression,除非引用具有先前的初始化和 [...]

之前的初始化是什么?在我回答之前,lemme提供了一个不同的程序,并通过它的语义必须是:

矛盾

 struct Int { constexpr operator int() const { return i; } int i; };
template <int> struct X { };

template <typename T>
constexpr auto foo(T&& t) {
    constexpr int i = t;
    return X<i>{};
}

constexpr auto i = foo(Int{1});
constexpr auto j = foo(Int{2});
 

只有一个函数foo<Int>,所以它必须有一个特定的返回类型.如果允许这个程序,那么foo(Int{1})将返回X<1>foo(Int{2})将返回X<2> – 也就是说,foo<Int>可以返回不同的类型?这不能发生,所以这必须是ill-formate.

最小的盒子

当我们处于需要常量表达式的情况下,将其视为打开一个新框.该框中的所有内容都必须满足常量评估的规则,好像我们刚刚从那一点开始.如果我们需要一个嵌套在该框中的新的常量表达式,我们会打开一个新框.框一直下来.

在原始复制(使用One)和新的复制(使用Int)中,我们有这个声明:

 constexpr int i = t;
 

这打开了一个新框.初始化器t必须满足常量表达式的限制. t是一个引用类型,但在此框中没有先前的初始化,因此这是ill-formate.

现在在公认的情况下:

 struct One { constexpr operator int() const { return 1; } };

template <typename T>
constexpr int foo(T&& t) {
    return t;
}

constexpr int i = foo(One{});
 

我们只有一个框:全局i的初始化.在该框中,我们仍然评估参考类型的id-expression,在该return t;中,但在这种情况下,我们的框中确实有一个先前的初始化:我们将t绑定到One{}.所以这有效.没有矛盾可以从这些规则构建.事实上,这也很好:

 constexpr int j = foo(Int{1});
constexpr int k = foo(Int{2});
static_assert(i+k == 3);
 

因为我们每次都只有一个入口进入常量评估,在该评估中,引用t具有先前的初始化,Int的成员也可用于常量表达式.

回到执行部分

删除引用有效,因为我们不再违反引用限制,也没有任何其他限制我们可以违反.我们没有读取任何可变状态或任何东西,转换函数只返回一个常数.

我们尝试将Int{1}按值传递给foo的类似例子仍然会失败 – 这次不是对于参考规则,而是对于lvalue-torvalue转换规则.基本上,我们正在阅读我们无法读取的东西 – 因为我们最终会出现同样的矛盾,即能够构建具有多个返回类型的函数.