【问题标题】:Conditional type alias definition条件类型别名定义
【发布时间】:2016-04-10 12:02:09
【问题描述】:

我有一个这样的类型:

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&lt;T&gt; 时,上面的代码会导致错误。

我不能让wrapperT 继承,因为wrapper 不应该做所有T 可以做的事情。此外,使用部分专业化会导致某种指数爆炸,并很快变得无法维护。我可能会制作foobar...模板类型别名以在默认模板参数中注入std::enable_if,但用户必须编写wrapper&lt;T&gt;::foo&lt;&gt;wrapper&lt;T&gt;::bar&lt;&gt;而不是wrapper&lt;T&gt;::foo、@987654339 @,等等...我不想要那个。

只有在T中存在对应的类型别名时,是否有一种简单但可维护的方法来定义这样的类型别名?

【问题讨论】:

    标签: c++ templates c++14 template-meta-programming sfinae


    【解决方案1】:

    您可以定义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&lt;T&gt;::bar 而不是wrapper&lt;T&gt;::bar&lt;&gt;,你可能需要付出这个代价。如果您使用宏版本,添加新类型将意味着新的DEFINE_CHECKER(newname) 并将check_newname&lt;T&gt; 添加到包装器继承列表中。可能会更糟。

    Live Demo

    【讨论】:

    • 这就是我想到的解决方案,我准备尝试一下。它不是很漂亮,但仍然比其他解决方案更易于维护,不会向用户暴露无用的东西。谢谢:)
    • 当。我希望有比这更好的解决方案。
    【解决方案2】:

    请注意,@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>>);
    }
    

    Live Example 请注意,他是 libstdc++ 6.0 SVN 主干可以(目前!)做一些 libc++ 3.9 SVN 主干不能做的非常罕见的例子之一。

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

    【讨论】:

    • 真正优雅的解决方案!当然,可以在 C++14 中通过实现适当的帮助程序来做到这一点,或者公然窃取它们,例如 this
    • 哦,现在这是一个有趣的解决方案:o
    • @TemplateRex 虽然这不是@Morwenn 要求的,对吧? wrapper&lt;Test&gt; 只是尝试从 int 继承三次,而不是继承 foobarbaz 成员别名。
    • @TartanLlama 谢谢,我确实太快了,更新了。请注意,它现在需要为每个要注入的类型添加一个持有者和一个检测器助手,但条件注入的逻辑现在被分解为单个 inject_or_t
    • 我有点困惑empty_base 的工作原理。在我看来,当Op&lt;Arg&gt; 无效时,意味着is_detected_v&lt;Op, Arg&gt;false,我们然后使用empty_base&lt;Op&lt;Arg&gt;&gt;。我看不出它实际上是如何编译的。