Today's Question:  What does your personal desk look like?        GIVE A SHOUT

Set Theory in C++11

  Malte        2012-03-11 13:15:55       2,732        0    


Have you ever felt the need to perform set theoretic operations on types? Not really? Me neither, but I thought it’s a fun thing to try out. So, if you ever feel the need of using type sets, C++11 makes it quite easy to do so. Especially variadic templates allow for a much more condensed syntax compared to type list constructs formerly used. (Disclaimer: This is rather a proof of concept, but maybe somebody comes up with a useful scenario.)

Let’s start by defining an empty type_set type:

template<typename ... Types>
struct type_set;
 
typedef type_set<> empty_set;

As you can see, the struct is just a placeholder for the template arguments. The empty set is defined using a typedef and an empty template argument list. The first type set operator we will define is the element_of operator.

template<typename A, typename B>
struct is_element;
 
template<typename A>
struct is_element<A, empty_set>
{
  static const  bool value = false;
};
 
template<typename A, typename HeadB, typename ... TypesB>
struct is_element<A, type_set<HeadB, TypesB...>>
{
  static const bool value = 
    std::is_same<A,HeadB>::value ||
    is_element<A, type_set<TypesB...>>::value;
};
 
template<typename A, typename B>
struct is_element
{
  static const bool value = false;
  static_assert(always_false<A,B>::value, 
                "Second template argument is not a type set");  
};

where always_false is defined as follows

template<typename ... T>
struct always_false
{
  static const bool value = false;
};

The implementation of element_of is just a recursive look up function operating on the type level. The always_false construct is needed to trigger the static assert and print out a nice error warning only in the case that the template is instantiated with a non type set type in the second parameter.

Note, at the moment there is no mechanism that stops us from putting the same type twice into the type set (i.e. type_set<int,int,bool> is a valid type name). I will come to that in a minute. First lets define subset and equality operators:

template<typename A, typename B>
struct is_subset;
 
template<typename ... TypesB>
struct is_subset<empty_set, type_set<TypesB...>>
{
  static const bool value = true;
};
 
template<typename HeadA,
         typename ... TypesA,
         typename ... TypesB>
struct is_subset<type_set<HeadA, TypesA...>, type_set<TypesB...>>
{
  static const bool value = 
    is_element<HeadA, type_set<TypesB...>>::value &&
    is_subset<type_set<TypesA...>, type_set<TypesB...>>::value;
};
 
template<typename A, typename B>
struct is_subset
{
  static const bool value = false;
 
  static_assert(always_false<A,B>::value, 
                "Template arguments are no type sets");  
};
 
template<typename A, typename B>
struct is_set_equal
{
  static const bool value = is_subset<A,B>::value 
                            && is_subset<B,A>::value;
};

These implementations are possibly not the most efficient (we’re speaking about compile time efficiency here), but they stick quite closely to the mathematical definitions of the operators:

A⊆B⟺∀a∈A:a∈B

and

A=B⟺A⊆B∧B⊆A.

It should be clear now how a naive implementation of a proper subset (A⊂B) operator could be defined. The next thing we will need is a cons like operator. However, we will add a predicate on which the cons depends, so an element is only added to the set if the predicate evaluates to true:

template<bool condition,
         typename NewA,
         typename A>
struct cons_set_if;
 
template<typename NewA,
         typename ... TypesA>
struct cons_set_if<true, NewA, type_set<TypesA...>>
{
  typedef type_set<NewA, TypesA...> type;
};
 
template<typename NewA,
         typename ... TypesA>
struct cons_set_if<false, NewA, type_set<TypesA...>>
{
  typedef type_set<TypesA...> type;
};
 
template<bool condition,
         typename NewA,
         typename A>
struct cons_set_if
{
  typedef void type;
  static_assert(always_false<NewA,A>::value, 
                "Template arguments are no type sets");  
};

Using the conditional cons operator we can reduce a set by eliminating duplicates. The reduce operation uses the cons_set_if operator to recursively add elements to a new type set if the element is not yet contained in this set:

template<typename A>
struct reduce_set;
 
template<>
struct reduce_set<empty_set>
{
  typedef empty_set type;
};
 
template<typename HeadA, typename ... TypesA>
struct reduce_set<type_set<HeadA, TypesA...>>
{
  typedef typename reduce_set<type_set<TypesA...>>::type tail_reduced;
  typedef typename cons_set_if<!is_element<HeadA, tail_reduced>::value, 
                               HeadA, 
                               tail_reduced>::type
    type;
};
 
template<typename A>
struct reduce_set
{
  typedef void type;
  static_assert(always_false<A>::value, 
                "Template argument is not a type set");  
};

