Conditional type alias definition
Asked Answered
N

2

8

I have a type like this:

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

I would like foo, bar, baz and equivalent type aliases to be defined if and only if the equivalent type exists in T. Solutions using std::conditional allow to replace it by something else when it doesn't exist, but I don't know how to make sure that it doesn't exist at all when the corresponding type doesn't exist in the template type. The code above causes an error when wrapper<T> is instantiated if T doesn't define one of the type aliases.

I can't make wrapper inherit from T because wrapper isn't supposed to do everything T can do. Also, using partial specialization would lead to some kind of exponential explosion and would quickly become unmaintainable. I could probably make foo, bar... template type aliases to inject an std::enable_if in a default template parameter but then users would have to write wrapper<T>::foo<>, wrapper<T>::bar<> instead of wrapper<T>::foo, wrapper<T>::bar, etc... and I don't want that.

Is there a simple yet maintainable way to define such a type alias only when the corresponding type alias exists in T?

Naturalize answered 6/1, 2016 at 11:49 Comment(0)
M
9

You could define check_foo, check_bar and check_baz traits which only have the type if it exists, then inherit from all of them in 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>
{ };

It's one extra struct per type, but certainly preferable to the exponential version you mentioned. You could even make it a macro if you were suitably perverse:

#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)

Horrible, I know, but I think you might need to pay that price if you really want wrapper<T>::bar rather than wrapper<T>::bar<>. If you use the macro version, adding a new type would mean just a new DEFINE_CHECKER(newname) and adding check_newname<T> to the wrapper inheritance list. Could be worse.

Live Demo

Markos answered 6/1, 2016 at 12:6 Comment(3)
That's the solution I had in mind and I was ready to try it. It's not really pretty, but still more maintainable than the other solutions without exposing useless things to users. Thanks :)Naturalize
Dang. I was hoping that there'd be a nicer solution than this.Semipalatinsk
Is it possible to avoid using inheritance?Escutcheon
S
5

Note that the answer using void_t by @TartanLlama is fine as it is. However, in C++17 there most likely will be a couple of Standard Library helpers such as is_detected_v that will do the calls to void_t under the hood.

#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 Note that his is one of the very rare examples where libstdc++ 6.0 SVN trunk can (currently!) do something that libc++ 3.9 SVN trunk cannot.

This requires adding a detector alias and a holder struct for each to be injected type, and completely eliminates the need for a macro wrapper.

Spieler answered 19/1, 2016 at 19:15 Comment(5)
Really elegant solution! Of course one could do this in C++14 by implementing the appropriate helpers, or blatantly stealing them, like this.Markos
Oh, now that's an interesting solution :oNaturalize
@Spieler Although this isn't what @Naturalize asked for, right? wrapper<Test> just tries to inherit from int three times rather than inheriting foo, bar and baz member aliases.Markos
@Markos thanks, I was too quick indeed, updated. Note that it now requires to add both a holder and a detector helper for each to be injected type, but the logic of conditionally injecting is now factored into a single inject_or_tSpieler
I'm a little confused how empty_base works. It seems to me as if, when Op<Arg> is invalid, meaning that is_detected_v<Op, Arg> is false, that we then use empty_base<Op<Arg>>. I can't see how that actually compiles.Semipalatinsk

© 2022 - 2024 — McMap. All rights reserved.