Search code examples
c++templatessfinaetype-traitsenable-if

SFINAE failure with typedef in class template referring to typedef in another class template


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:

  • contains a typedef WrappedType defining the type of the wrapped class; and
  • overloads a struct template called IsWrapper 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!


Solution

  • 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.