如何用 boost.hana 解决 "read of non-constexpr variable 'a' is not allowed in a constant expression" 的问题

How to solve the issue of "read of non-constexpr variable 'a' is not allowed in a constant expression" with boost.hana

我正在使用带有 Boost.hana 的 c++17 来编写一些元编程程序。一个困扰我的问题是在像 static_assert 这样的 constexpr 上下文中可以使用什么样的表达式。这是一个例子:

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2.1
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        // static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression
    }
    {   //test2.2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

首先我写了一个 class X 字段 data 和一个访问器 getData() 。在main()test1部分,x1.datax1.getData() 的行为与我预期的一样。但是在 test2 部分,将参数更改为 boost::hana 的元组,static_assert(x2.data[0_c] == 1_c) 仍然表现良好但 static_assert(x2.getData()[0_c] == 1_c) 编译失败,错误为'在常量表达式 中不允许读取非 constexpr 变量 'x2''。奇怪的是,如果我将 x2.getData()[0_c] 拆分为 auto data = x2.getData();static_assert(data[0_c] == 1_c);,它又可以正常编译了。我希望他们的行为相同。那么谁能帮忙解释一下为什么在这个例子中static_assert不能使用x2.getData()[0_c]

重现:clang++8.0 -I/path/to/hana-1.5.0/include -std=c++17 Test.cpp

问题是因为您正在尝试检索 运行 时间值并在编译时对其进行测试。

你可以做的是在编译时通过 decltype 强制表达式,它会像一个魅力:)。

static_assert(decltype(x2.getData()[0_c]){} == 1_c);

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

   constexpr explicit X(T x) : data(x) {}

   constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

         static_assert(decltype(x2.getData()[0_c]){} == 1_c);

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

现在表达式在编译时求值,因此类型在编译时已知,并且由于它在计算时也可构造,因此可以在 static_assert

中使用它

所以首先,您在 getData() 方法中缺少 const 限定符,所以它应该是:

constexpr T getData() const

如果变量未标记为 constexpr,至少从标准的角度来看,没有变量被提升为 constexpr。

请注意,x1 没有必要,因为 X 类型专门用于 hana::integral_constant,因为 1_c 的结果是没有用户定义的复制构造函数的类型,内部不包含任何数据,所以 getData() 中的复制操作实际上是空操作,所以表达式: static_assert(x1.getData() == 1_c); 很好,因为没有完成实际的复制(也不需要访问 x1 的非 const this 指针)。

这与您的 hana::tuple 容器非常不同,它包含 x2.data 字段中数据的 hana::tuple 的实际复制构造。这需要实际访问您的 this 指针 - 在 x1 的情况下这不是必需的,它也不是 constexpr 变量。

这意味着您在 x1x2 中都表达了错误的意图,并且至少对于 x2,有必要将这些变量标记为 constexpr。还要注意,使用空元组,它是一般 hana::tuple 的基本空的(没有用户定义的复制构造函数)专业化,确实可以无缝工作(test3 部分):

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() const {
        return data;
    }
};

template<typename V>
constexpr auto make_X(V value)
{
    return value;
}

int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        constexpr auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
    {   //test3
        auto x3 = X(boost::hana::make_tuple());
        static_assert(x3.data == boost::hana::make_tuple());

        static_assert(x3.getData() == boost::hana::make_tuple());
    }
}

问题是 boost::hana::tuple 没有复制构造函数。

它有 a constructor 看起来 像复制构造函数:

template <typename ...dummy, typename = typename std::enable_if<
    detail::fast_and<BOOST_HANA_TT_IS_CONSTRUCTIBLE(Xn, Xn const&, dummy...)...>::value
>::type>
constexpr tuple(tuple const& other)
    : tuple(detail::from_index_sequence_t{},
            std::make_index_sequence<sizeof...(Xn)>{},
            other.storage_)
{ }

但由于这是一个模板,所以它是 not a copy constructor

因为boost::hana::tuple没有复制构造函数,一个是declared implicitly并且定义为默认(它没有被抑制因为boost::hana::tuple没有any 复制或移动构造函数或赋值运算符,因为,您猜对了,它们不能是模板)。

这里我们看到 implementation divergence,在以下程序的行为中得到证明:

struct A {
    struct B {} b;
    constexpr A() {};
    // constexpr A(A const& a) : b{a.b} {}    // #1
};
int main() {
    auto a = A{};
    constexpr int i = (A{a}, 0);
}

gcc 接受,而 Clang 和 MSVC 拒绝,但如果第 #1 行未注释则接受。也就是说,编译器不同意非(直接)空 class 的隐式定义的复制构造函数是否允许在常量评估上下文中使用。

根据 Clang 11 的 implicitly-defined copy constructor there is no way that #1 is any different to constexpr A(A const&) = default; so gcc is correct. Note also that if we give B a user-defined constexpr copy constructor Clang and MSVC again accept, so the issue appears to be that these compilers are unable to track the constexpr copy constructibility of recursively empty implicitly copyable classes. Filed bugs for MSVC and Clang (fixed 定义。

请注意 operator[] 的使用是转移注意力;问题是编译器是否允许在 static_assert.

等常量求值上下文中调用 getData()(复制构造 T

显然,理想的解决方案是 Boost.Hana 更正 boost::hana::tuple,使其具有实际的 copy/move 构造函数和 copy/move 赋值运算符。 (这将修复您的用例,因为代码将调用用户提供的复制构造函数,这在常量评估上下文中是允许的。)As a workaround,您可以考虑黑客攻击 getData() 来检测非-有状态 T:

constexpr T getData() {
    if (data == T{})
        return T{};
    else
        return data;
}