Tech

C ++ 20: Concepts – the placeholder syntax

Today I give a simple answer to a challenging question: When can Concepts be used? They can be used where auto can be used.


C ++ 20: Concepts - the placeholder syntax


Before I write about the placeholder syntax and the new syntax for function templates, let me say a little bit. In C ++ 11/14 there are some asymmetries.

From asymmetries in C ++ 11/14 to symmetries in C ++ 20

I often have discussions of the following kind in my seminar:

What is the Standard Bird's Eye Template Library? Generic containers that can be manipulated with generic algorithms. The link between the two disjoint components are iterators.


C ++ 20: Concepts - the placeholder syntax


Generic containers and generic algorithms mean that they are not tied to a specific data type. Many of the algorithms can be parameterized by a callable unit. For example, owns std::sort an overload that takes on a binary predicate (callable unit). The binary predicate is applied to the elements of the container. A binary predicate is a function that has two parameters and returns a truth value. You can use a function, a function object or, for C ++ 11, a lambda expression as a binary predicate.

A slight asymmetry in C ++ 11

What conceptual error does the following program have? (I know that we are a predefined predicate std::greater own in C ++.)

// lambdaCpp11.cpp#include 
#include 
#include 
#include 
#include 

template 
void sortDescending(Cont& cont, Pred pred){
std::sort(cont.begin(), cont.end(), pred);
}

template 
void printMe(const Cont& cont){
for (auto c: cont) std::cout << c << " ";
std::cout << std::endl;
}

int main(){

std:: cout << std::endl;

std::array myArray{5, -10, 3, 2, 7, 8, 9, -4, 3, 4};
std::vector myVector{5.1, -10.5, 3.1, 2.0, 7.2, 8.3};
std::vector myVector2{"Only", "for", "testing", "purpose"};

sortDescending(myArray, 
()(int fir, int sec){ return fir > sec; });         // (1)
sortDescending(myVector, 
()(double fir, double sec){ return fir > sec; });   // (2)
sortDescending(myVector2,
()(const std::string& fir, const std::string& sec){ // (3)
return fir > sec; 
});

printMe(myArray);
printMe(myVector);
printMe(myVector2);

std::cout << std::endl;

}

The program has a std::array from ints, one std::vector from doubles and one std::vector from std::strings. All containers should be sorted and displayed in descending order. To make my job as easy as possible, I used the two function templates sortDescending and printMe,


C ++ 20: Concepts - the placeholder syntax

Although conainters and algorithms are generic, C ++ 11 has only type-bound lambdas. Therefore, I have to implement a binary predicate for each data type (line 1 -3) and therefore break my generic path. With C ++ 14 this asymmetry disappears. Containers and algorithms are generic and lambdas can be used generically.

The strong asymmetry in C ++ 20

With C ++ 14 will be the new implementation of the previous program lambdaCpp11.cpp much easier.

// lambdaCpp14.cpp#include 
#include 
#include 
#include 
#include 

template 
void sortDescending(Cont& cont){
std::sort(cont.begin(), cont.end(), ()(auto fir, auto sec){   // (1)
return fir > sec; 
});
}

template 
void printMe(const Cont& cont){
for (auto c: cont) std::cout << c << " ";
std::cout << std::endl;
}

int main(){

std:: cout << std::endl;

std::array myArray{5, -10, 3, 2, 7, 8, 9, -4, 3, 4};
std::vector myVector{5.1, -10.5, 3.1, 2.0, 7.2, 8.3};
std::vector myVector2{"Only", "for", "testing", "purpose"};

sortDescending(myArray);      // (2)
sortDescending(myVector);     // (2)
sortDescending(myVector2);    // (2)

printMe(myArray);
printMe(myVector);
printMe(myVector2);

std::cout << std::endl;

}

All good? Yes and no. Yes, because I can use a generic lambda expression (line 1) for each data type in line 2. No, as I have replaced the slight asymmetry in C ++ 11 with a strong asymmetry in C ++ 14. The slight asymmetry in C ++ 11 was that the lambda expression was type-bound. The strong asymmetry in C ++ 14 is that generic lambdas define a new syntactic form to write generic function (function templates). Here is the proof.

// genericLambdaTemplate.cpp#include 
#include 

auto addLambda = ()(auto fir, auto sec){ return fir + sec; }; // (1)

template                             // (2)
auto addTemplate(T fir, T2 sec){ return fir + sec; }

int main(){

std::cout << std::boolalpha << std::endl;

std::cout << addLambda(1, 5) << " " << addTemplate(1, 5) << std::endl;
std::cout << addLambda(true, 5) << " " << addTemplate(true, 5)
<< std::endl;
std::cout << addLambda(1, 5.5) << " " << addTemplate(1, 5.5) 
<< std::endl;

const std::string fir{"ge"};
const std::string sec{"neric"};
std::cout << addLambda(fir, sec) << " " << addTemplate(fir, sec) 
<< std::endl;

std::cout << std::endl;

}

The generic lambda expression in row 1 produces the same results as the function template in row 2.


C ++ 20: Concepts - the placeholder syntax

This is exactly the asymmetry in C ++ 14. Generic lambdas introduce a new way to define function templates.

When I explain this in my seminars, I almost always get the question: Can we auto to use in function declaration to get function templates? No, not with C ++ 17, but with C ++ 20. In C ++ 20, Constrained Placeholders (Concepts) or Unconstrained Placeholders (auto) can be used in function declaration. This turns the function into a function template. Of course that also means that the asymmetry in C ++ 20 has been overcome.

The solution in C ++ 20

Before I try to define myself with the new kind of function templates, I would like to answer the original question: When can Concepts be used? Concepts can be used in the places where auto can be used.

// placeholdersDraft.cpp#include 
#include 
#include 

template                                   // (1)
concept Integral = std::is_integral::value;

Integral auto getIntegral(int val){                    // (2)
return val;
}

int main(){

std::cout << std::boolalpha << std::endl;

std::vector vec{1, 2, 3, 4, 5};
for (Integral auto i: vec) std::cout << i << " ";  // (3)

Integral auto b = true;                            // (4)
std::cout << b << std::endl;

Integral auto integ = getIntegral(10);             // (5)
std::cout << integ << std::endl;

auto integ1 = getIntegral(10);                     // (6)
std::cout << integ1 << std::endl;

std::cout << std::endl;

}

The concept Integral in line 1 can be used as a return type (line 2), in a range base forLoop (line 3), or as a data type for the variable b (Line 3) or the variable integ (Line 5). To bring the symmetry to the point, I turn in line 6 type deduction with auto on. I have to admit that at the moment there is no compiler that fully supports the syntax suggested for standardization, which I used in my example. The following issue is therefore based on the GCC and the provisional syntax of the Concepts TS (Technical Specification).


C ++ 20: Concepts - the placeholder syntax

What's next?

My next article deals with Syntactic Sugar, which we use with Constrained Placeholders (Concepts) and Unconstrained Placeholders (auto) in C ++ 20. I think the function (the function template) getIntegral gives a first taste.