C++ - Back to Basics: C++ Concepts

12 minute read

Jeff Garland - Back to Basics: C++ Concepts - CppCon 2025

In this session from CppCon 2025, Jeff Garland presents a practitioner’s guide to C++20 Concepts. The talk covers what concepts are, how to use them in code, reading and writing requires expressions and clauses, and designing with concepts.

Concept Basics

Why Do We Want Concepts?

  • Write good generic libraries that are fast
  • Get reasonable error messages
  • Create code that programmers can understand and maintain
  • Build better interfaces that are more descriptive with better dependency management

What is a C++ Concept?

Concepts are boolean predicate compositions that:

  • Support complex compile-time logic composition (conjunction and disjunction)
  • Classify types into sets that share syntax/semantics
  • Only check syntax (though semantics is the desired goal)

Types vs Concepts

Type Concept
Describes operations that can be performed Describes how a type can be used
Relationships with other types (base class, dependent type) Operations it can perform
Describes a memory layout Relationships with other types

Simple One Parameter Concept: printable

1
2
3
4
5
6
7
8
namespace io {
// Type T has print( std::ostream& ) const member function
template<class T>
concept printable = requires(std::ostream& os, T v)
{
    v.print( os ); // An expression that if compiles yields true
};
}

Notable Concept Properties

  • Evaluated completely at compile time (no runtime footprint)
  • Compatible with high performance code
  • Can be scoped in namespaces
  • Bridge between ‘pure auto’ and a specific type
  • Core use case is constraining templates

Using Concepts in Code

What Concepts Can Do

  • Constrain an overload set
  • Initialize a variable with <concept_name> auto
  • Conditional compilation with constexpr if
  • Use a pointer or unique_ptr of concept
  • Partially specialize a template with concept
  • Make template code into ‘regular code’

What Concepts Cannot Do

  • Cannot inherit from concept
  • Cannot constrain a concrete type using requires
  • Cannot ‘allocate’ via new
  • Cannot apply requires to virtual function

Where Can We Write <concept_name> auto?

Where a type name might otherwise appear:

  • Variable declaration
  • Function parameter
  • Function return type
  • Class template member (if template argument)

But not:

  • Class member
  • Base class
  • Template parameter and aliases (no auto)

Concept Usage: Basic Examples

1
2
3
4
5
6
7
8
9
10
11
template<class T>
concept printable = requires(std::ostream& os, T v)
{
    v.print( os ); // An expression that if compiles yields true
};

void f(printable auto s) { /* ... */ }

int main() {
    printable auto s = init_something();  // Must be initialized!
}

Note: printable auto s; without initialization is a compile error.

Concept Usage: Function Parameter or Return Value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<printable T>
printable auto
print( const T& s )
{
    //...
    return s;
}

printable auto
print2( const printable auto& s )
{
    //...
    return s;
}

godbolt

Unconstrained Auto for Function Parameter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// auto parameter -- this is a template function!
// template<typename T>
// void print_ln( T p )
void print_ln( auto p )
{
    std::cout << p << "\n";
}

class my_type {};

int main()
{
    print_ln( "foo" );
    print_ln( 100 );
    my_type m;
    print_ln( m );  // Compile error - no operator<< for my_type
}

godbolt

Concrete Overload for my_type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void print_ln( auto p )
{
    std::cout << p << "\n";
}

// Selected ahead of print_ln(auto) because better match
void print_ln( my_type p )
{
    p.print( std::cout );
    std::cout << "\n";
}

int main()
{
    print_ln( "foo" );
    print_ln( 100 );
    my_type m;
    print_ln( m );  // Now works!
}

godbolt

Concepts printable and output_streamable

1
2
3
4
5
6
7
8
9
10
11
12
// Type T has print( std::ostream& ) member function
template<typename T>
concept printable = requires(std::ostream& os, T v)
{
    v.print( os );
};

template<typename T>
concept output_streamable = requires(std::ostream& os, T v)
{
    os << v;
};

godbolt

A Type Satisfying printable

1
2
3
4
5
6
7
8
9
10
11
12
class my_type
{
    int i = 1;
    std::string s = "foo\n";
public:  // Concept not satisfied if print isn't public!
    void print( std::ostream& os ) const
    {
        os << "i: " << i << " s: " << s;
    }
};

static_assert( printable<my_type> );

godbolt

Constrained Overload for print_ln

1
2
3
4
5
6
7
8
9
10
11
void print_ln( auto p )
{
    std::cout << p << "\n";
}

// Constrained resolution
void print_ln( printable auto p )
{
    p.print( std::cout );
    std::cout << "\n";
}

godbolt

Overloaded Functions with Concepts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Example of overload resolution
void print_ln( output_streamable auto p )
{
    std::cout << p << "\n";
}

void print_ln( printable auto p )
{
    p.print( std::cout );
    std::cout << "\n";
}

int main()
{
    print_ln( "foo" );   // Uses output_streamable version
    my_type m;
    print_ln( m );       // Uses printable version
}

godbolt

Pointers and Concepts

