I've been working on a way of producing compile-time information about classes that wrap other classes in C++. In a minimal example of the problem I am about to ask about, such a wrapper class:
typedef WrappedType
defining the type of the wrapped class; andIsWrapper
to indicate that it is a wrapper class. There is a struct template called WrapperTraits
that can then be used to determine the (non-wrapper) root type of a hierarchy of wrapped types. E.g. if the wrapper class is a class template called Wrapper<T>
, the root type of Wrapper<Wrapper<int>>
would be int
.
In the code snippet below, I have implemented a recursive struct template called GetRootType<T>
that defines a typedef RootType
that gives the root type of wrapper type T
. The given definition of WrapperTraits
just contains the root type as defined by GetRootType
, but in practice it would have some additional members. To test it, I have written an ordinary function f
that takes an int
, and an overloaded function template f
that takes an arbitrary wrapper class that has int
as its root type. I have used SFINAE to distinguish between them, by using std::enable_if
in the function template's return type to check whether or not f
's argument's root type is int
(if f
's argument is not a wrapper, attempting to determine its root type will fail). Before I ask my question, here is the code snippet:
#include <iostream>
#include <type_traits>
// Wrapper #######################################
template<class T>
struct Wrapper {typedef T WrappedType;};
template<class T, class Enable=void>
struct IsWrapper: std::false_type {};
template<class T>
struct IsWrapper<Wrapper<T> >: std::true_type {};
// WrapperTraits #######################################
template<
class T,
bool HasWrapper=
IsWrapper<T>::value && IsWrapper<typename T::WrappedType>::value>
struct GetRootType {
static_assert(IsWrapper<T>::value,"T is not a wrapper type");
typedef typename T::WrappedType RootType;
};
template<class T>
struct GetRootType<T,true> {
typedef typename GetRootType<typename T::WrappedType>::RootType RootType;
};
template<class T>
struct WrapperTraits {
typedef typename GetRootType<T>::RootType RootType;
};
// Test function #######################################
void f(int) {
std::cout<<"int"<<std::endl;
}
// #define ROOT_TYPE_ACCESSOR WrapperTraits // <-- Causes compilation error.
#define ROOT_TYPE_ACCESSOR GetRootType // <-- Compiles without error.
template<class T>
auto f(T) ->
typename std::enable_if<
std::is_same<int,typename ROOT_TYPE_ACCESSOR<T>::RootType>::value
>::type
{
typedef typename ROOT_TYPE_ACCESSOR<T>::RootType RootType;
std::cout<<"Wrapper<...<int>...>"<<std::endl;
f(RootType());
}
int main() {
f(Wrapper<int>());
return 0;
}
This compiles correctly (try it here) and produces the output:
Wrapper<...<int>...>
int
However, I have used GetRootType
to determine the root type in the call to std::enable_if
. If I instead use WrapperTraits
to determine the root type (you can do this by changing the definition of ROOT_TYPE_ACCESSOR
), GCC produces the following error:
test.cpp: In instantiation of ‘struct WrapperTraits<int>’:
test.cpp:49:6: required by substitution of ‘template<class T> typename std::enable_if<std::is_same<int, typename WrapperTraits<T>::RootType>::value>::type f(T) [with T = int]’
test.cpp:57:15: required from ‘typename std::enable_if<std::is_same<int, typename WrapperTraits<T>::RootType>::value>::type f(T) [with T = Wrapper<int>; typename std::enable_if<std::is_same<int, typename WrapperTraits<T>::RootType>::value>::type = void]’
test.cpp:62:19: required from here
test.cpp:21:39: error: ‘int’ is not a class, struct, or union type
bool HasWrapper=IsWrapper<T>::value && IsWrapper<typename T::WrappedType>::value>
My question is: what rules about argument deduction in the C++ standard explain why using WrapperTraits
causes a compilation error but using GetRootType
doesn't? Please note that what I want in asking this is to be able to understand why this compilation error occurs. I'm not so interested in what changes could be made to make it work, as I already know that changing the definition of WrapperTraits
to this fixes the error:
template<
class T,
class Enable=typename std::enable_if<IsWrapper<T>::value>::type>
struct WrapperTraits {
typedef typename GetRootType<T>::RootType RootType;
};
template<class T>
struct WrapperTraits<T,typename std::enable_if<!IsWrapper<T>::value>::type> {
};
However, if anyone can see a more elegant way of writing f
and WrapperTraits
, I would be very interested in seeing it!
The problem you are having is due to the fact that SFINAE only happens in the "immediate context" (a term that the standard uses, but does not define well) of a template instantiation. The instantiation of WrapperTraits<int>
is in the immediate context of the instantiation of auto f<int>() -> ...
, and it succeeds. Unfortunately, WrapperTraits<int>
has an ill-formed member RootType
. Instantiation of that member is not in the immediate context, so SFINAE does not apply.
To get this SFINAE to work as you intend, you need to arrange for WrapperTraits<int>
to not have a type named RootType
, instead of having such a member but with an ill-formed definition. This is why your corrected version works as intended, although you could save some repetition by reordering:
template<class T, class Enable=void>
struct WrapperTraits {};
template<class T>
struct WrapperTraits<T,typename std::enable_if<IsWrapper<T>::value>::type> {
typedef typename GetRootType<T>::RootType RootType;
};
I would probably code up the entire traits system as (DEMO):
// Plain-vanilla implementation of void_t
template<class...> struct voider { using type = void; };
template<class...Ts>
using void_t = typename voider<Ts...>::type;
// WrapperTraits #######################################
// Wrapper types specialize WrappedType to expose the type they wrap;
// a type T is a wrapper type iff the type WrappedType<T>::type exists.
template<class> struct WrappedType {};
// GetRootType unwraps any and all layers of wrappers.
template<class T, class = void>
struct GetRootType {
using type = T; // The root type of a non-WrappedType is that type itself.
};
// The root type of a WrappedType is the root type of the type that it wraps.
template<class T>
struct GetRootType<T, void_t<typename WrappedType<T>::type>> :
GetRootType<typename WrappedType<T>::type> {};
// non-WrappedTypes have no wrapper traits.
template<class T, class = void>
struct WrapperTraits {};
// WrappedTypes have two associated types:
// * WrappedType, the type that is wrapped
// * RootType, the fully-unwrapped type inside a stack of wrappers.
template<class T>
struct WrapperTraits<T, void_t<typename WrappedType<T>::type>> {
using WrappedType = typename ::WrappedType<T>::type;
using RootType = typename GetRootType<T>::type;
};
// Convenience aliases for accessing WrapperTraits
template<class T>
using WrapperWrappedType = typename WrapperTraits<T>::WrappedType;
template<class T>
using WrapperRootType = typename WrapperTraits<T>::RootType;
// Some wrappers #######################################
// Wrapper<T> is a WrappedType
template<class> struct Wrapper {};
template<class T>
struct WrappedType<Wrapper<T>> {
using type = T;
};
// A single-element array is a WrappedType
template<class T>
struct WrappedType<T[1]> {
using type = T;
};
// A single-element tuple is a WrappedType
template<class T>
struct WrappedType<std::tuple<T>> {
using type = T;
};
Although there's a lot of machinery there, and it may be more heavy-weight than you need. For example, the WrapperTraits
template could probably be eliminated in favor of simply using WrappedType
and GetRootType
directly. I can't imagine you'll often need to pass a WrapperTraits
instantiation around.