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
?
The type identifier you're creating an alias for. An alias doesn't introduce a new type and can't change the meaning of an existing type name. The simplest form of an alias is equivalent to the typedef mechanism from C++03: Both of these forms enable the creation of variables of type counter.
This seems like a similar concept but there is no direct equivalent of a type based alias in C#, only namespace aliases. If you want to avoid type name clashing in your code, namespace aliasing is the way to go. If you want to make a new type which "is a" instance of another type, inheritance is one option.
Alias template is a name that refers to a family of types. Alias declarations are declarations with the following syntax: the name that is introduced by this declaration, which becomes either a type name (1) or a template name (2) abstract declarator or any other valid type-id (which may introduce a new type, as noted in type-id ).
Aliases and typedefs (C++) You can use an alias declaration to declare a name to use as a synonym for a previously declared type. (This mechanism is also referred to informally as a type alias). You can also use this mechanism to create an alias template, which can be particularly useful for custom allocators.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With