在标准库中键入聚合可初始化性的特征?

Type trait for aggregate initializability in the standard library?

提问人:Bernard 提问时间:12/19/2017 更新时间:12/20/2017 访问量:1216

问:

C++标准库有 std::is_constructible<Class、T...>检查是否可以从给定类型构造类作为参数。

例如,如果我有一个具有构造函数的类,那么将是 .MyClassMyClass(int, char)std::is_constructible<MyClass, int, char>::valuetrue

是否有类似的标准库类型特征来检查聚合初始化是否有效,即 格式正确,并返回 ?MyClass{int, char}MyClass

我的用例:

我想编写一个函数模板,该模板使用聚合初始化将 a 转换为(通常是 POD)类,其签名如下:std::tuple

template <typename Class, typename... T>
inline Class to_struct(std::tuple<T...>&& tp);

为了防止用户使用这个无效的函数,我可以在这个函数内部写一个来检查给定的参数是否具有可转换为 的成员的类型。似乎像这样的类型特征会派上用场。Classstatic_asserttpClassis_aggregate_initializable<Class, T...>

我可以自己实现这个特征,但仅供参考,标准库中是否有我忽略的特征,或者即将成为标准库的一部分?

C++ C++17 类型特征 聚合初始化

评论

1赞 StoryTeller - Unslander Monica 12/19/2017
老实说,我不明白为什么它会那么有用。 几乎存在,因此对于像这样的类型,泛型代码可以避免意外地进行列表初始化。为什么你不能做并且需要关心它是否是一个聚合?std::is_constructiblestd::vector<int>MyClass { Args... }
0赞 Bernard 12/19/2017
@StoryTeller 在更通用的代码中,只要 和 被定义,就应该工作,所以它应该看起来像 ,具有不依赖于 的适当内部实现函数。例如,在那之后,我可能希望在给定对象周围有另一个重载,而不解包(仅当它无法解包时)。在这种情况下,我需要使用类型特征来限制第一个重载(可能使用 SFINAE 的东西)。to_struct()std::tuple_sizestd::tuple_elementtemplate <typename Class, typename Tuple> inline Class to_struct(Tuple&& tp);std::tupleto_struct()Class
0赞 Nicol Bolas 12/19/2017
@Bernard:“我可以自己实现这个特质” 不,你不能。没有特质,你就无法分辨工作和工作的区别。is_aggregateaggregate{1, 2}non_aggregate{1, 2}
4赞 Barry 12/19/2017
@NicolBolas is_aggregate
0赞 Yakk - Adam Nevraumont 12/19/2017
真的,你想知道是否合法吗?你关心它是否使用普通的构造函数或聚合初始化?T foo{a,b,c}

答:

6赞 Bernard 12/20/2017 #1

从评论中的讨论和浏览 C++ 参考来看,似乎有一个标准的库类型特征既没有聚合可初始化性也没有列表可初始化性,至少在 C++17 之前是这样。

评论中强调,一般列表的可初始化性()和聚合的可初始化性是有区别Class{arg1, arg2, ...}

列表可初始化性(特别是直接列表可初始化性)更容易编写类型特征,因为此特征完全取决于特定语法的有效性。对于我测试结构是否可以从元组元素构造的用例,直接列表可初始化性似乎更合适。

实现此特征的可能方法(使用适当的 SFINAE)如下:

namespace detail {
    template <typename Struct, typename = void, typename... T>
    struct is_direct_list_initializable_impl : std::false_type {};

    template <typename Struct, typename... T>
    struct is_direct_list_initializable_impl<Struct, std::void_t<decltype(Struct{ std::declval<T>()... })>, T...> : std::true_type {};
}

template <typename Struct, typename... T>
using is_direct_list_initializable = detail::is_direct_list_initializable_impl<Struct, void, T...>;

template<typename Struct, typename... T>
constexpr bool is_direct_list_initializable_v = is_direct_list_initializable<Struct, T...>::value;

然后我们可以通过做 来测试直接列表的可初始化性。is_direct_list_initializable_v<Class, T...>

这也适用于移动语义和完美转发,因为尊重完美的转发规则。std::declval

聚合可初始化性不那么简单,但有一个解决方案可以涵盖大多数情况。聚合初始化要求被初始化的类型是聚合(请参阅有关聚合初始化的C++参考的说明),并且我们有一个 C++17 特征 std::is_aggregate 来检查类型是否为聚合。

但是,这并不意味着仅仅因为类型是聚合,通常的直接列表初始化就会无效。仍然允许与构造函数匹配的普通列表初始化。例如,以下编译:

struct point {
    int x,y;
};

int main() {
    point e1{8}; // aggregate initialization :)
    point e2{e1}; // this is not aggregate initialization!
}

为了禁止这种列表初始化,我们可以利用聚合不能有自定义(即用户提供的)构造函数这一事实,因此非聚合初始化必须只有一个参数,并且将满足 .Class{arg}std::is_same_v<Class, std::decay_t<decltype(arg)>>

幸运的是,我们不能有与其封闭类类型相同的成员变量,因此以下内容无效:

struct point {
    point x;
};

需要注意的是:允许对同一对象的引用类型,因为成员引用可能是不完整的类型(GCC、Clang 和 MSVC 都接受这一点,没有任何警告):

struct point {
    point& x;
};

虽然不寻常,但此代码按标准有效。我没有解决方案来检测这种情况并确定是否可以使用类型的对象进行聚合初始化。pointpoint&

忽略上面的警告(很少需要使用这样的类型),我们可以设计一个有效的解决方案:

template <typename Struct, typename... T>
using is_aggregate_initializable = std::conjunction<std::is_aggregate<Struct>, is_direct_list_initializable<Struct, T...>, std::negation<std::conjunction<std::bool_constant<sizeof...(T) == 1>, std::is_same<std::decay_t<std::tuple_element_t<0, std::tuple<T...>>>, Struct>>>>;

template<typename Struct, typename... T>
constexpr bool is_aggregate_initializable_v = is_aggregate_initializable<Struct, T...>::value;

它看起来不是很好,但确实按预期运行。

评论

0赞 ildjarn 5/18/2022
在独立地跌跌撞撞地进入同一个兔子洞时,我遇到了一个异常规范的绊脚石。在我投入太多之前,我希望有一个快速的“是”或“否”:即使在理论上,特质是否可能?is_nothrow_aggregate_initializable
0赞 Bernard 5/18/2022
@ildjarn 您应该能够用类型特征替换,并且它将在相同的警告下工作。由于有一个 noexcept 运算符来测试任意表达式是否为 noexcept,因此您应该能够使用它来构建自己的 SFINAE。is_direct_list_initializableis_nothrow_direct_list_initializablestd::enable_ifis_nothrow_direct_list_initializable
0赞 ildjarn 5/19/2022
不幸的是,noexcept 运算符并不是包罗万象的;我希望是这样。考虑一下:godbolt.org/z/175vzz43E 我希望我在这里遗漏了一些明显的东西:-]