Concepts-only tutorial

In this short tutorial, we introduce only the concept checking facilities of Catsfoot, for people not interested in the testing utilities. 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 versions of overloaded functions based on run-time behavior.

Concept requirements

Concepts describe a set of requirements, usually representing the existence 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 returns a type convertible to an instance of type U.

These requirements can be represented by these 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 operator() is dependant on the existence of the get method. Here this is achieved using decltype. The decltype expression resolves a template parameter involving operator() (but 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 mostly want to use automatic concepts. (Later, in introducing overloading based on concepts, we will use non-automatic concepts.)

Requirements are represented through the 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 the concept_list class template to list them all.

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 the 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 this 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 rather than a function has requirements, it is possible to check them like this:

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 functions like this 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 should note that here we have used only automatic concepts. A signature does not differentiate between two types. Sometimes two different types with the same signature might need to include different versions of an overloaded algorithm. For instance sorting an ordered container should not do anything whereas a sorting algorithm should be run for other containers. There would not be much difference in the signatures of the types, however. 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 types 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 different 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&) {}