The implementation of a union ∪ operator is now quite easy, as it is just the reduction of the concatenation of two type sets:

template<typename A, typename B>
struct set_union;
 
template<typename ... TypesA, typename ... TypesB>
struct set_union<type_set<TypesA...>, type_set<TypesB...>>
{
  typedef typename reduce_set<type_set<TypesA..., TypesB...>>::type type;
};
 
template<typename A, typename B>
struct set_union
{
  typedef void type;
  static_assert(always_false<A,B>::value, 
                "Template arguments are no type sets");  
};

The intersection ∩ operator needs a little bit more work. We will use the cons_set_if operator again. By recursively adding only elements that are in the other set as well, we end up with the intersection of both sets:

template<typename A, typename B>
struct set_intersection;
 
template<typename ... TypesB>
struct set_intersection<empty_set,type_set<TypesB...>>
{
  typedef empty_set type;
};
 
template<typename HeadA, typename ... TypesA, typename ... TypesB>
struct set_intersection<type_set<HeadA, TypesA...>, type_set<TypesB...>>
{
  typedef typename set_intersection<type_set<TypesA...>, 
                                    type_set<TypesB...>>::type tail_intersection;
  typedef typename reduce_set<
    typename cons_set_if<is_element<HeadA, 
                           type_set<TypesB...>>::value, 
                HeadA, 
                tail_intersection>::type
    >::type type;
};
 
template<typename A, typename B>
struct set_intersection
{
  typedef void type;
  static_assert(always_false<A,B>::value, 
                "Template arguments are no type sets");  
};

The last part I will show here, is a cross product operator. The cross product operator maps two type sets to a type set of tuple types. This is done by recursion again, but this time by splitting a square into a head square, vertical rectangle, horizontal rectangle and a tail square:

template<typename A, typename B>
struct set_cross;
 
template<typename ... Types>
struct set_cross<empty_set, type_set<Types...>>
{
  typedef empty_set type;
};
 
template<typename ... Types>
struct set_cross<type_set<Types...>, empty_set>
{
  typedef empty_set type;
};
 
template<typename HeadA,
         typename HeadB>
struct set_cross<type_set<HeadA>, type_set<HeadB>>
{
  typedef type_set<std::tuple<HeadA, HeadB>> type;
};
 
template<typename HeadA, typename ... TypesA, 
         typename HeadB, typename ... TypesB>
struct set_cross<type_set<HeadA, TypesA...>, type_set<HeadB, TypesB...>>
{
private:
  typedef typename set_cross<type_set<HeadA>, type_set<HeadB>>::type head_square;
  typedef typename set_cross<type_set<HeadA>, type_set<TypesB...>>::type head_vert;
  typedef typename set_cross<type_set<TypesA...>, type_set<HeadB>>::type head_horiz;
  typedef typename set_cross<type_set<TypesA...>, type_set<TypesB...>>::type tail;
  typedef typename set_union<head_square, head_vert>::type u1;
  typedef typename set_union<u1, head_horiz>::type u2;
public:
  typedef typename set_union<u2, tail>::type type;
};
 
template<typename A, typename B>
struct set_cross
{
  typedef void type;
  static_assert(always_false<A,B>::value, 
                "Template arguments are no type sets");  
};

Equipped with these set operators you can do funky things like:

cout << is_subset<type_set<int,string>, type_set<int,string,int>>::value << endl;
cout << is_subset<type_set<int,string>, type_set<int>>::value << endl;
 
cout << is_set_equal<type_set<int, string>, 
                     reduce_set<type_set<int, string, int>>::type
                    >::value << endl;
cout << is_set_equal<set_union<type_set<int, string>,
                               type_set<int, bool, double>>::type, 
                     type_set<int, string, bool, double>>::value << endl;
 
cout << is_set_equal<set_intersection<type_set<int, string>,
                                      type_set<int, bool, double>>::type, 
                     type_set<int>>::value << endl;
 
cout <<is_set_equal<
        set_cross<
         type_set<int, string>,
         type_set<int, bool>
        >::type, 
       type_set<
        tuple<int,int>,
        tuple<int,bool>,
        tuple<string,int>, 
        tuple<string,bool>
       >
      >::value << endl;

I haven’t figured out whether there is an actual use case for set theoretic type constructions like this. If anybody comes up with an idea, that would be appreciated! Otherwise, it’s a nice exercise to learn how to use variadic templates.

Source:http://www.bleedingmind.com/index.php/2012/03/04/set-theory-in-cpp11/

C++  SET THEORY  MATH 

Share on Facebook  Share on Twitter  Share on Weibo  Share on Reddit 

  RELATED


  0 COMMENT


No comment for this article.