条件类型别名定义

8

我有一个这样的类型:

template<typename T>
struct wrapper
{
    using foo = typename T::foo;
    using bar = typename T::bar;
    using baz = typename T::baz;
    // More of those...
};

如果在模板类型T中等效的类型存在,我希望定义foobarbaz和等效类型别名。使用std::conditional的解决方案可以在不存在时将其替换为其他内容,但当相应类型在模板类型中不存在时,我不知道如何确保它根本不存在。如果T没有定义其中一个类型别名,则上面的代码在实例化wrapper<T>时会导致错误。
我不能让wrapper继承自T,因为wrapper不应该做到T所能做到的一切。另外,使用部分特化会导致某种指数爆炸并且很快变得不可维护。我可以将foobar等模板类型别名制成默认模板参数,以注入std::enable_if,但是用户将不得不编写wrapper<T>::foo<>wrapper<T>::bar<>而不是wrapper<T>::foowrapper<T>::bar等,我不想要这个结果。
是否有一种简单而易于维护的方法只在T中存在相应类型别名时定义这样的类型别名?
2个回答

9

您可以定义check_foocheck_barcheck_baz特性,如果存在则仅拥有类型,然后在wrapper中从它们全部继承:

template <typename T, typename=void> 
struct check_foo{};

template <typename T> 
struct check_foo<T, void_t<typename T::foo>> { 
    using foo = typename T::foo; 
};

// ditto for bar, baz, etc.

template <typename T>
struct wrapper :
    check_foo<T>,
    check_bar<T>,
    check_baz<T>
{ };

每种类型都需要一个额外的结构体,但这比你提到的指数版本要好得多。如果您足够古怪,甚至可以将其制作成宏:

#define DEFINE_CHECKER(NAME) \
    template <typename T, typename=void> struct check_##NAME{}; \
    template <typename T> struct check_##NAME<T,void_t<typename T::NAME>> \
    { using NAME = typename T::NAME; };

DEFINE_CHECKER(foo)
DEFINE_CHECKER(bar)
DEFINE_CHECKER(baz)

恶心,我知道,但是如果你真的想要wrapper<T>::bar而不是wrapper<T>::bar<>,那么我认为你可能需要付出这个代价。 如果使用宏版本,添加新类型只需要一个新的DEFINE_CHECKER(newname)和将check_newname<T>添加到包装器继承列表中。 可能会更糟。
演示版

1
这就是我心中的解决方案,我已经准备好尝试了。虽然不是很漂亮,但仍比其他解决方案更易于维护,而且不会向用户公开无用的东西。谢谢 :) - Morwenn
当时候,我还希望有比这更好的解决方案。 - Justin
有没有可能避免使用继承? - Kaiyakha

5
请注意,@TartanLlama使用void_t的答案是有效的。然而,在C++17中,可能会有一些标准库助手,例如is_detected_v,它们将在幕后调用void_t
#include <experimental/type_traits>

// helpers to reduce boilerplate
template<class Tag>
struct empty_base {};

template<template<class> class Holder, template<class> class Op, class Arg>
using inject_or_t = std::conditional_t
<
    std::experimental::is_detected_v<Op, Arg>,
    Holder<Arg>,
    empty_base<Op<Arg>>
>;

// add detector + holder for every conditional nested type

template<class T>
using foo_t = typename T::foo;

template<class T>
struct foo_holder { using foo = foo_t<T>; };

template<class T>
using bar_t = typename T::bar;

template<class T>
struct bar_holder { using bar = bar_t<T>; };

template<class T>
using baz_t = typename T::baz;

template<class T>
struct baz_holder { using baz = baz_t<T>; };

// wrapper is now simply:

template<class T>
struct wrapper
:   inject_or_t<foo_holder, foo_t, T>
,   inject_or_t<bar_holder, bar_t, T>
,   inject_or_t<baz_holder, baz_t, T>
{};

struct Test
{
    using foo = int;
    using bar = int;
    using baz = int;
};

int main()
{
    static_assert(!std::experimental::is_detected_v<foo_t, wrapper<int>>);
    static_assert(!std::experimental::is_detected_v<bar_t, wrapper<int>>);
    static_assert(!std::experimental::is_detected_v<baz_t, wrapper<int>>);

    static_assert(std::experimental::is_detected_v<foo_t, wrapper<Test>>);
    static_assert(std::experimental::is_detected_v<bar_t, wrapper<Test>>);
    static_assert(std::experimental::is_detected_v<baz_t, wrapper<Test>>);
}

实时示例 请注意,这是非常罕见的例子之一,其中 libstdc++ 6.0 SVN trunk 目前可以做到 libc++ 3.9 SVN trunk 无法做到的事情。

这需要为每个要注入的类型添加一个检测器别名和一个持有者结构,并完全消除了宏包装器的需要。


真是一种非常优雅的解决方案!当然,人们可以通过实现适当的辅助工具或者像这样那样大胆地窃取它们来在C++14中完成这个任务。 - TartanLlama
哦,现在这是一个有趣的解决方案 :o - Morwenn
@TemplateRex,虽然这并不是@Morwenn所要求的内容,对吗?wrapper<Test>只是尝试三次从int继承,而不是继承foobarbaz成员别名。 - TartanLlama
@TartanLlama 谢谢,我确实太急了,已经更新。请注意,现在需要为每个要注入的类型添加一个持有者和一个检测器助手,但是条件注入的逻辑现在被整合到一个名为inject_or_t的单一实体中。 - TemplateRex
我有点困惑 empty_base 是如何工作的。在我的理解中,当 Op<Arg> 无效时,也就是说 is_detected_v<Op, Arg>false,我们会使用 empty_base<Op<Arg>>。但我不明白这实际上是如何编译通过的。 - Justin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接