First impression of concepts



I decided to deal with the new C ++ 20 feature - concepts.

Concepts (or concepts , as the Russian-speaking Wiki writes) is a very interesting and useful feature that has long been lacking.

Essentially, it is typing for template arguments.

The main problem of templates before C ++ 20 is that you could substitute anything in them, including something that they were not designed for at all. That is, the template system was completely untyped. As a result, incredibly long and completely unreadable error messages occurred when passing the wrong parameter to the template. They tried to fight this with the help of different language hacks, which I do not even want to mention (although I had to deal with).

Concepts are designed to correct this misunderstanding. They add a typing system to the templates, and it’s very powerful. And now, understanding the features of this system, I began to study the available materials on the Internet.

Frankly, I'm a little shocked :) C ++ is an already complicated language, but at least there is an excuse: it happened. Metaprogramming on templates was discovered, not laid down, when designing a language. And then, when developing the next versions of the language, they were forced to adapt to this “discovery”, since a lot of code was written in the world. Concepts are a fundamentally new opportunity. And, it seems to me, some opacity is already present in their implementation. Perhaps this is a consequence of the need to take into account the huge amount of inherited capabilities? Let's try to figure it out ...

General information


A concept is a new language entity based on template syntax. A concept has a name, parameters, and a body - a predicate that returns a constant (i.e., computed at the compilation stage) logical value depending on the parameters of the concept. Like this:

template<int I> 
concept Even = I % 2 == 0;  

template<typename T>
concept FourByte = sizeof(T)==4;

Technically, concepts are very similar to template constexpr expressions like bool:

template<int I>
constexpr bool EvenX = I % 2 == 0; 

template<typename T>
constexpr bool FourByteX = sizeof(T)==4;

You can even use concepts in common expressions:

bool b1 = Even<2>; 

Using


The main idea of ​​the concepts is that they can be used instead of the typename or class keywords in templates. Like metatypes ("types for types"). Thus, static typing is introduced into the templates.

template<FourByte T>
void foo(T const & t) {}

Now, if we use int as a template parameter, then the code in the vast majority of cases will compile; and if double, then a short and understandable error message will be issued. Simple and clear typing of templates, so far everything is ok.

requires


This is a new “contextual” C ++ 20 keyword with a dual purpose: requires clause and requires expression. As will be shown later, this weird keyword savings leads to some confusion.

requires expression


First, consider requires expression. The idea is quite good: this word has a block in braces, the code inside of which is evaluated for compilation. True, the code there should not be written in C ++, but in a special language, close to C ++, but having its own characteristics (this is the first oddity, it was quite possible to make just C ++ code).

If the code is correct - requires expression returns true, otherwise false. The code itself, of course, never gets into code generation ever, much like expressions in sizeof or decltype.

Unfortunately, the word is contextual and works only inside templates, that is, outside the template, this does not compile:

bool b = requires { 3.14 >> 1; };

And in the template - please:

template<typename T>
constexpr bool Shiftable = requires(T i) { i>>1; };

And it will work:

bool b1 = Shiftable<int>; // true
bool b2 = Shiftable<double>; // false

The main use of requires expression is the creation of concepts. For example, this is how you can check for the presence of fields and methods in a type. A very popular case.

template <typename T>
concept Machine = 
  requires(T m) {  //   `m` ,   Machine
	m.start();     //    `m.start()` 
	m.stop();      //   `m.stop()`
};  

By the way, all variables that may be required in the tested code (not only the template parameters) must be declared in parentheses requires expression. For some reason, declaring a variable is simply not possible.

Type checking inside requires


This is where the differences between requires code and standard C ++ begin. To check the returned types, a special syntax is used: the object is taken in curly brackets, an arrow is placed, and after it, a concept is written that the type must satisfy. Moreover, the use of directly types is not allowed.

Check that the return of the function can be converted to int:

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}  

Check that the return function is exactly int:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>; 
}  

(std :: same_as and std :: convertible_to are concepts from the standard library).

If you do not enclose an expression whose type is checked in braces, the compiler does not understand what they want from him and interprets the entire string as a single expression that needs to be checked for compilation.

requires inside requires


The requires keyword has a special meaning inside requires expressions. Nested requires-expressions (already without curly braces) are checked not for compilation, but for equality true or false. If such an expression turns out to be false, then the enclosing expression immediately turns out to be false (and further compilation analysis is interrupted). General form:

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

As a predicate, for example, previously defined concepts or type traits can be used. Example:

requires(Iter it) {
  //     (   Iter   *  ++)
  *it++;
 
  //    -  
  requires std::convertible_to<decltype(*it++), typename Iter::value_type>;
 
  //    -  
  requires std::is_convertible_v<decltype(*it++), typename Iter::value_type>;
}

At the same time, nested requires-expressions are allowed with code in curly brackets, which is checked for validity. However, if you simply write one requires-expression inside another, then the nested expression (everything as a whole, including the nested requires keyword) will simply be checked for validity:

