EduC++ Advanced C++20 Concepts

Advanced C++20 Concepts

WHY THIS FILE: Builds on concepts_intro.cpp (in 06_templates/concepts/). Covers the deeper mechanics of concepts: requires-expressions for testing arbitrary compile-time requirements, concept composition via && / ||, and *subsumption* -- the rule the compiler uses to pick the "more constrained" overload.

Prerequisites: Function/class templates, concepts_intro.cpp, SFINAE (to appreciate why concepts are better)
Standard: C++20 (header <concepts>)

1. requires-expression: test complex requirements

Checks if expressions are valid, their types, etc.
template<typename T>
concept Printable = requires(T t, std::ostream& os) {
    { os << t } -> std::same_as<std::ostream&>;  // Must support <<
};

template<typename T>
concept Hashable = requires(T t) {
    { std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};

2. Compound requirements with nested requires

template<typename T>
concept Serializable = requires(T t) {
    // Simple requirement: expression must be valid
    t.serialize();

    // Compound requirement: check return type
    { t.serialize() } -> std::convertible_to<std::string>;

    // Nested requirement: additional constraint
    requires std::is_default_constructible_v<T>;
};

// Type that satisfies Serializable
struct Config {
    std::string name;
    int value;

    std::string serialize() const {
        return std::format("{}={}", name, value);
    }
};

static_assert(Serializable<Config>);

3. Concept composition (combining concepts)

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<typename T>
concept OrderedNumeric = Numeric<T> && std::totally_ordered<T>;

template<typename T>
concept PrintableNumeric = Numeric<T> && Printable<T>;

// Use composed concept
template<PrintableNumeric T>
void print_numeric(T value) {
    std::cout << std::format("Value: {}\n", value);
}

4. Concept subsumption (overload resolution)

More constrained overloads are preferred.

   one concept directly includes another via
   conjunction (&&).  Logically equivalent but
   structurally different concepts are ambiguous
   to the compiler.

Watch out: concept subsumption only works when

template<typename T>
concept Animal = requires(T t) {
    { t.name() } -> std::convertible_to<std::string>;
};

// More constrained: Animal that also speaks
template<typename T>
concept SpeakingAnimal = Animal<T> && requires(T t) {
    { t.speak() } -> std::convertible_to<std::string>;
};

// Less constrained overload
void describe(const Animal auto& a) {
    std::cout << std::format("Animal: {}\n", a.name());
}

// More constrained overload -- preferred when both match
void describe(const SpeakingAnimal auto& a) {
    std::cout << std::format("{} says: {}\n", a.name(), a.speak());
}

struct Dog {
    std::string name() const { return "Dog"; }
    std::string speak() const { return "Woof!"; }
};

struct Rock {  // Has name() but no speak()
    std::string name() const { return "Rock"; }
};

5. Real-world pattern: constrained factory

Only allows creating objects that are both
   default-constructible and printable.
template<typename T>
concept Creatable = std::default_initializable<T> && requires(T t) {
    { std::format("{}", t.to_string()) };
};

template<typename T>
    requires std::default_initializable<T>
std::unique_ptr<T> make() {
    return std::make_unique<T>();
}

6. Abbreviated function templates (terse syntax)

'auto' with concepts is the cleanest form.
// All three are equivalent:
// (a) template<std::integral T> T double_it(T x) { return x * 2; }
// (b) auto double_it(std::integral auto x) { return x * 2; }
// (c) template<typename T> requires std::integral<T> T double_it(T x) { ... }

auto double_it(std::integral auto x) { return x * 2; }

// Multiple constrained auto parameters
auto safe_divide(std::floating_point auto a, std::floating_point auto b) {
    if (b == 0.0) return 0.0;
    return static_cast<double>(a) / static_cast<double>(b);
}

int main() {
    // Printable concept
    static_assert(Printable<int>);
    static_assert(Printable<std::string>);

    // Serializable
    Config cfg{"timeout", 30};
    std::cout << std::format("Serialized: {}\n", cfg.serialize());

    // Composed concepts
    print_numeric(42);
    print_numeric(3.14);
    // print_numeric("hello");  // Error: not Numeric

    // Concept subsumption: compiler picks the more constrained overload
    std::cout << "\n--- Subsumption ---\n";
    Dog dog;
    describe(dog);  // Calls SpeakingAnimal overload (more constrained)

    // Abbreviated syntax
    std::cout << "\n--- Terse Syntax ---\n";
    std::cout << std::format("double_it(21) = {}\n", double_it(21));
    std::cout << std::format("safe_divide(10.0, 3.0) = {:.4f}\n",
                              safe_divide(10.0, 3.0));

    return 0;
}