C++20 concepts for nicer compiler errors

C++20 concepts for nicer compiler errors

In C++, templates enable generic programming by allowing functions and classes to operate on different data types without sacrificing type safety. Defined using the template keyword, they let developers write reusable, type-agnostic code, such as functions (e.g., template <typename T> max(T a, T b)) or classes (e.g., std::vector), where the type T is specified at compile time.

Historically, the C++ language has tended to produce complicated compiler error messages. The main culprit is template metaprogramming. C++ templates are powerful but complex. When errors occur in template code, the compiler generates long, verbose messages with nested type information, often involving deep template instantiations. A simple mistake in a template function can produce a message spanning multiple lines with obscure type names.

Let us consider an example. In C++, we often use the ‘Standard Template Library (STL)’. It includes a useful dynamic array template: std::vector. A vector manages a sequence of elements with automatic memory handling and flexible sizing. Unlike fixed-size arrays, it can grow or shrink at runtime through operations like push_back to append elements or pop_back to remove them. You can store just about anything in an std::vector but there are some limits. For example, your type must be copyable.

Let us create a C++ type that is not copyable:

struct non_copyable {
    non_copyable() = default;
    non_copyable(const non_copyable&) = delete; // No copy
};
        

If we try to create an empty std::vector with non_copyable, it might seemingly work:

std::vector<non_copyable> v;        

However, once you try to actually work with the std::vector instance, you may get verbose error messages. Let me write a vectorize function template takes a single value of any type and returns a std::vector containing that value as its sole element. If I try to call this vectorize function template with my non_copyable type, I get in trouble:

template <typename type>
std::vector<type> vectorize(type&& t) {
    return {t};
}


void g() {
    non_copyable m;
    // works:
    std::vector<non_copyable> v;
    // fails:
    vectorize(m);
}        

It does not work simply because the compiler tries to copy a non_copyable instance. It is not difficult error. Yet the compiler error message can be epic. For example, you might get the following error:

include/c++/16.0.0//bits/allocator.h:133:30: note: in instantiation of template class 'std::__new_allocator' requested here
      |     class allocator : public __allocator_base<_Tp>
      |                              ^
include/c++/16.0.0/ext/alloc_traits.h:46:47: note: in instantiation of template class 'std::allocator' requested here
      | template
      |                                               ^
include/c++/16.0.0/bits/stl_vector.h:93:35: note: in instantiation of default argument for '__alloc_traits<std::allocator>' required here
      |       typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
      |                                   ^~~~~~~~~~~~~~~~~~~~~~
include/c++/16.0.0/bits/stl_vector.h:458:30: note: in instantiation of template class 'std::_Vector_base>' requested here
      |     class vector : protected _Vector_base<_Tp, _Alloc>
      |                              ^
:50:5: note: in instantiation of template class 'std::vector' requested here
      |     vectorize(m);
        

C++ concepts, introduced in C++20, are a compile-time mechanism for defining and enforcing constraints on template parameters. Using the concept keyword, developers can specify requirements that a type must satisfy to be used with a template, such as having certain operations, member functions, or inheriting from a specific base class. Concepts improve error messages by catching type mismatches early and make code more expressive by documenting intent. They can be applied directly in template declarations (e.g., template).

In our case, we can define a type that can be used in an std::vector instance: we require the type to be destructible, copyable, and default constructible.

template <typename T>
concept vector_element = requires(T a, T b) {
    // Must be destructible
    requires std::destructible<T>;
    
    // Must be copy constructible
    requires std::copy_constructible<T>;
    
    // Must be copy assignable
    requires std::assignable_from<T&, T>;
    
    // Must be default constructible (for operations like resize)
    requires std::default_initializable<T>;
};        

Concepts in C++20 are general. For example, if I wanted to require that an instance can be compared with itself, I might define the following concept:

template<typename T>
concept equality_comparable = requires(T a, T b) {
    { a == b } -> std::convertible_to<bool>;
};        

You can similarly define a concept for a type like std::vector that supports a ‘push_back’ method.

template<typename T>
concept pushable = requires(T a, typename T::value_type val) {
    { a.push_back(val) }; 
};        

In any case, let me now write a new vectorize function with my newly defined vector_element concept:

template <vector_element type>
std::vector<type> safe_vectorize(type&& t) {
    return {t};
}        

And this time, you are going to get a much better error message when writing the following erroneous code:

void g() {
   non_copyable m;
   safe_vectorize(m);
}        

For example, you may get the following error:

 error: no matching function for call to 'safe_vectorize'
 note: candidate template ignored: constraints not satisfied [with type = non_copyable &]
 note: because 'non_copyable &' does not satisfy 'vector_element'
        

C++ templates, while powerful for enabling generic and reusable code, often lead to complex and verbose error messages, particularly when misused, as seen with the std::vector and non_copyable example. The introduction of C++20 concepts allows developers to enforce type constraints explicitly, resulting in clearer, more concise error diagnostics. By using concepts like vector_element, programmers can catch errors early and improve code readability.

The adoption of C++20 has been excellent, both by compiler vendors and by major users. If you can, I recommend you take C++ concepts out for a spin.

Concepts are really what was missing for generic programming to reach its full potential and become easier to use.

To view or add a comment, sign in

More articles by Daniel Lemire

  • House prices and fertility

    No, rising house prices are not the driver of sharp fertility declines. The evidence shows only modest, mixed effects…

  • You can beat the binary search

    We sometimes have to look for a value in a sorted array. The simplest algorithm consists in just going through the…

    7 Comments
  • The fastest way to match characters on ARM processors?

    Consider the following problem. Given a string, you must match all of the ASCII white-space characters (\t, \n, \r, and…

    1 Comment
  • A brief history of C/C++ programming languages

    Initially, we had languages like Fortran (1957), Pascal (1970), and C (1972). Fortran was designed for number crunching…

    10 Comments
  • Can your AI rewrite your code in assembly?

    Suppose you have several strings and you want to count the number of instances of the character ! in your strings. In…

    3 Comments
  • A Fast Immutable Map in Go

    Consider the following problem. You have a large set of strings, maybe millions.

    4 Comments
  • Prefix sums at tens of gigabytes per second with ARM NEON

    Suppose that you have a record of your sales per day. You might want to get a running record where, for each day, you…

    2 Comments
  • You can use newline characters in URLs

    We locate web content using special addresses called URLs. We are all familiar with addresses like https://google.

  • How fast do browsers correct UTF-16 strings?

    JavaScript represents strings using Unicode, like most programming languages today. Each character in a JavaScript…

    1 Comment
  • How bad can Python stop-the-world pauses get?

    When programming, we need to allocate memory, and then deallocate it. If you program in C, you get used to malloc/free…

    3 Comments

Others also viewed

Explore content categories