Use-cases for Concepts

Discover use-cases of concepts in C++20.

First and foremost, concepts are compile-time predicates. A compile-time predicate is a function that is executed at compile-time and returns a boolean.

Before I dive into the various use-cases of concepts, I want to demystify concepts and present them simply as functions returning a boolean at compile time.

Compile-time predicates

A concept can be used in a control structure, which is executed at run time or compile-time. Run the code below.

C++
#include <compare>
#include <iostream>
#include <string>
#include <vector>
struct Test{};
int main() {
std::cout << "\n";
std::cout << std::boolalpha;
std:: cout << "std::three_way_comparable<int>: "
<< std::three_way_comparable<int> << "\n";
std::cout << "std::three_way_comparable<double>: ";
if (std::three_way_comparable<double>) std::cout << "True";
else std::cout << "False";
std::cout << "\n\n";
static_assert(std::three_way_comparable<std::string>);
std::cout << "std::three_way_comparable<Test>: ";
if constexpr(std::three_way_comparable<Test>) std::cout << "True";
else std::cout << "False";
std::cout << '\n';
std::cout << "std::three_way_comparable<std::vector<int>>: ";
if constexpr(std::three_way_comparable<std::vector<int>>) std::cout << "True";
else std::cout << "False";
std::cout << '\n';
}

In the program above, I use the concept std::three_way_comparable, which checks at compile-time if T supports the six comparison operators. Being a compile-time predicate means that std::three_way_comparable can be used at run time (lines 14 and 18) or at compile time. static_assert (line 23) and if constexpr (lines 26 and 32) are evaluated at compile time.

Class templates

The class template MyVector requires that its template parameter T be regular, meaning that T behaves such as an int. The formal definition of regular is provided later in this chapter.

Warning: The code gives an error message.

C++
#include <concepts>
#include <iostream>
template <std::regular T>
class MyVector{};
int main() {
MyVector<int> myVec1;
MyVector<int&> myVec2; // ERROR because a reference is not regular
}

Line 9 causes a compile-time error because a reference is not regular.

Generic member functions

In the code below, I add a generic push_back member function to the class MyVector. push_back requires that its arguments be copyable.

Warning: The code gives an error message.

C++
#include <concepts>
#include <iostream>
struct NotCopyable {
NotCopyable() = default;
NotCopyable(const NotCopyable&) = delete;
};
template <typename T>
struct MyVector {
void push_back(const T&) requires std::copyable<T> {}
};
int main() {
MyVector<int> myVec1;
myVec1.push_back(2020);
MyVector<NotCopyable> myVec2;
myVec2.push_back(NotCopyable()); // ERROR because not copyable
}

The compilation fails intentionally at line 18. Instances of NotCopyable are not copyable because the copy constructor is declared as deleted.

Variadic templates

You can use concepts in variadic templates.

C++
#include <concepts>
#include <iostream>
template <std::integral... Args>
bool all(Args... args) {
return(... && args);
}
template <std::integral... Args>
bool any(Args... args) {
return(... || args);
}
template <std::integral... Args>
bool none(Args... args) {
return not(... || args);
}
int main() {
std::cout << std::boolalpha << '\n';
std::cout << "all(5, true, false): " << all(5, true, false) << '\n';
std::cout << "any(5, true, false): " << any(5, true, false) << '\n';
std::cout << "none(5, true, false): " << none(5, true, false) << '\n';
}

The definitions of the function templates above are based on fold expressions. C++11 supports variadic templates that can accept an arbitrary number of template arguments. The arbitrary number of template parameters is held by a so-called parameter pack. Additionally, with C++17 you can directly reduce a parameter pack with a binary operator. This reduction is called a fold expression.

In this example, the && (line 6), (||) (line 11), and the negation of the logical or (line 16) are applied as binary operators. Furthermore, all, any, and none require from its type parameters that they have to support the concept std::integral.