C++ Concepts and Requires Contracts

Use C++20 concepts and requires expressions to state template capabilities, reject unsupported calls clearly, and separate type checks from semantic rules.

Lesson 41 of 44Advanced10 min read

An unconstrained template can fail deep inside its body when a caller supplies an unsupported type. A constraint moves the stated capability toward the declaration, where the compiler can reject the candidate more clearly. Concepts are useful names for those requirements. They do not prove every runtime behavior of a type, so a syntactically valid operation can still violate a semantic contract.

This guide requires C++20. You will define a size capability, constrain a numeric helper and implement a reservable-object operation. The examples keep runtime validation in place. The aim is to express what the implementation needs, rather than attaching the most restrictive familiar concept to every generic function.

A concept names a compile-time predicate

A concept is a named constraint on template arguments. Standard concepts such as integral and same_as describe common requirements. A custom concept can combine them with Boolean operators or a requires expression. The resulting name should describe a useful capability or domain restriction rather than a particular implementation class.

Constraints participate in whether a template candidate is viable. A failed requirement normally removes that constrained candidate from consideration, allowing an appropriate alternative or producing a clearer rejection. This differs from waiting for an unsupported expression deep in the selected body. Keep public concepts stable and documented, because changing a constraint can affect which calls compile and which overload is selected.

Requires expressions check expressions without executing them

A requires expression can ask whether an expression is well-formed, whether a type exists, or whether an expression meets a return-type requirement. The local parameters used inside it describe hypothetical values for checking; they do not construct or execute a runtime operation.

A compound requirement with an arrow can apply a concept to the expression's type. Convertible_to permits a supported conversion, while same_as requires the specified type relationship. Choose deliberately. A result convertible to bool is broader than a result exactly bool, and the broader allowance can be inappropriate for a small protocol interface. A noexcept requirement can additionally test a nonthrowing expression, but still depends on the called operation's honest declaration.

Require the minimum capability the body uses

A size-reporting helper should not require random-access indexing, sorting or copying when it only calls size. Overconstraining reduces useful interoperability and ties the API to accidental implementation details. Underconstraining lets unsupported operations fail later.

A constraint cannot replace domain validation. An unsigned type can still hold an excessive count, and a perfectly valid addition can overflow. Standard integral concepts also include Boolean under relevant classifications, which may be unsuitable as a quantity type. Combine a concept with an explicit exclusion when the feature's meaning requires it. Think about both accepted types and accepted values before calling a template generic.

Constraint relationships influence overload selection

Several constrained overloads can be viable. The language has rules for ordering constraints, including subsumption, but a reader should not assume that two expressions with the same mathematical meaning are always interchangeable for those rules. Reuse named concepts consistently when building a hierarchy of capabilities.

Keep overload sets small enough that callers can predict the selected behavior. If an operation behaves differently only because one type happens to expose an unrelated member, the concept may encode the wrong responsibility. Prefer explicit policy arguments or separate names when different semantics are intended. Compile-time selection should clarify the interface, not hide a surprise behind a clever constraint.

Semantic requirements need evidence beyond syntax

An ordering concept can verify the presence of comparison expressions, but cannot automatically prove every value obeys the promised mathematical laws. Floating-point NaN behavior is a familiar reason to examine the domain of an ordering operation carefully. A comparator can compile while violating transitivity.

Document semantics in prose and test representative laws and boundary cases. Concepts make the type-level contract inspectable, while tests and implementation review support the behavioral contract. Avoid saying a concept proves thread safety, ownership, authorization or performance unless a separate explicit mechanism establishes those properties. A named capability is only as useful as the meaning its users share.

Diagnose constraints at the call boundary

When a call fails, inspect which requirement is unsatisfied before editing the implementation. A missing member, incorrect return type or const mismatch can explain the failure. A requires expression using const T checks an observer contract; using T& checks a mutable access path. Those choices should match the function body.

Use small static assertions to record supported and unsupported types when appropriate. Do not weaken a requirement merely to accept one accidental type if the body still cannot implement the operation safely. If the type's interface differs, provide an adapter or another deliberate overload. Keep the C++20 compilation flag visible so syntax failures are not confused with semantic constraint failures.

Official references

C++ Concepts and Requires Contracts Examples

Each example shows the code, the output it produces and what each part does.

Example 1

State a size capability and a count type

The complete program checks a size expression and implements bounded unsigned addition. It excludes bool from the count capability and checks overflow before computing.

HasSize verifies the expression and conversion, not that an arbitrary custom size method is cheap or semantically accurate. The numeric helper adds its own runtime safety rule.

C++
#include <concepts>
#include <cstddef>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <vector>

