C# where constraints enable our generics to do a lot more. C++ also has constraints and they enable us to write more expressive and efficient code. Today we’ll see how to add some constraints to our templates to achieve these goals.

Table of Contents

Constraints

C# has 11 specific where constraints we can put on type parameters to generics. These include constraints like where T : new() indicating that T has a public constructor that takes no parameters. In contrast, C++ provides us with tools to build our own constraints out of compile-time expressions.

So far in C++, all of our templates have had no constraints. Still, we’ve been able to use those parameters in a great many ways. The difference between the two languages is that the default in C# is to treat generic parameters as the least common denominator type: System.Object/object. The default in C++ is the other end of the spectrum: template parameters may be used in any way that compiles.

Both languages allow us to set constraints to move the requirements for parameters more toward the opposite end of the spectrum. This means that C# where constraints make generics’ type parameters more specific and therefore allow us to use more specific functionality like calling a constructor with no parameters. We’re about to see how C++ template constraints make template parameters less specific to perform better overload resolution and give more programmer-friendly compiler error messages. All of this was added in C++20 and is becoming available in all the major compilers.

Requires Clauses

While C# uses the keyword where to add a constraint, C++ uses requires. Let’s jump right in and add one to a function template:

struct Vector2
{
    float X;
    float Y;
};
 
// Variable template
// "Default" value is false
template <typename T>
constexpr bool IsVector2 = false;
 
// Specialization of the variable template
// Change value to true for a specific type
template <>
constexpr bool IsVector2<Vector2> = true;
 
// Function template
template <typename TVector, typename TComponent>
// Requires clause
// Compile-time expression evaluates to a bool
// Can use template parameters here
requires IsVector2<TVector>
// The function
TComponent Dot(TVector a, TVector b)
{
    return a.X*b.X + a.Y*b.Y;
}
 
// OK
Vector2 vecA{2, 4};
Vector2 vecB{2, 4};
DebugLog(Dot<Vector2, float>(vecA, vecB));
 
// Compiler error:
//
// Candidate template ignored: constraints not satisfied
// [with TVector = int, TComponent = int]
// TComponent Dot(TVector a, TVector b)
//            ^
// test.cpp:60:10: note: because 'IsVector2<int>' evaluated to false
// requires IsVector2<TVector>
DebugLog(Dot<int, int>(2, 4));

Rather than a language-specified where constraint like we’d use in C#, we instead specify any compile-time expression after the keyword requires. In this case we’re using a variable template that we’ve defaulted to false and then specialized to opt-in the specific Vector2 type to true.

These compile-time expressions have access to the template parameters. Like other compile-time expressions, they’re allowed to be arbitrarily complex. For example, consider this variable template that doesn’t have a requires clause:

// Variable template
template <typename T, int N>
// Recurse to next-lower value
constexpr T SumUpToN = N + SumUpToN<T, N-1>;
 
// Specialization for 0 stop recursion
template <typename T>
constexpr T SumUpToN<T, 0> = 0;
 
// OK
DebugLog(SumUpToN<float, 3>); // 6
 
// Compile error:
// 
// test.cpp:44:28: fatal error: recursive template instantiation exceeded
// maximum depth of 1024
// constexpr T SumUpToN = N + SumUpToN<T, N-1>;
//                            ^
// test.cpp:44:28: note: in instantiation of variable template specialization
// 'SumUpToN<float, -1025>' requested here
// test.cpp:44:28: note: in instantiation of variable template specialization
// 'SumUpToN<float, -1024>' requested here
// test.cpp:44:28: note: in instantiation of variable template specialization
// 'SumUpToN<float, -1023>' requested here
// test.cpp:44:28: note: in instantiation of variable template specialization
// 'SumUpToN<float, -1022>' requested here
// test.cpp:44:28: note: in instantiation of variable template specialization
// 'SumUpToN<float, -1021>' requested here
// 
// ... many more lines of errors
DebugLog(SumUpToN<float, -1>);

To stop that infinite recursion, we can add a requires clause:

template <typename T, int N>
// Constraint to positive values only
requires (N >= 0)
constexpr T SumUpToN = N + SumUpToN<T, N-1>;
 
template <typename T>
constexpr T SumUpToN<T, 0> = 0;
 
// OK
DebugLog(SumUpToN<float, 3>); // 6
 
// Compiler error:
// 
// test.cpp:54:14: error: constraints not satisfied for variable template
// 'SumUpToN' [with T = float, N = -1]
//     DebugLog(SumUpToN<float, -1>);
//              ^~~~~~~~~~~~~~~~~~~
// test.cpp:42:11: note: because '-1 >= 0' (-1 >= 0) evaluated to false
// requires (N >= 0)
DebugLog(SumUpToN<float, -1>);

