Search code examples
c++templatesidiomspartial-specialization

Better pattern for partial specialization disambiguation precedence chain?


Consider the following series of partial specializations:

template <typename T, typename Enable=void>
struct foo {
  void operator()() const { cout << "unspecialized" << endl; }
};

template <typename T>
struct foo<T, enable_if_t<
  is_integral<T>::value
>>{
  void operator()() const { cout << "is_integral" << endl; }
};

template <typename T>
struct foo<T, enable_if_t<
  sizeof(T) == 4
    and not is_integral<T>::value
>>{
  void operator()() const { cout << "size 4" << endl; }
};

template <typename T>
struct foo<T, enable_if_t<
  is_fundamental<T>::value
    and not (sizeof(T) == 4)
    and not is_integral<T>::value
>>{
  void operator()() const { cout << "fundamental" << endl; }
};

// etc...   

Live Demo

I see this kind of thing all of the time (indeed, another StackOverflow answer elsewhere gives the same pattern for a similar problem). While this works, this code has some serious maintainability issues, and also precludes, e.g., user-level partial specializations at higher priority if the above code is in a library. What's a better pattern for expressing this idea? I feel like there has to be something (maybe involving inheritance and variadic template parameters?) that can express this idea more cleanly and maintainably. (Suppose also that each of the specializations is a full-on class rather than a simple functor, so overloaded functions don't work in a simplistic way).


Solution

  • Why am I answering my own question

    So I've been bugged by this ever since asking this question, and I was never completely satisfied with the original answer. After much fiddling and trial/error, I've come up with a pattern I'm much happier with that uses tag dispatch. Whether or not it's actually better, more readable, and more maintainable than the previous answer is for you to judge, but I like it better. Feel free to pick it apart, criticize it, and break it. :-)

    The Basic Version

    Without further ado, here's the code that solve the simplest version of the problem

    template <typename> struct always_true : true_type { };
    template <typename> struct always_false : false_type { };
    
    template <typename T, template <class...> class condition=always_false,
      typename flag=integral_constant<bool, condition<T>::value>
    >
    struct foo;
    
    ////////////////////////////////////////
    // "unspecialized" version
    
    // put always_true and false_type together here so that no one gets here accidentally
    template <typename T, typename true_or_false_type>
    struct foo<T, always_true, true_or_false_type> {
      void operator()() const { cout << "unspecialized" << endl; }
    };
    
    ////////////////////////////////////////
    // is_fundamental
    
    template <typename T>
    struct foo<T, is_fundamental, true_type> {
      void operator()() const { cout << "is_fundamental" << endl; }
    };
    template <typename T> struct foo<T, is_fundamental, false_type> : foo<T, always_true> { };
    
    ////////////////////////////////////////
    // is_integral
    
    template <typename T>
    struct foo<T, is_integral, true_type> {
      void operator()() const { cout << "is_integral" << endl; }
    };
    template <typename T>
    struct foo<T, is_integral, false_type> : foo<T, is_fundamental> { };
    
    ////////////////////////////////////////
    // sizeof(T) == 4
    
    template <typename T>
    using size_is_4 = integral_constant<bool, sizeof(T) == 4>;
    
    template <typename T>
    struct foo<T, size_is_4, true_type> {
      void operator()() const { cout << "size_is_4" << endl; }
    };
    template <typename T>
    struct foo<T, size_is_4, false_type> : foo<T, is_integral> { };
    
    ////////////////////////////////////////
    // Now put the most specialized condition in the base of this template
    
    template <typename T, typename true_or_false_type>
    struct foo<T, always_false, true_or_false_type> : foo<T, size_is_4> { };
    

    The chain of precedence, held in a helper struct in the previous answer, is encoded in inheritance.

    More bells and whistles

    Adding the ability to enable user partial specializations with higher precedence than the library ones takes a little more doing, but the principle is the same. The full version in this demo.