Implementing is_constexpr_copiable

Implementing is_constexpr_copiable

本文关键字:copiable constexpr Implementing is      更新时间:2023-10-16

我试图实现一个类似于std::is_constructible的值模板,但只有在 constexpr 环境中该类型可复制时才为 true(即其复制构造函数是 constexpr 限定的)。我得出了以下代码:

#include <type_traits>
struct Foo {
constexpr Foo() = default;
constexpr Foo(const Foo&) = default;
};
struct Bar {
constexpr Bar() = default;
Bar(const Bar&);
};
namespace detail {
template <int> using Sink = std::true_type;
template<typename T> constexpr auto constexpr_copiable(int) -> Sink<(T(T()),0)>;
template<typename T> constexpr auto constexpr_copiable(...) -> std::false_type;
}
template<typename T> struct is_constexpr_copiable : decltype(detail::constexpr_copiable<T>(0)){ };
static_assert( is_constexpr_copiable<Foo>::value, "");
static_assert(!is_constexpr_copiable<Bar>::value, "");

现在我问自己这是否符合标准,因为编译器似乎不同意输出。 https://godbolt.org/g/Aaqoah


编辑(c++17 功能):

在实现略有不同的is_constexpr_constructible_from时,使用 c++17 的新自动非类型模板类型,我再次发现编译器之间的差异,当使用SFINAE在 constexpr 表达式中取消引用 nullptr 时。

#include <type_traits>
struct Foo {
constexpr Foo() = default;
constexpr Foo(const Foo&) = default;
constexpr Foo(const Foo*f):Foo(*f) {};
};
struct Bar {
constexpr Bar() = default;
Bar(const Bar&);
};
namespace detail {
template <int> struct Sink { using type = std::true_type; };
template<typename T, auto... t> constexpr auto constexpr_constructible_from(int) -> typename Sink<(T(t...),0)>::type;
template<typename T, auto... t> constexpr auto constexpr_constructible_from(...) -> std::false_type;
}
template<typename T, auto... t> struct is_constexpr_constructible_from : decltype(detail::constexpr_constructible_from<T, t...>(0)){ };
constexpr Foo foo;
constexpr Bar bar;
static_assert( is_constexpr_constructible_from<Foo, &foo>::value, "");
static_assert(!is_constexpr_constructible_from<Foo, nullptr>::value, "");
static_assert(!is_constexpr_constructible_from<Bar, &bar>::value, "");
int main() {}

https://godbolt.org/g/830SCU


编辑:(四月2018)

现在两个编译器都应该支持 C++17,我发现以下代码工作得更好(不需要"T"上的默认构造函数),而只需要在 clang 上。一切都保持不变,但将命名空间"detail"替换为以下内容: 命名空间详细信息 { 模板结构接收器 {}; 模板 constexpr 自动接收器 -> 标准::true_type; 模板 constexpr 自动 try_copy() -> 接收器; template constexpr auto constexpr_copiable(int) -> decltype(sink(std::d eclval,0)>>())); 模板 constexpr auto constexpr_copiable(...) -> 标准::false_type; } https://godbolt.org/g/3fB8jt 这非常深入到标准中关于未求值上下文的部分内容,并且两个编译器都拒绝允许将"const T*"替换为"const T&"并使用"std::d eclval()"而不是"nullptr"转换。如果我确认 clang 的行为是公认的标准化行为,我会把这个版本提升到一个答案,因为它只需要确切地要求所问的内容。

Clang接受一些未定义的行为,取消引用nullptr,在计算未计算的decltype操作数时。

最艰巨的挑战是,给出一个函数来评估任意 T 是否存在来自const T&constexpr构造函数,在这里给出似乎几乎不可能在 C++17 中。幸运的是,我们可以走很长的路。其理由如下:

了解问题空间