We’ve successfully stopped the infinite recursion before it started with a requires constraint. The compiler didn’t need to instantiate thousands of templates and it didn’t print out thousands of error messages for us to decipher. Instead, we simply get one readable message telling us that the constraint wasn’t satisfied.

Concepts

So far requires has been a pretty limited tool. That’s because this isn’t the primary way of using it. Instead, we usually use requires to define a “concept.” A concept is supposed to be a semantic description of a category of types. For example, we might define a Number concept as a type that supports various numeric operators:

template <typename T>
concept Number = requires(T t) {
    t + t;
    t - t;
    t * t;
    t / t;
    -t;
    +t;
    --t;
    ++t;
    t--;
    t++;
};

This is a new use of requires in a couple of ways. First, we’ve added a parameter list like a function has. This gives us a named parameter t with the type T matching the template parameter. Second, we then use that parameter in a series of statements, similar to what a function is. If those statements compile, the constraint is satisfied.

Finally, we save the constraint created by requires using the concept keyword. This gives us a way to name the constraint elsewhere when we want to use it. All concepts are templates taking type parameters since their purpose is to categorize types.

Now let’s put the concept to use:

// Function template with two type parameters (the latter is defaulted)
template <typename TVal, typename TThreshold=TVal>
// Requires clause names concepts
requires Number<TVal> && Number<TThreshold>
// The function
bool IsNearlyZero(TVal val, TThreshold threshold)
{
    return (val < 0 ? -val : val) < threshold;
}
 
// All of these are OK since double, float, and int satisfy Number
DebugLog(IsNearlyZero(0.0, 0.1)); // true
DebugLog(IsNearlyZero(0.2f, 0.1f)); // false
DebugLog(IsNearlyZero(2, 1)); // true
 
struct Player{};
 
// Compiler error: Player doesn't satisfy the Number constraint
DebugLog(IsNearlyZero(Player{}, Player{}));

Instead of directly evaluating to a bool, this use of requires names the Number concept and passes arguments to it: Number<TVal> and Number<TThreshold>. The requires clause can still perform binary logic using && and || operators to combine concepts or check bool values.

There are a few alternate syntaxes we can use. First, we can put the requires clause after the parameter list:

template <typename TVal, typename TThreshold=TVal>
bool IsNearlyZero(TVal val, TThreshold threshold)
    requires Number<TVal> && Number<TThreshold>
{
    return (val < 0 ? -val : val) < threshold;
}

If we have a trivial requires clause that simply names a single concept, which is quite typical, we can replace typename with the name of the concept:

// Trivial concept version (still using typename)
template <typename T>
bool IsNearlyZero(T val, T threshold)
    requires Number<T>
{
    return (val < 0 ? -val : val) < threshold;
}
 
// Replace typename with concept name
template <Number T>
bool IsNearlyZero(T val, T threshold)
{
    return (val < 0 ? -val : val) < threshold;
}

If we’re using auto parameters to create “abbreviated function templates,” then we won’t have the template. Instead, we can simply put the concept name before auto to require that the parameter satisfy that template:

bool IsNearlyZero(Number auto val, Number auto threshold)
{
    return (val < 0 ? -val : val) < threshold;
}

Regardless of which syntax we choose, the usage is always the same as the above because all of these are equivalent. The preference of syntax is mostly a matter of the level of flexibility we need and of style.

Because this form of requires defines a concept and a concept is what’s named after the other form of requires, we sometimes use requires requires to define an ad-hoc concept:

template <typename T>
// Requires clause names ad-hoc concept
requires requires(T t) {
    t + t;
    t - t;
    t * t;
    t / t;
    -t;
    +t;
    --t;
    ++t;
    t--;
    t++;
}
bool IsNearlyZero(T val, T threshold)
{
    return (val < 0 ? -val : val) < threshold;
}

Note that this Number concept is quite incomplete and for example purposes only. The C++ Standard Library has many well-designed concepts such as std::integral and std::floating_point that are suitable for production code.

Combining Concepts

Many concepts are defined in terms of other concepts. Just like how we used && in our requires clause, we can do the same when defining a concept:

// Define a concept in terms of another concept and an ad-hoc concept
template <typename T>
concept Integer = Number<T> && requires(T t) {
    t << t;
    t <<= t;
    t >> t;
    t >>= t;
    t % t;
};

We can also use concepts within the definition of our concepts. These are known as “nested concepts:”

template <typename T>
concept Vector2 = requires(T t) {
    // Use a concept from within a concept
    // This requires the type of t.X to satisfy the Number constraint
    Number<decltype(t.X)>;
 
    // Also require Y to be a Number
    Number<decltype(t.Y)>;
};
 
struct Vector2f
{
    float X;
    float Y;
};
 
bool IsOrthogonal(Vector2 auto a, Vector2 auto b)
{
    return (a.X*b.X + a.Y*b.Y) == 0;
}
 
