📜 ⬆️ ⬇️

Concepts for the desperate

It all started with the fact that I needed to write a function that assumes ownership of an arbitrary object. It would seem that it could be simpler:


template <typename T> void f (T t) { //   `t`  `T`. ... //  — . g(std::move(t)); //   —  . ... } 

But there is one nuance: it is required that the received object be strictly rvalue . Therefore, it is necessary:


  1. Report a compilation error when trying to pass lvalue .
  2. Avoid unnecessary constructor calls when creating an object on the stack.

But this is more difficult to do.


I will explain.


Input Argument Requirements


Suppose we want the opposite, that is, so that the function lvalue only lvalue and does not compile if rvalue is fed to it. For this, the language has a special syntax:


 template <typename T> void f (T & t); 

Such a record means that the function f accepts an lvalue link to an object of type T However, cv qualifiers are not agreed in advance. This may be a reference to a constant, or a reference to a nonconstant, or any other options.


But it cannot be a reference to rvalue : if you pass a reference to rvalue to the function f , the program will not compile:


 template <typename T> void f (T &) {} int main () { auto x = 1; f(x); //  , T = int. const auto y = 2; f(y); //  , T = const int. f(6.1); //  . } 

Maybe there is a syntax for the opposite case, when you need to take only rvalue and report an error when passing lvalue ?


Unfortunately not.


The only way to accept an rvalue link to an arbitrary object is through a forwarding reference :


 template <typename T> void f (T && t); 

But the pass-through link can be either a rvalue link or an lvalue link. Consequently, we have not yet achieved the desired effect.


You can achieve the desired effect with the help of the SFINAE mechanism, but it is quite cumbersome and inconvenient for both writing and reading:


 #include <type_traits> template <typename T, typename = std::enable_if_t<std::is_rvalue_reference<T &&>::value>> void f (T &&) {} int main () { auto x = 1; f(x); //  . f(std::move(x)); //  . f(6.1); //  . } 

And what would you really want?


I would like to write this:


 template <typename T> void f (rvalue<T> t); 

I think the meaning of this record is expressed quite clearly: take an arbitrary rvalue .


The first thought that comes to mind is to create a type alias:


 template <typename T> using rvalue = T &&; 

But such a thing, unfortunately, does not work, because the substitution of the alias occurs before the output of the template type, so in this situation, the entry rvalue<T> in the function arguments is completely equivalent to the T && entry.


Hidden text

It's funny that due to an error in the Clang compiler type inference system (I don’t remember exactly the version, it seems 3.6), this version worked. In the GCC compiler, this error was not, therefore, at first my mind was clouded with insane ideas, decided that the error was not in Klang, but in Getset. But, having conducted a small investigation, I realized that it was not. And after a while, this mistake was corrected in Klang.


Another idea - in essence, a similar one - that may come to the mind of the master metaprogramming expert is to write the following code:


 template <typename T> struct rvalue_t { using type = T &&; }; template <typename T> using rvalue = typename rvalue_t<T>::type; 

rvalue_t could be added to the rvalue_t SFINAE , which would fall off if T were a reference to lvalue .


But, unfortunately, this idea is also doomed to failure, because such a structure "breaks" the type inference mechanism. As a result, the function f will not be able to be called at all without explicitly specifying a template argument.


I was very upset and for a while abandoned this idea.


Return


At the beginning of this year, when there was news that the committee did not include concepts in the C ++ 17 standard , I decided to return to the abandoned idea.


A little thought, I formulated the "requirements":


  1. The type inference mechanism should work.
  2. It should be possible to set SFINAE -check on the inferred type.

It immediately follows from the first requirement that you still need to use type aliases.


Then the logical question arises: is it possible to set SFINAE on type aliases?


It turns out you can. And it will look, for example, as follows:


 template <typename T, typename = std::enable_if_t<std::is_rvalue_reference<T &&>::value>> using rvalue = T &&; 

Finally, we get both the required interface and the required behavior:


 template <typename T> void f (rvalue<T>) {} int main () { auto x = 1; f(x); //  . f(std::move(x)); //  . f(6.1); //  . } 

Victory.


Concepts


The attentive reader is indignant: "So where are the concepts here?".


But if he is not only attentive, but also clever, then he will quickly understand that this idea can also be used for "concepts." For example, as follows:


 template <typename I, typename = std::enable_if_t<std::is_integral<I>::value>> using Integer = I; template <typename I> void g (Integer<I> t); 

We have created a function that takes only integer arguments. At the same time, the resulting syntax is pleasant enough for both the writer and the reader.


 int main () { g(1); //  . g(1.2); //  . } 

What else can you do?


You can try to get even closer to the true syntax of concepts, which should look like this:


 template <Integer I> void g (I n); 

To do this, use, ahem, macro:


 #define Integer(I) typename I, typename = Integer<I> 

We will get the opportunity to write the following code:


 template <Integer(I)> void g (I n); 

This is where the capabilities of this technology end.


disadvantages


If you recall the title of the article, you might think that this technique has some flaws.


Taki yes. There is.


Firstly, it does not allow organizing overloading by concepts.
The compiler will not see the difference between function signatures.


 template <typename I> void g (Integer<I>) {} template <typename I> void g (Floating<I>) {} 

and will give an error about the redefinition of the function g .


Secondly, it is impossible to simultaneously check several properties of the same type. Or rather, perhaps, but it will be necessary to fence in rather complex constructions that will negate all readability.


findings


The above technique - let's call it the technique of filtering the pseudonym of types - has a rather limited scope.


But in those cases when it is applicable, it opens up quite good opportunities for a programmer to clearly express intentions in code.


I believe that she has the right to life. I personally use it . And I do not regret.


Related Links


  1. Library "Boost Concept Check"
  2. Concepts from the range-v3 range library prototype
  3. Library "TICK"
  4. Article "Concepts Without Concepts"

')

Source: https://habr.com/ru/post/304728/


All Articles