Home/Blog/Programming/Learn about concept in C++ 20
Home/Blog/Programming/Learn about concept in C++ 20

Learn about concept in C++ 20

Syed Atif Mehdi
Jan 11, 2024
5 min read

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

#

Introduction#

The most recent version of C++ was released in 2020 as C++ 20. It includes several enhancements and new features that increases the flexibility and capability of the language. This blog will examine some of the most essential and valuable features of C++20 with example codes that might be helpful for programmers.

In this blog, we'll be discussing concept with various examples.

concept#

concept is a new language feature introduced in C++20. It is defined in the <concepts> header. It provides a way to define requirements on template arguments, enabling more concise and readable code. This is achieved by allowing the programmer to specify constraints on the template parameters. These constraints are checked at compile-time without sacrificing type safety. This helps identify errors in templatized code early in the development cycle, improving code reliability.

widget

Generally, type traits are used to ensure template arguments meet certain requirements. However, type traits can be challenging to understand and can lead to verbose code. Concepts provide a way to define these requirements directly in the template parameter list.

Defining concept#

A concept can be easily defined as follows:

template <typename T>
concept Integral = std::is_integral_v<T>;

The code above defines an Integral concept, which ensures that the T parameter type is of integral type. This concept can be used to add two numbers in the following way:

template <Integral T>
T Add(T lhs, T rhs)
{
return lhs + rhs;
}

The Add template function uses the Integral concept to ensure that the parameters passed are of integral type. The function then adds the two numbers and returns their sum.

int main()
{
/*
Integral Types are:
char , signed char , unsigned char -8 bits
short int , signed short int , and unsigned short int -16 bits
int , signed int , and unsigned int -32 bits
long int , signed long int , and unsigned long int -32 bits (OpenVMS)
long int , signed long int , and unsigned long int -64 bits (Digital UNIX)
signed __int64 (Alpha) and unsigned __int64 (Alpha)-64 bits
enum -32 bits
*/
std::cout<<"The sum of two integers is "<< Add(10, 20)<<"\n";
std::cout<<"The sum of two 8 bit characters converted to ASCII is "<< Add('a', 'b')<<"\n";
//Following line will generate an error as floating point is not an integral value.
// std::cout<<"The sum of two doubles is "<< Add(10.2, 20.3)<<"\n";
return 0;
}

In lines 14–15 in the code above, the sum of two int and two char have been performed using the Add function. The code will successfully generate the correct output, as shown below. Remember that ASCII characters are 8-bit, and there will be an overflow.

The result of adding two integers and two 8-bit characters
The result of adding two integers and two 8-bit characters

However, if line 17 is uncommented, the code will generate a compile-time error, as shown below. The Add function is defined for integral values but not for floating point values.

Compile-time error when a float or double is being added using the Add function
Compile-time error when a float or double is being added using the Add function

Including a list of constraints#

concept can also be used to include a list of constraints. This can be done using requires. The following code defines a String concept that requires a T type to have a c_str member function that returns a const char *.

widget

The print_string template function is defined using the String concept. It's important to note the use of requires in the definition of the function. This ensures that the T will follow the String concept. The print_string function prints the value using the c_str() function.

template <typename T>
concept String = requires(T s)
{
{ s.c_str() } -> std::convertible_to<const char*>;
};
template <typename T>
requires String<T>
void print_string(T value)
{
std::cout << value.c_str();
}

User-defined data types#

Let's take another example that explains how a concept with the + operator works with a specific type. In this code, requires is being used to include a list of constraints.

template <typename T>
concept Addable = requires(T x, T y)
{
{x + y} -> std::convertible_to<T>;
};

In the example above, the Addable concept requires two objects of type T and is labeled as requires. The expression {x + y} has to be a valid statement, and the result of the statement is convertible to the same type T as x and y.

With this Addable concept, we can write a function that requires its arguments to be addable.

template <Addable T>
T Sum(T a, T b)
{
return a + b;
}

In the example above, the Sum function takes two arguments of type T and requires that T satisfies the Addable concept. This means that the + operator is guaranteed to be available for the arguments, and the function can be used with any type that satisfies the concept.

The following code uses the concept and function defined above to evaluate the sum of two Rational numbers.

struct Rational
{
int Numerator;
int Denominator;
Rational operator + (const Rational & rhs) const
{
return {Numerator * rhs.Denominator + Denominator * rhs.Numerator, Denominator * rhs.Denominator};
}
};
template <>
struct std::common_type<Rational>
{
using type = Rational;
};
int main()
{
Rational num1 {1, 2};
Rational num2 {1, 3};
Rational result = Sum (num1, num2);
std::cout <<"The sum of two rational numbers is "<< result.Numerator << "/" << result.Denominator <<"\n";
// Output: The sum of two rational numbers is 5/6
std::cout<<"The sum of two integers is "<< Sum(10, 20)<<"\n";
// Output: The sum of two integers is 30
std::cout<<"The sum of two doubles is "<< Sum(10.5, 20.9)<<"\n";
// Output: The sum of two doubles is 31.4
return 0;
}

In lines 1–10, a user-defined type Rational representing a rational number has been defined. The + operator in lines 6–9 that satisfies the requirements of the Addable concept has been implemented in the structure. Finally, a std::common_type specialization has been provided to specify that the common type of two Rational objects is also Rational. In the main function, two objects of type Rational have been created and initialized. The Sum function has been called using these two objects. The function calls the overloaded + operator and returns the result of the addition, which is stored in another result object and displayed on the console. Moreover, two examples of primitive datatypes, int and double, have also been provided to elaborate on the use of concept. As the concept of the + operator has been defined in this example, the concept can be used to ensure the availability of other operators for different data types.

Refinement#

Let's take another example where concept can be used for refinement. In the following code, a Range has been defined that requires the type T to implement begin and end functions that return an iterator of the same type.

template <typename T>
concept Range = requires(T t)
{
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
};
template <Range R>
void print_range(const R & lhs)
{
for (auto it = lhs.begin(); it != lhs.end(); it++)
std::cout << *it << " ";
}

The print_range function in lines 9–13 implements the Range concept to print all container content starting from the first to the last element.

int main() {
// Initializing a vector with C++20 initializer syntax
std::vector<int> vec{ 1, 2, 3, 4, 5 };
print_range(vec);
//Output: 1 2 3 4 5
//int array[] = {1,2,3,4,5};
//print_range(array);
// Compile-time error: no matching function for call to ‘print_range(int [5])’
return 0;
}

In line 3, a vector has been defined and initialized. In line 4, the print_range function has been called to print the vector. The output is as desired. However, when lines 6–7 are uncommented, an error is generated at compile-time, indicating that no matching function exists for the int array because it doesn't have begin and end functions. This saves any runtime error that may have caused the termination of an execution.

Conclusion#

In short, concept provides a more readable and maintainable way to define constraints on template parameters. It makes it easier to express the requirements of a template argument and to write generic code that works with a wide range of types.

We hope that this blog has not only triggered your quest to learn more about C++ 20 but also inspired you to learn to code in C++ with greater depth. For further readings please continue with the following courses:

C++ Concepts: Improve Type Safety with C++ 20 is all about using concept for type safety.

The All-in-One Guide to C++20 introduces C++20 in great detail.

C++ Fundamentals for Professionals is for refreshing C++.

Frequently Asked Questions

What are the features released in C++20?

  • Provided a way to specify constraints on template parameters, improving code readability and compile-time error messages.
  • Introduced a new way to work with sequences, enabling more expressive and efficient code.
  • Allowed for asynchronous programming and non-blocking code execution, enhancing performance in concurrent applications.

  

Free Resources