Concepts

Back to Generic programming

A concept is a set of syntactical requirements for a type. Concepts are used for constrained overloading of function templates, and to state requirements for the instantiation of function templates and class templates.

Proper support for defining and checking concepts requires language support, which will be available shortly in the form of the Concepts Technical Specification. Meanwhile, we provide an emulation layer for concepts using C++14.

This emulation layer is based on the ideas of Eric Niebler.

Error messages

C++ is well known for its support for generic programming via templates. Almost as well known are the cryptic error messages which are triggered when a template argument does not conform to the required concept. The problem is that the unfilled requirement is often detected very late in a deep instantiation stack of generic code, thus exposing lots of implementation details, none of which clearly states what the missing requirement for a type was.

The cryptic error messages can be avoided by checking — at the time of instantation — whether the template arguments model their concepts. This is called concept checking.

Defining concepts

In the emulation layer, a concept is a class, together with a member function template requires. The template argument of requires encodes the type to be tested, and the argument of requires provides an object that type.

struct Printable_Concept
{
    template <typename Type>
    auto requires_(Type&& t) -> decltype
    (
        conceptCheck(
            Concept::convertsTo<std::string>(asString(t))
        )
    );
};

Here we define the Printable concept, whose only requirement is that there must exist a free function asString(t), which takes as input an object of the type, and outputs an object which is implicitly convertible to an std::string.

decltype

By using the decltype in the return-type, we are able to write arbitrary expressions as concept-requirements.

conceptCheck

The conceptCheck function is a variadic template which accepts anything, and has return type void. The reason for calling this function is to be able to write a sequence of expressions; without a function call a comma is interpreted as a comma-operator.

Specifying requirements

Function calls

A free function with a non-void return-type is required by calling it:

asString(t)

A free function with a void return-type is required by calling it, followed by the comma-operator:

(f(t), 0)

The return-type of a function can be required to be implicitly convertible to a given type by wrapping the call into the Concept::convertsTo<ReturnType>(...) function call:

Concept::convertsTo<std::string>(asString(t))

The return-type can be required to be explicitly convertible to a given type by explicit conversion:

(std::string)(asString(t))

Note that this is weaker than implicit convertibility.

Member types

A concept may require the existence of a member type. This requirement is best embedded into default template arguments.

struct Printable_Concept
{
    template <
        typename Type,
        typename Member = typename Type::Member>
    auto requires_(Type&& t) -> decltype
    (
        conceptCheck(
            Concept::convertsTo<std::string>(asString(t)),
            Concept::holds<std::is_integral<Member>>()
        )
    );
};

The template argument Member is useful as a shorthand for typename Type::Member in subsequent concept-requirements. Alternatively, the existence of a member type can be checked with

Concept::exists<typename Type::Member>()

Type-traits

We may test whether a type-trait holds by calling

Concept::holds<std::is_integral<Member>>()

Checking concepts

If a type T does not model the Printable_Concept, then the return-type deduction of Printable_Concept::requires fails, and is treated by the compiler through the SFINAE rule (Substitution Failure Is Not An Error). We can then test whether T satisfies the requirements of Printable_Concept based on SFINAE function overloading, using the type-function Models:

static_assert(Models<int, Printable_Concept>::value, "int does not model the Printable concept.");

We may check that a type models a concept by:

PASTEL_CONCEPT_CHECK(int, Printable_Concept);

We may check that a type does not model a concept by:

PASTEL_CONCEPT_REJECT(void, Printable_Concept);

Refining concepts

A concept can be refined by deriving from the concept, wrapped in a Refines class template:

struct Additive_SemiGroup_Concept
: Refines<Element_Concept>
{
    template <typename Type>
    auto requires_(Type&& t) -> decltype
    (
        conceptCheck(
            Concept::hasType<Type&>(t += t),
            Concept::convertsTo<Type>(t + t)
        )
    );
};

A concept can refine multiple concepts; the Refines class template is a variadic template. The Refines wrapper is needed to be able to traverse the concept-tree at compile-time.