requires (T v) { 
  requires (typename T::value_type x) { ++x; }; //     , 
												//     !
};  

Therefore, a strange form with double requires arose:

requires (T v) { 
  requires requires (typename T::value_type x) { ++x; }; //       "++x"
};  

Here is such a fun escape sequence from “requires”.

By the way, another combination of two requires is this time clause (see below) and expression:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

requires clause


Now let's move on to another use of the word requires - to declare restrictions of a template type. This is an alternative to using concept names instead of typename. In the following example, all three methods are equivalent:

//  require
template<typename Cont>
	requires Sortable<Cont>
void sort(Cont& container);

//   require (  )
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;

//    typename
template<Sortable Cont>
void sort(Cont& container)  

The requires declaration can use several predicates combined by logical operators.

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 
 
int main()
{
  std::string s;
 
  fun(1);  // ok
  fun(s);  // compiler error
}

However, just invert one of the conditions, as a compilation error occurs:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v); 

Here is an example that will not compile either

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);	

The reason for this is the ambiguities that arise when parsing some expressions. For example, in such a template:

template <typename T> 
  requires (bool)&T::operator short unsigned int foo();

it is unclear what to attribute unsigned to - the operator or the prototype of the foo () function. Therefore, the developers decided that without parentheses as arguments requires clause only a very limited set of entities can be used - true or false literals, field names of the bool type of the form value, value, T :: value, ns :: trait :: value, Concept names of the type Concept and requires expressions. Everything else should be enclosed in parentheses:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

Now about predicate features in requires clause


Consider another example.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v); 

In this example, requires uses a trait that depends on the nested value_type type. It is not known in advance whether an arbitrary type has such a nested type that can be passed to the template. If you pass, for example, a simple int type to such a template, there will be a compilation error, however, if we have two specializations of the template, then there will be no error; just another specialization will be chosen.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 
 
template <typename T>
void fun(T v) { std::cout << "2"; } 
 
int main()
{
  fun(1);  // displays: "2"
}

Thus, specialization is discarded not only when the require clause predicate returns false, but also when it turns out to be incorrect.

The parentheses around the predicate are an important reminder that in requires clause the inverse of the predicate is not the opposite of the predicate itself. So,

requires is_trivial_v<typename T::value_type> 

means that the trait is correct and returns true. Wherein

!is_trivial_v<typename T::value_type> 

would mean “the trait is correct and returns false” The
real logical inversion of the first predicate is NOT (“the trait is correct and returns true”) == “the trait is INCORRECT or returns false” - this is achieved in a slightly more complex way - through an explicit definition of the concept:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 
 
template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v); 

Conjunction and Disjunction


The logical conjunction and disjunction operators look as usual, but actually work a little differently than in normal C ++.

Consider two very similar code snippets.

The first is a predicate without parentheses:

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
		|| std::is_trivial_v<typename U::value_type>
void fun(T v, U u); 

The second is with brackets:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
		 || std::is_trivial_v<typename U::value_type>)
void fun(T v, U u); 

The difference is only in brackets. But because of this, in the second template, there are not two constraints united by a “require-clause”, but one united by a usual logical OR.

This difference is as follows. Consider the code

std::optional<int> oi {};
int i {};
fun(i, oi);

Here the template is instantiated by types int and std :: optional.

In the first case, the int :: value_type type is invalid, and the first limitation is thereby not satisfied.

But the type optional :: value_type is valid, the second trait returns true, and since there is an OR operator between the constraints, the whole predicate is satisfied as a whole.

In the second case, this is a single expression containing an invalid type, because of which it is invalid in general and the predicate is not satisfied. So simple brackets imperceptibly change the meaning of what is happening.

In conclusion


Of course, not all features of the concepts are shown here. I just did not go further. But as a first impression - a very interesting idea and a somewhat strange confused implementation. And a funny syntax with repeating requires, which really confuses. Is there really so few words in English that you had to use one word for completely different purposes?

The idea with compiled code is definitely good. It is even somewhat similar to “quasi-quoting” in syntax macros. But was it worth mixing up the special syntax for checking return types? IMHO, for this it would simply be necessary to make a separate keyword.

Implicit mixing of the concepts “true / false” and “compiles / does not compile” in one heap, and as a result, jokes with brackets are also wrong. These are different concepts, and they must exist strictly in different contexts (although I understand where it came from - from the SFINAE rule, where uncompiled code just silently excluded specialization from consideration). But if the goal of the concepts is to make the code as explicit as possible, was it worth it to drag all these implicit things into new features?

The article is written mainly on the basis of
akrzemi1.wordpress.com/2020/01/29/requires-expression
akrzemi1.wordpress.com/2020/03/26/requires-clause
(there are much more examples and interesting features)
with my additions from other sources,
all examples can be checked onwandbox.org

All Articles