Useful for factory functions:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
    const printable auto* m = new my_type();
    m->print(std::cout);

    const std::unique_ptr<printable auto> upm = std::make_unique<my_type>();
    upm->print(std::cout);
}
// Output:
// s: foo
// s: foo

godbolt

Pointer to Concept - Compile Error

1
2
3
4
5
6
7
8
9
10
class whatever {};  // No print method

int main()
{
    printable auto* m = new whatever();  // Compile error!
}

// Error: deduced initializer does not satisfy placeholder constraints
// Note: constraints not satisfied
// Note: the required expression 'v.print(os)' is invalid

Using if constexpr with Concepts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class T>
std::ostream&
print_ln( std::ostream& os, const T& v )
{
    if constexpr ( printable<T> )
    {
        v.print(os);
    }
    else {
        os << v;
    }
    os << "\n";
    return os;
}

int main()
{
    my_type m;
    print_ln( std::cout, m );  // Uses print()
    int i = 100;
    print_ln( std::cout, i );  // Uses operator<<
}

godbolt

if constexpr with Inline Requires Expression

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>
std::ostream&
print_ln( std::ostream& os, const T& v )
{
    if constexpr ( requires{ v.print( os ); } )
    {
        v.print(os);
    }
    else {
        os << v;
    }
    os << "\n";
    return os;
}

godbolt

Non-Template Member Function of Template Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
class wrapper {
    T val_;
public:
    wrapper(T val) : val_(val) {}

    T operator*() requires std::is_pointer_v<T>  // Type trait constraint
    {
        return val_;
    }
};

int main()
{
    int i = 1;
    wrapper<int*> wi{&i};
    std::cout << *wi << std::endl;  // OK

    // wrapper<int> wi2{i};
    // std::cout << *wi2 << std::endl;  // Error: no match for operator*
}

Template Alias with Concept Shorthand

1
2
3
4
5
6
7
8
9
10
11
12
13
// Template alias using concepts
// All elements must satisfy concept printable
template<printable T>
using vec_of_printable = std::vector<T>;

int main()
{
    vec_of_printable<my_type> vp{ {}, {} };
    for ( const auto& e : vp )
    {
        e.print(std::cout);
    }
}

Template Alias Error Message

1
2
3
4
5
6
vec_of_printable<int> vp;  // Compile error!

// Error: template constraint failure for
//   'template<class T> requires printable<T> using vec_of_printable = std::vector<T>'
// Note: constraints not satisfied
// Note: the required expression 'v.print(os)' is invalid

Standard Library Concepts

Standard concepts are found in headers <concepts>, <type_traits>, <iterator>, and <ranges>.

Numeric Concepts

Concept Description
floating_point<T> float, double, long double
integral<T> char, int, unsigned int, bool
signed_integral<T> char, int
unsigned_integral<T> char, unsigned

Comparison Concepts

Concept Description
equality_comparable<T> operator== is an equivalence
equality_comparable_with<T,U> cross-type equality
totally_ordered<T> ==, !=, <, >, <=, >= are a total ordering
totally_ordered_with<T,U> cross-type total ordering

Object Relation Concepts

Concept Description
same_as<T,U> types are same
derived_from<T,U> T is subclass of U
convertible_to<T,U> T converts to U
assignable_from<T,U> T can assign from U

Object Construction Concepts

Concept Description
default_initializable<T> default construction provided
constructible_from<T,...> T can construct from variable pack
move_constructible<T> support move
copy_constructible<T> support move and copy

Regular and Semi-Regular Concepts

Concept Description
semiregular<T> copy, move, destruct, default construct
regular<T> semiregular and equality comparable

See Sean Parent talks on why regular is so useful. TL;DR - type corresponds to usual expectations (like int).

Enforcing Regularity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <string>
#include <concepts>

class my_type
{
    std::string s = "foo\n";
public:
    void print( std::ostream& os ) const
    {
        os << "s: " << s;
    }
};

static_assert( std::regular<my_type> );  // Fails!

Enforcing Regular Error

1
2
3
error: static assertion failed
note: constraints not satisfied
note: required for the satisfaction of 'equality_comparable<_Tp>' [with _Tp = my_type]

Enforcing Regular Fix

1
2
3
4
5
6
7
8
9
10
11
12
13
class my_type
{
    std::string s = "foo\n";
public:
    void print( std::ostream& os ) const
    {
        os << "s: " << s;
    }
    // Added this line
    bool operator==( const my_type& ) const = default;
};

static_assert( std::regular<my_type> );  // Now passes!

Using Range Concepts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void print_ints( const std::ranges::range auto& R )
{
    for ( auto i : R )
    {
        std::cout << i << std::endl;
    }
}

// This function works on all the types below
int main()
{
    std::vector<int> vi = { 1, 2, 3, 4, 5 };
    print_ints( vi );

    std::array<int, 5> ai = { 1, 2, 3, 4, 5 };
    print_ints( ai );

    std::span<int> si2( ai );
    print_ints( si2 );

    int cai[] = { 1, 2, 3, 4, 5 };
    std::span<int> si3( cai );
    print_ints( si3 );

    std::ranges::iota_view iv{1, 6};
    print_ints( iv );
}

godbolt

Reading & Writing Concepts

