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

问题描述 投票:5回答:1

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

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

c++ c++17 constexpr rvalue-reference pass-by-rvalue-reference
1个回答
0
投票

这里的复制时间较短,请考虑使用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情况还可以,其他情况则格式不正确。为什么?有问题的规则是[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>可以返回不同的类型吗?这不可能发生,因此必须格式错误。

最小的盒子

[当我们处于需要常量表达式的情况时,请将其视为打开一个新框。该框中的所有内容都必须满足不断评估的规则,就好像我们只是从这一点开始一样。如果我们需要在该框中嵌套一个新的常量表达式,请打开一个新框。箱子一直向下。

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

constexpr int i = t;

这将打开一个新框。初始化程序t必须满足常量表达式的限制。 t是引用类型,但是没有先前的初始化在此框内,因此格式不正确。

现在已接受的情况:

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

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

constexpr int i = foo(One{});

我们只有一个框:全局i的初始化。在该框内,我们仍然在该return t;内评估引用类型的id-expression,但在这种情况下,我们在框内预先进行了[[do初始化:在绑定的位置我们可以看到tOne{}。所以这可行。这些规则不能构成任何矛盾。确实,这也很好:

constexpr int j = foo(Int{1}); constexpr int k = foo(Int{2}); static_assert(i+k == 3);
因为每次每次都只有一个入口可以进行常量评估,并且在该评估中,引用t具有预先的初始化,并且Int的成员也可以在常量表达式中使用。 

返回OP

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

[一个类似的例子,我们试图通过值将Int{1}传递给foo仍然会失败-这次不是针对引用规则,而是针对左值到右值转换规则。基本上,我们正在读一些我们不允许阅读的东西,因为我们最终陷入了能够构造具有多个返回类型的函数的矛盾。

© www.soinside.com 2019 - 2024. All rights reserved.