在转发引用上调用时,自定义类型转换运算符不起作用(当按值传递对象时起作用)

Vio*_*ffe 5 c++ rvalue-reference constexpr pass-by-rvalue-reference c++17

我无法理解此错误的实质,因此,如果标题更好,请原谅。此代码无法编译:

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());
};
Run Code Online (Sandbox Code Playgroud)

错误行是constexpr int i = index;,错误是“表达式未求得常数”。

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

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

Bar*_*rry 2

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{});
Run Code Online (Sandbox Code Playgroud)

正如我对宏的选择可能表明的那样,这种ACCEPT情况是好的,而另一种情况则格式不正确。为什么?有问题的规则是[expr.const]/4.12

表达式e核心常量表达式,除非 的计算e遵循抽象机的规则,将计算以下其中一项: [...]引用引用类型的变量或数据成员的id 表达式,除非引用有一个预先的初始化并且[...]

什么是前置初始化?在我回答这个问题之前,让我提供一个不同的程序并演练它的语义:

一个矛盾

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});
Run Code Online (Sandbox Code Playgroud)

只有一个函数foo<Int>,因此它必须有一种特定的返回类型。如果这个程序被允许,那么foo(Int{1})会返回一个X<1>并且foo(Int{2})会返回一个X<2>——也就是说,foo<Int>可以返回不同的类型?这是不可能发生的,所以这一定是格式错误的。

尽可能最小的盒子

当我们处于需要持续表达的情况时,可以将其视为打开一个新盒子。该框中的所有内容都必须满足持续评估的规则,就好像我们刚刚从该点开始一样。如果我们需要嵌套在该框中的新常量表达式,我们将打开一个新框。箱子一直往下走。

在原始复制品(带有One)和新复制品(带有Int)中,我们都有这样的声明:

constexpr int i = t;
Run Code Online (Sandbox Code Playgroud)

这将打开一个新框。初始化器t必须满足常量表达式的限制。是一个引用类型,但在此 box 内t没有预先初始化,因此这是格式错误的。

现在在已接受的情况下:

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

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

constexpr int i = foo(One{});
Run Code Online (Sandbox Code Playgroud)

我们只有一个框:全局i. 在该框中,我们仍然评估引用类型的id 表达式return t;,但在这种情况下,我们确实在框中进行了前面的初始化:我们在绑定到t的位置具有可见性One{}。所以这有效。这些规则不存在矛盾。事实上,这也很好:

constexpr int j = foo(Int{1});
constexpr int k = foo(Int{2});
static_assert(i+k == 3);
Run Code Online (Sandbox Code Playgroud)

因为我们每次仍然只有一个进入常量求值的入口,并且在该求值中,引用t具有前面的初始化,并且 的成员Int也可以在常量表达式中使用。

回到OP

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

Int{1}我们尝试按值传递的类似示例foo仍然会失败 - 这次不是针对引用规则,而是针对左值到右值转换规则。基本上,我们正在阅读一些我们不被允许阅读的内容——因为我们最终会遇到与能够构造具有多个返回类型的函数相同的矛盾。