Requires Expression vs Requires Clause

  • Clause: A boolean expression used after template and method declarations. Clauses can contain expressions.
  • Expression: Syntax for describing type constraints.

Requires Expression Basics

1
2
3
4
5
6
requires { requirement-sequence }
requires ( ..parameters.. ) { requirement-sequence }

// Simplest possible boolean example - no parameters
template<typename T>
concept always = true;

Requires Expression: Realistic Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Type T has print( std::ostream& ) member function
template<typename T>
concept printable = requires(std::ostream& os, T v)
{
    // This is a conjunction (AND) --> all must be true
    v.print( os );           // Member function
    format( v );             // Free function
    std::movable<T>;         // Another concept
    typename T::format;      // Nested type requirement
};

template<class T>
concept output_streamable = requires(std::ostream& os, T v)
{
    // Compound requirement: constraint on return type
    { os << v } -> std::same_as<std::ostream&>;
};

Constraint Composition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Disjunction (OR)
template<typename T>
concept printable_or_streamable =
    printable<T> || output_streamable<T>;

// Same as above using 'or' keyword
template<typename T>
concept printable_or_streamable2 =
    printable<T> or output_streamable<T>;

// Conjunction (AND)
template<typename T>
concept fully_outputable =
    printable<T> and output_streamable<T>;

More Constraint Composition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>
concept printable = requires(std::ostream& os, T v)
{
    v.print( os );
    std::movable<T>;
    typename T::format;
};

// Equivalent formulation
template<typename T>
concept printable2 =
    std::movable<T> and
    requires(std::ostream& os, T v)
    {
        v.print( os );
        typename T::format;
    };

Standard Library Example: derived_from

1
2
3
4
template<class Derived, class Base>
concept derived_from =
    std::is_base_of_v<Base, Derived> and
    std::is_convertible_v<const volatile Derived*, const volatile Base*>;

Example Concept: number

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <concepts>

template<typename T, typename U>
concept not_same_as = not std::is_same_v<T, U>;

static_assert( not_same_as<int, double> );

template<typename T>
concept number =
    not_same_as<bool, T> and
    not_same_as<char, T> and
    std::is_arithmetic_v<T>;

static_assert( number<int> );
static_assert( number<double> );
static_assert( !number<bool> );

Note: is_arithmetic is a trait that includes char and bool, which we often don’t want in a “number” concept.

Ranges and Concepts

1
2
3
4
5
6
7
std::vector<int> vi{ 0, 1, 2, 3, 4, 5, 6 };
auto is_even = [](int i) { return 0 == i % 2; };

for (int i : std::ranges::filter_view( vi, is_even ))
{
    std::cout << i << " ";  // Output: 0 2 4 6
}

std::ranges::filter_view Declaration

1
2
3
4
template<std::ranges::input_range V,
         std::indirect_unary_predicate<std::ranges::iterator_t<V>> Pred>
    requires std::ranges::view<V> && std::is_object_v<Pred>
class filter_view : public view_interface<filter_view<V, Pred>> { /* ... */ };

std::ranges::view_interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class D>
    requires std::is_class_v<D> && std::same_as<D, std::remove_cv_t<D>>
class view_interface
{
    constexpr const D& derived() const noexcept
    {
        return static_cast<const D&>(*this);
    }

    // Concept-based specialization of operator[]
    // Only applies if subclass is random_access_range
    template<std::ranges::random_access_range R = const D>
    constexpr decltype(auto) operator[](std::ranges::range_difference_t<R> n) const
    {
        return std::ranges::begin(derived())[n];
    }
};

Designing with Concepts

Concepts and Dependencies

  • Move dependency from a concrete type to an abstraction
  • Simple to test that a type models a concept
  • Trade-offs:
    • Type may evolve to no longer model concept (working code fails)
    • Concept may evolve so type no longer models it (working code fails)

Code Readability and Evolution

1
2
3
auto result = some_function();            // Return type unknown, flexible
int result = some_function();             // Return type obvious, brittle
time_duration auto result = some_function();  // Flexible + clear

Type, auto, or <concept_name> auto

Approach Characteristics
Concrete type Only one type can be returned; inflexible if return type evolves
auto We don’t care/know the type; still need recompile on change
<concept_name> auto A set of types; good sweet spot; allows bounded evolution

Breaking the Grip of Type Dependency

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// This function works on all the types below
void print_ints( const std::ranges::range auto& R )
{
    for ( auto i : R )
    {
        std::cout << i << std::endl;
    }
}

int main()
{
    std::vector<int> vi = { 1, 2, 3, 4, 5 };
    print_ints( vi );

    std::array<int, 5> ai = { 1, 2, 3, 4, 5 };
    print_ints( ai );

    std::span<int> si2( ai );
    print_ints( si2 );

    int cai[] = { 1, 2, 3, 4, 5 };
    std::span<int> si3( cai );
    print_ints( si3 );

    std::ranges::iota_view iv{1, 6};
    print_ints( iv );
}

godbolt

Final Thoughts

  • Concepts are a powerful tool in C++20
  • Practical and easy to use
  • Alter designs in important ways
  • Integrated into many aspects of the standard library

Additional Resources