Most refined concept

Given a type T, and a concept C, we can find the most-refined concept of C modeled by T by the alias template

MostRefinedConcept<T, Additive_SemiGroup_Concept>

If T does not model any concept in C, then this type is void. The most-refined concept can be used for function overloading, when a better algorithm is available for a more refined concept. An example of this is advancing an iterator 10 times.

Coarsest failed concepts

Given a type T, and a concept C, we can find the coarsest concepts of C not modeled by T by the alias template

CoarsestFailedConcept<std::string, Additive_SemiGroup_Concept>

These concepts are encoded as the template arguments of the Refines template. The compiler can be forced to output the the coarsest concepts that T fails by using invalid syntax (Type is not a member of the Refines template):

CoarsestFailedConcept<std::string, Additive_SemiGroup_Concept>::Type;

This is useful for debugging concept-related issues.

Limitations of the emulation layer

Here are some limitations of the concept-emulation layer.

Concept-requirement errors

If there is a syntactic error in a concept-requirement itself, then this will also be treated as a SFINAE error, which causes the concept to trivially reject all types. Clang and gcc will provide the reasons for why the SFINAE-error occurred. This is not so for Visual C++ 2015 RC, where debugging such errors will amount to a binary search on the concept requirements, to find the offending requirement.

Native types

Native types are sometimes treated differently in the concept-requirement check:

struct Realish_Concept
{
    template <typename Type>
    auto requires_(Type&& t) -> decltype
    (
        conceptCheck(
            Concept::convertsTo<bool>(isInfinity(t))
        )
    );
};

bool isInfinity(float that)
{
    return true;
}

PASTEL_CONCEPT_CHECK(float, Realish_Concept);

Logically, you would first define the concept (Realish_Concept), and then define models of that concept (float). However, this code does not compile in Clang and gcc. It compiles in Visual C++ 2015 RC, but only because it implements name lookup incorrectly.

Since the isInfinity function call refers to t, the function is a dependent name, and will be looked up at the dependent-phase of two-phase name lookup. Argument-dependent lookup (ADL) is then used to search the function from the namespace of the argument. The search fails because a native type does not have an associated namespace. If float were a struct instead, this code would compile.

A workaround is to define the needed functions for native types before the concept is defined:

bool isInfinity(float that)
{
    return true;
}

struct Realish_Concept
{
    template <typename Type>
    auto requires_(Type&& t) -> decltype
    (
        conceptCheck(
            Concept::convertsTo<bool>(isInfinity(t))
        )
    );
};

PASTEL_CONCEPT_CHECK(float, Realish_Concept);

Types in other namespaces

The problem with native types generalizes. Suppose you wish to make std::vector support Printable_Concept by default, with this case implemented after the concept. Then ADL fails to find asString, because it looks in the std namespace. One bad way to solve this would be open the std namespace and place the asString function for std::vector there. This is a bad solution; it may work today, but may not work tomorrow because of modifications to the std namespace. The correct solution, as with native types, is to place the version for asString before the concept. Similarly for any other types you wish to model the concept by default.

Function templates without parameters

Function templates without parameters are not dependent names, and so cannot be checked in the concept:

struct Realish_Concept
{
    template <typename Type>
    auto requires_(Type&& t) -> decltype
    (
        conceptCheck(
            Concept::convertsTo<Type>(infinity<Type>())
        )
    );
};

struct A {};

template <
    typename Type,
    EnableIf<std::is_same<Type, A>> = 0>
Type infinity()
{
    return Type();
}

PASTEL_CONCEPT_CHECK(A, Realish_Concept);

Since the infinity function template call does not depend on objects of type Type, it is resolved in the first non-dependent phase of two-phase name lookup. Since there is no preceding definition of the function template infinity, an error is generated.

Files

Concept module

Concept-checking

Find the coarsest concepts not modeled by a type

Find the most refined concept modeled by a type

Testing for concepts