# Modern C++ Metaprogramming: Concepts and Constraints

In C++20, the introduction of concepts and constraints revolutionizes how developers write generic code. Concepts provide a declarative way to specify template requirements, improving readability, error messages, and compile-time checks. This article explores the core ideas behind concepts, demonstrates common patterns, and shows how to use constraints effectively in real-world scenarios.

What Are Concepts?

A concept is a compile-time predicate that can be applied to a type or a set of types. It is expressed as a bool-valued expression that depends on template parameters. For example:

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

Here, Integral is satisfied only by integral types (int, long, etc.). Concepts can also combine multiple requirements:

template <typename T>
concept Incrementable = requires(T x) {
    { ++x } -> std::same_as<T&>;
    { x++ } -> std::same_as <T>;
};

The requires clause lists expressions that must be valid for T. The trailing -> specifies the expected return type of each expression.

Using Concepts in Function Templates

Previously, we would rely on static_assert or SFINAE tricks to restrict template parameters. Concepts allow us to place constraints directly in the template parameter list:

template <Incrementable T>
void increment_all(std::vector <T>& v) {
    for (auto& e : v) ++e;
}

If a type that does not satisfy Incrementable is passed, the compiler produces a clear diagnostic that the concept is not satisfied.

Example: A Generic Sort Function

template <typename RandomIt>
requires std::sortable <RandomIt>
void quick_sort(RandomIt first, RandomIt last) {
    if (first == last) return;
    auto pivot = *first;
    RandomIt left = first, right = last - 1;
    while (left < right) {
        while (*left <= pivot) ++left;
        while (*right > pivot) --right;
        if (left < right) std::iter_swap(left, right);
    }
    std::iter_swap(first, right);
    quick_sort(first, right);
    quick_sort(right + 1, last);
}

std::sortable is a standard concept that checks whether the iterator type can be sorted with operator<. If you try to call quick_sort with a container that doesn’t meet this requirement, the compiler will emit a concise error.

Constraints with requires Clauses

Constraints can also be applied to entire function bodies using requires expressions:

template <typename T>
void foo(T t)
    requires std::default_initializable <T> && std::copy_constructible<T>
{
    T a; // default-constructible
    T b = a; // copy-constructible
}

This syntax is useful when multiple templates or overloads share the same constraint logic but you want to keep the primary signature clean.

Practical Tips

  1. Start with Built-in Concepts
    C++20 provides a set of standard concepts in `

    `: `std::integral`, `std::floating_point`, `std::default_initializable`, `std::sortable`, etc. Use them before writing custom ones.
  2. Write Small, Reusable Concepts
    Break complex constraints into smaller concepts. For instance, separate Comparable and Swappable before combining them into a Sortable concept.

  3. Document Concepts Clearly
    When you create a concept, add comments describing its intent. IDEs can display these comments as tooltips, improving maintainability.

  4. Leverage requires Expressions for Overload Disambiguation
    Constraints can resolve ambiguity between overloaded templates by selecting the most constrained candidate.

  5. Test with Edge Cases
    Compile your code with types that intentionally fail constraints to ensure diagnostic messages are informative.

Example: A Generic Hash Table

Below is a simplified hash table that uses concepts to enforce requirements on the key type:

#include <vector>
#include <string>
#include <iostream>
#include <concepts>
#include <functional>

template <typename K>
concept Hashable = requires(const K& k, std::size_t h) {
    { std::hash <K>{}(k) } -> std::convertible_to<std::size_t>;
};

template <Hashable K, typename V>
class HashTable {
public:
    HashTable(std::size_t sz = 16) : table(sz) {}

    void insert(const K& key, const V& value) {
        std::size_t idx = std::hash <K>{}(key) % table.size();
        table[idx].push_back({key, value});
    }

    V* find(const K& key) {
        std::size_t idx = std::hash <K>{}(key) % table.size();
        for (auto& [k, v] : table[idx]) {
            if (k == key) return &v;
        }
        return nullptr;
    }

private:
    std::vector<std::vector<std::pair<K, V>>> table;
};

int main() {
    HashTable<std::string, int> ht;
    ht.insert("foo", 42);
    if (auto p = ht.find("foo")) std::cout << *p << '\n';
}

Because the Hashable concept restricts K to types that can be hashed, any attempt to instantiate HashTable with an unsupported key type results in a compile-time error with a clear message.

Conclusion

Concepts and constraints bring a new level of clarity and safety to generic C++ programming. By defining precise, readable requirements, developers can catch errors early, improve compiler diagnostics, and write more expressive code. Embrace these features early in your projects to reap the benefits of modern C++ metaprogramming.

发表评论