template<class T>
concept HasSize = requires(const T& value) {
    { value.size() } -> std::convertible_to<std::size_t>;
};

template<class T>
concept CountType = std::unsigned_integral<T> &&
                    (!std::same_as<T, bool>);

template<HasSize T>
std::size_t observed_size(const T& value) {
    return static_cast<std::size_t>(value.size());
}

template<CountType T>
T add_counts(T left, T right) {
    if (left > std::numeric_limits<T>::max() - right)
        throw std::overflow_error("count overflow");
    return left + right;
}

int main() {
    static_assert(HasSize<std::vector<int>>);
    static_assert(!HasSize<int>);
    static_assert(!CountType<bool>);
    std::vector<int> values{2, 4, 6};
    std::cout << "Size: " << observed_size(values) << '\n';
    std::cout << "Count: " << add_counts(3u, 4u) << '\n';
}
Output
Size: 3
Count: 7

C++ Concepts and Requires Contracts in Real Projects

Worked exercise: call a reservable object

The task is to require a mutable object with try_reserve(int) returning exactly bool. The helper rejects nonpositive requests, then delegates the reservation. The concrete stock class enforces nonnegative remaining stock. This complete solution separates the syntactic capability from the business invariant.

C++
#include <concepts>
#include <iostream>
#include <stdexcept>

template<class T>
concept Reservable = requires(T& value, int quantity) {
    { value.try_reserve(quantity) } -> std::same_as<bool>;
};

template<Reservable T>
bool request_units(T& value, int quantity) {
    if (quantity <= 0)
        throw std::invalid_argument("nonpositive quantity");
    return value.try_reserve(quantity);
}

class Stock {
    int quantity_;
public:
    explicit Stock(int quantity) : quantity_(quantity) {
        if (quantity < 0)
            throw std::invalid_argument("negative stock");
    }

    bool try_reserve(int requested) {
        if (requested <= 0)
            throw std::invalid_argument("nonpositive quantity");
        if (requested > quantity_)
            return false;
        quantity_ -= requested;
        return true;
    }

    int remaining() const noexcept { return quantity_; }
};

int main() {
    Stock stock(3);
    std::cout << std::boolalpha;
    std::cout << request_units(stock, 2) << '\n';
    std::cout << request_units(stock, 2) << '\n';
    std::cout << "Remaining: " << stock.remaining() << '\n';
}
Output
true
false
Remaining: 1

A class with the same method signature but incorrect subtraction could satisfy Reservable while violating the semantic rule. Tests of rejection and unchanged state remain essential.

C++ Concepts and Requires Contracts FAQ

Does a requires expression run the operation?

No. It checks selected expressions and types in the compile-time constraint context. Hypothetical local parameters do not perform a reservation or allocate a real object. The actual constrained function still runs normally when called. This separation allows a method's presence and result type to be checked without claiming that the method's runtime behavior is correct.

Is convertible_to the same as same_as?

No. Convertible_to permits a conversion satisfying its requirements, while same_as expresses the specified type relationship. A capability returning exactly bool is narrower than one whose result can convert to bool. Choose the requirement from the API's actual contract. Excessively broad conversion can admit surprising proxy types; excessive exactness can exclude useful implementations unnecessarily.

Can a concept prove stock never becomes negative?

Not from a method signature alone. The Reservable concept verifies that the required expression exists and returns bool. The concrete implementation and tests must establish the state invariant. Runtime quantities also need validation. Keep the semantic promise documented and test insufficient requests, invalid requests and successful updates independently from compile-time type acceptance.

Should every template use the strongest standard concept?

No. Require the capabilities the body actually uses and the semantics the feature needs. A size observer does not need sorting or random access. Overconstraining reduces reuse and can create unnecessary conversion work. Underconstraining defers errors into the body. A focused named concept with a clear prose contract is usually more helpful than a large unrelated set of requirements.

C++ Concepts and Requires Contracts Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A type satisfies Reservable but returns true after subtracting too much stock. What follows?

C++ Concepts and Requires Contracts Quiz

2 questions to check what you learned.

  1. What does a requires expression primarily check?

    • That a runtime operation has already succeeded.
    • That all values fit in the destination integer type.
    • That every accepted method is thread-safe.
    • Whether specified expressions and types satisfy compile-time requirements.
  2. Why does CountType exclude bool?

    • Boolean satisfies relevant integral classifications but is not an intended quantity representation.
    • Boolean can never satisfy a standard integral concept.
    • Unsigned classification alone proves the type represents the intended quantity domain.
    • A requires clause automatically rejects Boolean arguments without an explicit restriction.