以下限制对于确定是否可以在constexpr内容中计算某些表达式非常重要:

  • 要计算T的复制构造函数,需要一个类型const T&的值。这样的值必须引用具有活动生存期的对象,即constexpr上下文中,它必须引用在逻辑封闭表达式中创建的某个值。

  • 为了创建此引用作为任意T的临时提升的结果,我们需要知道并调用一个构造函数,其参数可能涉及几乎任意的其他表达式,我们需要评估其constexpr性。据我所知,这似乎需要解决确定一般表达式constexpr性的一般问题。¹

  • ¹ 实际上,如果任何带有参数的构造函数(包括复制构造函数)被定义为constexpr,则必须有一些有效的方法来构造T,无论是作为聚合初始化还是通过构造函数。否则,程序的格式将不正确,这可以通过constexpr说明符 §10.1.5.5 的要求来确定:

    对于

    既不是默认值也不是模板的 constexpr 函数或 constexpr 构造函数,如果不存在参数值,使得函数或构造函数的调用可能是核心常量表达式的计算子表达式,或者对于构造函数,是某个对象的常量初始值设定项 ([basic.start.static]),则程序格式不正确,无需诊断。

    这可能会给我们一个小小的漏洞。

  • 所以表达式最好是未计算的操作数 §8.2.3.1

    在某些上下文中,会出现未计算的操作数([expr.prim.req]、[expr.typeid]、[expr.sizeof]、[expr.unary.noexcept]、[dcl.type.simple]、[temp])。 不计算未计算的操作数

  • 未计算的操作数是通用表达式,但不能要求它们在编译时可计算,因为它们根本不被计算。请注意,模板的参数不是未计算表达式本身的一部分,而是命名模板类型的非限定 id 的一部分。这是我最初困惑的一部分,并试图找到可能的实现。

  • 非类型模板参数必须是常量表达式 §8.6,但此属性是通过求值定义的(我们已经确定这通常是不可能的)。 §8.6.2

    表达式 e 是一个核心常量表达式,除非 e 的计算遵循抽象机器的规则,[由我自己突出显示]计算以下表达式之一:

  • 对未计算的上下文使用noexpect具有相同的问题:最好的鉴别器,推断的 noexceptness,仅适用于可以作为核心常量表达式计算的函数调用,因此此 stackoverflow 答案中的技巧 mentionend 不起作用。

  • sizeof有与decltype相同的问题。事情可能会随着concepts而改变。

  • 可悲的是,新引入的if constexpr不是一个表达式,而是一个带有表达式参数的语句。因此,它无助于强制实施表达式的constexpr可计算性。当语句被评估时,它的表达式也是如此,我们又回到了创建可评估const T&的问题。丢弃的语句对流程完全没有影响。

简单的可能性第一

由于困难的部分是创建const T&,我们只是为少数常见但易于确定的可能性这样做,其余的则由极其特殊的情况调用者进行专业化。

namespace detail {
template <int> using Sink = std::true_type;
template<typename T,bool SFINAE=true> struct ConstexprDefault;
template<typename T>
struct ConstexprDefault<T, Sink<(T{}, 0)>::value> { inline static constexpr T instance = {}; };
template<typename T> constexpr auto constexpr_copiable(int) -> Sink<(T{ConstexprDefault<T>::instance}, 0)>;
template<typename T> constexpr auto constexpr_copiable(...) -> std::false_type;
}
template<typename T>
using is_constexpr_copyable_t = decltype(detail::constexpr_copiable<T>(0));

对于声明 constexpr 复制构造函数的任何类类型,必须能够进行专用details::ConstexprDefault,如上所示。请注意,该参数不适用于没有构造函数的其他复合类型 §6.7.2。数组、联合、引用和枚举需要特殊考虑。

可以在 godbolt 上找到具有多种类型的"测试套件"。非常感谢reddit用户/u/dodheim,我从他那里复制了它。缺失化合物类型的其他专业化留给读者作为练习。

² 或What does this leave us with?

模板参数中的评估失败不是致命的。SFINAE可以涵盖各种可能的构造函数。本节的其余部分纯粹是理论上的,对编译器来说并不好,否则可能会很愚蠢。

有可能使用类似于magic_get的方法枚举一个类型的许多构造函数。本质上,使用一种类型Ubiq假装可转换为所有其他类型的类型来伪造你的方式decltype(T{ ubiq<I>()... })其中I是一个参数包,其中包含当前检查的初始值设定项计数,template<size_t i> Ubiq ubiq()只是构建正确数量的实例。当然,在这种情况下,需要明确禁止T的演员阵容。

为什么只有很多?和以前一样,一些 constexpr 构造函数将存在,但它可能有访问限制。这会在我们的模板机器中给出误报并导致无限搜索,并且在某个时候编译器会死亡:/。或者构造函数可能被重载隐藏,由于Ubiq太笼统而无法解决。同样的效果,悲伤的编译器和愤怒的PETC(人们为编译器™的道德待遇,而不是一个真正的组织)。实际上,访问限制可以通过以下事实来解决:这些限制不适用于模板参数,这可能允许我们提取指向成员的指针和 [...]。

我就到此为止。据我所知,这很乏味,而且大多是不必要的。当然,对于大多数用例来说,涵盖可能的构造函数调用 5 个参数就足够了。任意T非常非常困难,我们不妨等待 C++20,因为模板元编程将再次发生巨大变化。