Concepts-only tutorial

In this tutorial, we introduce the concept based checking only for people not interested in testing utilities from Catsfoot. We expect that you are familiar with C++ templates.

Concepts in this case are useful for two things:

  • giving better error messages to the user of a template library.
  • selecting optimized version of overload functions based run-time behavior.

Concept requirements

Concepts describe a set of requirements, usually representing the existense of members (types, methods, or static methods), the existence of compatible overloaded functions, and the relations between types (convertible, castable, inheriting, same...). For instance it is possible to check that a type T has a non-const method get that return a type convertible to an instance of type U.

These requirements can be represented by those two predicates:

is_callable<member_get(T&)>

And:

is_convertible<typename is_callable<member_get(T&)>::result_type,
               U>

member_get is a functor produced with macro DEF_MEMBER_WRAPPER(get). But it is possible to write it by hand.

For instance:

struct member_get {
   template <typename T, typename... U,
             typename Ret =
             decltype(std::declval<T>()
                     .get(std::declval<U>()...))>
   Ret operator()(T&& t, U&&... u...) const {
     return std::forward<T>(t).get(std::forward<U>(u)...);
   }
};

It is not important to make the functor that generic. However, it is very important that the return type of the operator parenthesis is dependant on the existence of the method using decltype. The decltype expression needs then to be dependent to a template parameter from the operator parenthesis (and not the functor).

If you fail to provide such a functor. Errors will not be properly detected.

Specifying a concept

Now we can write our first concept. If axioms do not interest you, you will probably most of the time want to use automatic concepts. However introducing overloading based on concepts, we will use then non-automatic concept.

Requirements are represented throught member type requirements of the concept. For instance.

template <typename T, typename U>
struct has_get: public auto_concept {
private:
  //easier with an alias to the predicate
  typedef is_callable<member_get(T&)> call;

public:
  typedef concept_list<
    call,
    is_convertible<typename call::result_type, U>
  > requirements;
};

If the concept has several requirements, like here, we just use class template concept_list.

When asserting a concept, the compiler will report only the missing predicates, not the missing concepts. Predicates should be seen as the lowest level of requirements.

If we have a function requiring concept has_get, we can easily check against the concept.

template <typename SimpleStream>
int something_useless(SimpleStream& s) {
  assert_concept(has_get<SimpleStream, int>{});
  int res = 0;
  for (unsigned i = 0; i < 8; ++i) {
    res ^= int(s.get());
  }
  return res;
}

If for example, the result of get was not convertible to int, then we will get kind of error message:

/usr/include/catsfoot/concept/concept_tools.hh: At global scope:
/usr/include/catsfoot/concept/concept_tools.hh: In instantiation of
  'catsfoot::details::class_assert_concept<std::
  is_convertible<return_type, int>, false, false, false>':
[...]
/usr/include/catsfoot/concept/concept_tools.hh:125:7: error: static
  assertion failed: "Missing requirement"

If a class template instead of a function has requirements, it is possible to check

template <typename SimpleStream>
struct some_class {
  //[...]
  private:
    class_axiom_assert<has_get<SimpleStream, int> > check;
};

Concept-based overloading

If we wanted to have a default implementation for types not implementing has_get, we could write such functions instead:

template <typename SimpleStream, ENABLE_IF(has_get<SimpleStream, int>)>
int something_useless(SimpleStream&);

template <typename NotAStream, ENABLE_IF_NOT(has_get<NotAStream, int>),
          typename = void> // work-around to make signatures different
int something_useless(NotAStream&);

In this case, the first function would be used if and only if SimpleStream implemented has_get. The second would be used otherwise.

It is possible to do the same for classes:

template <typename SimpleStream, bool = IF(has_get<SimpleStream, int>)>
struct some_class {
  // [...]
};

template <typename SimpleStream>
struct some_class<SimpleStream, false> {
  // [...]
};

You have to note that we have used here only automatic concepts. A signature does not differentiate two types. Sometimes two different types with the same signature might need to have different version of an overloaded algorithm. For instance sorting an ordered container should not do anything whereas it should run a sorting algorithm in all the other containers. There would be however not much difference on the signature of the types. In this case we can use concepts.

template <typename C>
struct sorted_container: concept {
  typedef container<C> requirements;
};

Since sorted_container is not automatic, no type by default implements it.

We can then populate the concept by marking them as "verified" through type traits.

namespace catsfoot {
  template <typename T>
  struct verified<sorted_container<std::set<T> > >:
    public std::true_type {};
}

Now we can write our two differen versions:

template <typename C,
          ENABLE_IF(container<C>),
          ENABLE_IF_NOT(sorted_container<C>)>
void sort(C&) {
  // [...]
}

template <typename C,
          ENABLE_IF(sorted_container<C>)>
void sort(C&) {}