C++ 20 Concepts: part 1 (the basics)
Concepts is one of the 4 major additions in C++20 standard. The idea of concepts has existed as long as C++ templates themselves but it has only been until few years that the foundation of C++ concepts have been laid out in the manner it had earlier been desired desired, i.e., in coherence with C++ templates design principles:
- a generalized code
- better code than hand written code with zero overhead
- well specified interfaces on the template method/class usage.
C++ templates fell short on the last requirements but the first two requirements were good enough and templates few off with a huge success. In simpler terms the last requirement on concepts is about imposing constraints on the template parameters themselves, that a function or a class can accept.
In this article, I’ll give an introductory example of how concepts could be useful in terms of specifying the interface of a generic code, which among many advantages helps get rid of the verbose error message and achieve faster compilation, and consequently help make generic programming more expressive and fun. In the later long running series about concepts with gradually increasing complexity, I’ll present details as and when necessary. Lastly before beginning, I encourage all users to try out the code examples with a compiler that supports C++20 . So lets start with concepts of concepts ;)
1. The problem with classical template program
Consider a trivial generic print
method:
#include <iostream>template <typename T>
void print(const T& msg){
std::cout << msg;
}int main(){
int a{1};
char b{'2'};
std::string msg1{"hello wolrd!"}; print(a);
print(b);
print(msg);
return 0;
}
In the above simple example, one can look at the body of the template
-tized print
method and immediately figure that the method can print, apart from all fundamental data types, any type T
that provides an overloaded operator<<
.
For trivial print
method it is anyhow obvious. For a more complex method or an algorithm that works on for e.g. STL containers, it might be that the generic method requires the container to provide an overloaded subscript operator ( []
) or that the container it uses needs to provide both forward and backward iterators which might not be obvious even from a cursory glance to the body of the template
-tized method. In such cases passing a type that doesn’t provide the necessary interface causes the compiler to vomit a huge error message which might be a nightmare even in simple cases, let alone a production environment.
For e.g. , consider a scenario when one does not pay attention to the details of the print
method and assumes that it can also print the contents of std::vector
.
#include <iostream>
#include <vector>template<typeneme T>
void print(const T& msg){
std::cout << msg;
}int main(){
std::vector<int> v{1,2,3,4,5,6};
print(v);
}// a horrendously long compiler error
which probably will make the programmer run away from generic programming, which might have been the case for a long time for most of us!
The error is due to the result of compiler trying to generate code to print v
, which is of type std::vector<int>
and failing to do so since std::vector<T>
doesn’t provide an overloaded operator<<
in its interface. This error is substitution failure.
2. The solution: concepts
Concepts enable the first step check, whether or not a particular inteface is provided by the type T
, before it is substituted during compilation. C++ reference page says the following about concepts (in addition to the gory details):
A concept is a named set of requirements …
The definition of a concept has the form
template
<template-parameter-list>
concept concept-name = constraint-expression;
The concept-name
and constraint-expression
are decided by the server code (or the person who intends the template code to be used by other developers/users). In general the last two lines define the concept. The constraint-expression
actually imposes the constrains on the type T
and it is in this expression, it is defined what interface the type T
should provide. In addition to the concept
keyword, C++ provides requires
keyword in the core language feature to allow user to define their concepts.
Without going into technical details for the time being, I’ll present the concept for our print
method right away and keep the details for next issue. The newly added code is highlighted in bold:
#include <iostream>
#include <vector>//first define a concept for print
//lets call it 'Printable'template<typename T>
concept Printable = requires(std::ostream& os, const T& msg)
{
{os << msg};
};//now impose the constraint define above on print method
template <Printable T>
void print(const T& msg){
std::cout << msg;
}
Now, when the variable v
of type std::vector<int>
is passed as an argument to print
defined above, a much terse error message appears which at first glance does appear to be long, but in my opinion exponentially smaller than the ones without concept
based constraint . Second half of the error message looks like below
note: constraints not satisfied:In substitution of 'template<class T> requires Printable<T> void print(const T&) [with T = std::vector<int>]':required for the satisfaction of 'Printable<T>' [with T = std::vector<int, std::allocator<int> >]in requirements with 'std::ostream& os', 'const T& msg' [with T = std::vector<int, std::allocator<int> >]the required expression '(os << msg)' is invalid {os << msg};
~~~^~~~~~
which can be read as: constrains not satisfied in substitution of template type T=std::vector<int>
required for satisfaction of (concept) Printable<T>
in requirements with std::ostream& os
…
Here compiler tells clearly that print
imposes a constraint on the type T
which must satisfy the concept Printable
requiring T
to provide a suitable interface for std::ostream
.
3. A small exercise
If you’ve made it till this section then why not try a small exercise to write a generic code with concept.
- Use the same generic
print
method without theconcept
first.
template <typename T>
void print(const T& msg){
std::cout << msg;
}
2. Create a class Foo
with an int
data member, say intData_
and provide a constructor to initialize the member to a desired value
3. Instantiate an object Foo
and try to print its value by passing it to the generic print
method.
4. Observe the error if you are brave. If not (like me) then ignore it and transform the generic print
method to a concept
based print method like in section 2 of the article.
5. Again observe the error when trying to print the variable of type Foo
.
6. Complete the interface of Foo
by providing an overloaded operator<<
to print the value of the int
data member within Foo
.
Conclusion
I hope you enjoyed writing your first C++20 concept (at least enjoyed more compared to the naked generic programming). Stay tuned for more articles on concepts which will gradually provide the nifty details with increasing complexity.
C++20 videos on youtube and course
You can learn more about Concepts from on my youtube channel for concepts. Subscribe to my youtube channel to learn more about coroutines and other advanced topics on C++20 or browse my course page to get access to all C++20 videos, quizzes and personal support.
Support me by becoming a member
If you like my tutorials and articles, please consider supporting me by becoming a member through my medium referral link and get unlimited access to all articles on medium.com for just $5 a month.