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.
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.
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
.
By using the decltype
in the return-type, we are able to write arbitrary expressions as concept-requirements.
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.
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.
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>()
We may test whether a type-trait holds by calling
Concept::holds<std::is_integral<Member>>()
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);
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.
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.
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.
Here are some limitations of the concept-emulation layer.
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 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);
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 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.