Vector2f a{0, 1};
Vector2f b{1, 0};
Vector2f c{1, 1};
DebugLog(IsOrthogonal(a, b)); // true
DebugLog(IsOrthogonal(a, c)); // false

Alternately, we can use “compound requirements” to implicitly pass the type that an expression evaluates to as the first argument to a concept. Here’s how the Vector2 concept could have used this:

template <typename T>
concept Vector2 = requires(T t) {
    // t.X must evaluate to a type that satisfies the Number constraint
    {t.X} -> Number;
 
    {t.Y} -> Number;
};
Overload Resolution

We’ve seen how to use specialization to write custom versions of templates. This generally works well for particular types such as float, but it’s difficult to specialize for whole categories of types. This is where concepts come in. When calling an overloaded function, the compiler will look at the concepts to find the one that’s most constrained:

// Incomplete definition of a dynamic array class
struct List
{
    int Length;
    int* Array;
 
    int* GetBegin()
    {
        return Array;
    }
 
    int& operator[](int i)
    {
        return Array[i];
    }
};
 
// Incomplete definition of a linked list class
struct LinkedList
{
    struct Node
    {
        int Value;
        Node* Next;
    };
 
    Node* Head;
 
    Node* GetBegin()
    {
        return Head;
    }
};
 
// A concept that defines types that can be iterated
template <typename T>
concept Iterable = requires(T t) {
    t.GetBegin();
};
 
// A concept that defines types that can be indexed into
// This is more constrained than just Iterable
template <typename T>
concept Indexable = Iterable<T> && requires(T t) {
    t[0]; // Can read from an index
    t[0] = 0; // Can write to an index
};
 
// Indexable overload simply indexes: O(1)
int GetAtIndex(Indexable auto collection, int index)
{
    return collection[index];
}
 
// Iterable version has to walk the list: O(N)
int GetAtIndex(Iterable auto collection, int index)
{
    auto cur = collection.GetBegin();
    for (int i = 0; i < index; ++i)
    {
        cur = cur->Next;
    }
    return cur->Value;
}
 
// Overload resolution calls the Indexable version
List list;
int a = GetAtIndex(list, 1000);
 
// Overload resolution calls the Iterable version
LinkedList linkedList;
int b = GetAtIndex(linkedList, 1000);
C# Equivalency

Now that we know how constraints work in C++, let’s see how we’d approximate each of the 11 where constraints that C# offers. To do this, we’ll use some pre-defined concepts out of the C++ Standard Library’s <concepts> header and a variable template out of <type_traits> rather than writing our own versions of these.

C# Constraint C++ Concept (approximation)
where T : struct template <class T> concept C = std::is_class_v<T>;
where T : class template <class T> concept C1 = !Nullable<T> && std::is_class_v<T>
where T : class? template <class T> concept C = std::is_class_v<T>;
where T : notnull template <class T> concept C = !Nullable<T>;
where T : unmanaged N/A. All C++ types are unmanaged.
where T : new() std::default_initializable<T>
where T : BaseClass template <class T> concept C = !Nullable<T> && std::derived_from<T, BaseClass>;
where T : BaseClass? std::derived_from<T, BaseClass>
where T : Interface template <class T> concept C = !Nullable<T> && std::derived_from<T, BaseClass>;
where T : Interface? std::derived_from<T, BaseClass>
where T : U std::derived_from<T, U>

In the table above, std::derived_from and std::default_initializable are concepts and std::is_class_v is a bool variable template. Nullable isn't in the C++ Standard Library, but it might look like this:

template <class T> concept Nullable =
    // Can assign nullptr to it
    requires(T t) { t = nullptr; } &&
    // Has a user-defined conversion operator to nullptr or is a pointer
    (requires(T t) { t.operator decltype(nullptr)(); } || std::is_pointer_v<T>);

This, and some of the other concepts, are approximations of their C# equivalents. C++ doesn't have exact matches for C# "nullable contexts" and other subtle language differences. Feel free to adjust these concepts to suit your intended usage.

Conclusion

C++ constraints provided by requires and concept fill a similar role to C# constraints provided by where. As is often the case when comparing the two languages, the C++ version is essentially a superset of the C# functionality. While some concepts are provided by the C++ Standard Library via the <concepts> header, we're also given the tools to write our own concepts as we did above with Nullable and others.

The constraints we create allow us to limit what our templates are allowed to work on, express that intent to users of our templates, generate much more readable compiler errors, and even choose the most optimal overloaded function.

This is in contrast to C# constraints that enable our generics to use more functionality of their type parameters. Because only 11 basic constraints are provided with no ability for us to create our own constraints, we're often forced into trade-offs such as taking a performance hit due to calling functions on an interface, creating garbage due to boxing to a reference type, or jumping through hoops to write generic code.