5

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

Community
  • 1
  • 1
Daisy Sophia Hollman
  • 6,046
  • 6
  • 24
  • 35

2 Answers2

4

The overgrowth of condition count can be solved by helper structs:

#include <iostream>
#include <type_traits>

using namespace std;

template <bool ThisCondition, class ParentCondition = void, class = void>
struct condition_resolver {
   static constexpr bool is_condition_resolver = true;
   static constexpr bool parent_condition_v = !ThisCondition;
   static constexpr bool value = ThisCondition;
};

template <bool ThisCondition, class ParentCondition>
struct condition_resolver<ThisCondition, ParentCondition, enable_if_t<ParentCondition::is_condition_resolver> > {
   static constexpr bool is_condition_resolver = true;
   static constexpr bool parent_condition_v = !ThisCondition && ParentCondition::parent_condition_v;
   static constexpr bool value = ThisCondition && ParentCondition::parent_condition_v;
};

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

template <typename T>
struct is_integral_foo: condition_resolver<is_integral<T>::value> { };

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

template <typename T>
struct has_size_four_foo: condition_resolver<sizeof(T) == 4, is_integral_foo<T>> { };

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

template <typename T>
struct is_fundamental_foo: condition_resolver<is_fundamental<T>::value, has_size_four_foo<T>> { };

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

typedef char four_sized[4];

int main() {
   foo<int>()();
   foo<four_sized>()();
   foo<nullptr_t>()();
}

Output:

is_integral
size 4
fundamental

PS. Have in mind that void which is also fundamental will cause compiler to produce a warning that sizeof(void) is considered...

Edit:

If you really need to use specialization for solving the overgrowth of condition problem this might interest you:

#include <iostream>
#include <type_traits>

using namespace std;

template <class Tag, int Level, class... Args>
struct concrete_condition_resolver;


template <class Tag, int Level, class... Args>
struct condition_resolver;

template <class ConditionResolver>
struct condition_resolver_parent {
   template<class CR = ConditionResolver>
   constexpr enable_if_t<CR::level != 0, bool> operator()(bool parent) {
      return (!parent && static_cast<const ConditionResolver*>(this)->condition && typename ConditionResolver::LevelUp()(true)) ||
             (parent && !static_cast<const ConditionResolver*>(this)->condition && typename ConditionResolver::LevelUp()(true));
   }

   template<class CR = ConditionResolver>
   constexpr enable_if_t<CR::level == 0, bool> operator()(bool parent) {
      return (!parent && static_cast<const ConditionResolver*>(this)->condition) ||
             (parent && !static_cast<const ConditionResolver*>(this)->condition);
   }
};

template <class Tag, int Level, class... Args>
struct condition_resolver: concrete_condition_resolver<Tag, Level, Args...>, condition_resolver_parent<condition_resolver<Tag, Level, Args...>> {
   using LevelUp = condition_resolver<Tag, Level - 1, Args...>;
   using tag = Tag;
   static constexpr int level = Level;
   constexpr condition_resolver() {}
};


struct foo_tag { };

template <class First, class... Args>
struct concrete_condition_resolver<foo_tag, 0, First, Args...> {
   static constexpr bool condition = is_integral<First>::value;
};

template <class First, class... Args>
struct concrete_condition_resolver<foo_tag, 1, First, Args...> {
   static constexpr bool condition = sizeof(First) == 4;
};

template <class First, class... Args>
struct concrete_condition_resolver<foo_tag, 2, First, Args...> {
   static constexpr bool condition = is_fundamental<First>::value;
};

template <typename T, typename = void>
struct foo;

template <typename T>
struct foo<T, enable_if_t<condition_resolver<foo_tag, 0, T>()(false)>>{
  void operator()() const { cout << "is_integral" << endl; }
};

template <typename T>
struct foo<T, enable_if_t<condition_resolver<foo_tag, 1, T>()(false)>>{
  void operator()() const { cout << "size 4" << endl; }
};

template <typename T>
struct foo<T, enable_if_t<condition_resolver<foo_tag, 2, T>()(false)>>{
  void operator()() const { cout << "is_fundamental" << endl; }
};


typedef char four_sized[4];

int main() {
   foo<int>()();
   foo<four_sized>()();
   foo<nullptr_t>()();
}

This approach is applicable even for overload functions using enable_if, while partial specialization deals only with structs...

W.F.
  • 13,888
  • 2
  • 34
  • 81
  • Looks like a pretty neat approach to manage the conditions. I was going to propose something similar, but mine would be a bit clumsier than yours. +1 – Zuu Apr 15 '16 at 22:02
  • How is this better than something much simpler like `template using condition1_t = std::integral_constant::value>;` (and then defining `condition_2` similarly but with a reference to `condition_1` instead of `condition_0`) – Daisy Sophia Hollman Apr 15 '16 at 22:16
  • @DavidHollman Well actually you can't do it using only a single using because it contains not negated partial value for the last condition... e.g. `condition1_t` contains `sizeof(T)==4` and if should be proper for use in `condition2_t` it should be `sizeof(T)!=4`... – W.F. Apr 15 '16 at 22:42
  • @DavidHollman of course you could split it for two usings but would it be really simpler than that approach? – W.F. Apr 15 '16 at 22:44
  • @WojciechFrohmberg of course. boolean logic mind fart on my part – Daisy Sophia Hollman Apr 16 '16 at 00:06
  • @DavidHollman one more pro for this solution is c++11 compatibility – W.F. Apr 16 '16 at 00:08
3

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.

Daisy Sophia Hollman
  • 6,046
  • 6
  • 24
  • 35
  • Your solution is interesting, you might also want to have a look on an approach in my edit. It uses specialization and kind of tag dispatching... – W.F. Apr 29 '16 at 14:38
  • 1
    @WojciechFrohmberg Thanks. I think it's useful to have both approaches here regardless of which one is "better" (for some definition of better). Thanks for your time and effort in contributing to this answer – Daisy Sophia Hollman May 02 '16 at 17:20