Ir al contenido

Concepts y Constraints de C++20

Define requisitos precisos sobre los argumentos de template usando concepts. Escribe codigo generico mas limpio con clausulas requires, abbreviated function templates, y concepts de la biblioteca estandar.

El Problema Que Resuelven los Concepts

Antes de los concepts, los mensajes de error de templates eran cripticos — usar std::sort en un tipo sin operator< podia producir paginas de texto de error profundamente dentro de la implementacion de la biblioteca estandar. Los concepts resuelven esto permitiendote declarar requisitos por adelantado sobre los parametros de template.

Un concept es un predicado booleano con nombre evaluado en tiempo de compilacion. Verifica si un tipo satisface un conjunto de requisitos sintacticos y semanticos. Cuando un concept no se satisface, el compilador produce un mensaje de error claro y conciso senalando el requisito no satisfecho — no un muro de trazas de instanciacion de templates.

Los concepts fueron introducidos en C++20 y representan la mejora mas significativa a la programacion generica desde los propios templates.

Definiendo Concepts

Un concept se define con la palabra clave concept seguida de una expresion requires que especifica que operaciones debe soportar un tipo. La expresion requires puede verificar expresiones validas, tipos de retorno, tipos anidados, y mas.

concept_definition.cpp
#include <concepts>
#include <iostream>
#include <string>

// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;  // a+b must return something convertible to T
    { a == b } -> std::convertible_to<bool>; // must be comparable
    requires std::copy_constructible<T>;     // nested requirement
};

// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
    { t.size() } -> std::convertible_to<std::size_t>;
    { t.empty() } -> std::convertible_to<bool>;
};

// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
    return a + b + c;
}

template <Sizable Container>
void report_size(const Container& c) {
    std::cout << "Size: " << c.size()
              << (c.empty() ? " (empty)" : "") << '\n';
}

int main() {
    std::cout << sum_three(1, 2, 3)       << '\n'; // 6
    std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0

    std::string s = "hello";
    report_size(s);  // Size: 5
    std::vector<int> v{1, 2, 3};
    report_size(v);  // Size: 3
}

Clausulas Requires y Sintaxis Abreviada

Hay multiples formas de aplicar un concept a un template. La clausula requires va despues de la lista de parametros de template o despues de la firma de la funcion. La sintaxis de abbreviated function template usa Concept auto como tipo de parametro, que es la forma mas concisa.

requires_clause.cpp
#include <concepts>
#include <iostream>

// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        T temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// Method 2: requires clause after template parameters
template <typename T>
    requires std::integral<T>
T lcm(T a, T b) {
    return (a / gcd(a, b)) * b;
}

// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
    return value < 0 ? -value : value;
}

// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
    std::cout << "Integral: " << value << '\n';
}

// Concept auto works for return types too (C++20)
std::integral auto compute() {
    return 42;  // must return an integral type
}

int main() {
    std::cout << gcd(12, 8)       << '\n'; // 4
    std::cout << lcm(12, 8)       << '\n'; // 24
    std::cout << absolute(-42)    << '\n'; // 42
    print_integral(100);                    // Integral: 100
    // print_integral(3.14);  // ERROR: constraint not satisfied
}

Composicion de Concepts y Concepts Estandar

Los concepts se componen naturalmente con && (conjuncion) y || (disyuncion). La biblioteca estandar proporciona un conjunto rico de concepts en :

Concepts del lenguaje base:
- std::same_as — T y U son el mismo tipo
- std::derived_from — Derived deriva de Base
- std::convertible_to — From es implicitamente convertible a To
- std::integral, std::floating_point, std::signed_integral

Concepts de comparacion:
- std::equality_comparable — soporta == y !=
- std::totally_ordered — soporta <, >, <=, >=, ==, !=

Concepts de objeto:
- std::movable, std::copyable, std::semiregular, std::regular

Concepts de invocable:
- std::invocable — F puede invocarse con Args
- std::predicate — F es un predicado que devuelve bool

Concepts de iterador ():
- std::input_iterator, std::forward_iterator, std::random_access_iterator, std::contiguous_iterator

Concepts de rango ():
- std::ranges::range, std::ranges::sized_range, std::ranges::random_access_range

Subsuncion de Concepts y Resolucion de Sobrecarga

Cuando multiples sobrecargas con constraints coinciden, el compilador usa la subsuncion de concepts para elegir la mas especifica. El concept A subsume al concept B si las restricciones de A implican logicamente las restricciones de B. Esto permite el "refinamiento" — un concept mas especifico gana sobre uno mas general.

subsumption.cpp
#include <concepts>
#include <iostream>

// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
    { os << t };
};

template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;

// Less constrained overload
template <Printable T>
void display(const T& value) {
    std::cout << "[generic] " << value << '\n';
}

// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
    std::cout << "[number] " << value << '\n';
}

int main() {
    display(42);              // [number] 42 — PrintableNumber is more specific
    display(std::string("hi")); // [generic] hi — only Printable matches
    display(3.14);            // [generic] 3.14 — not integral
}

El Patron requires requires

El algo inusual requires requires aparece cuando usas una expresion requires ad-hoc directamente en una clausula requires sin definir un concept con nombre. El primer requires introduce la clausula de restriccion; el segundo requires inicia la expresion requires.

requires_requires.cpp
#include <iostream>
#include <string>

// Ad-hoc constraint without naming a concept
template <typename T>
    requires requires(T a, T b) {
        { a + b } -> std::same_as<T>;
    }
T add(T a, T b) {
    return a + b;
}

// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template <AddableToSelf T>
T add_named(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2)                << '\n'; // 3
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
    std::cout << add_named(10, 20)        << '\n'; // 30
}
Buena práctica

Al disenar concepts:

- Nombra los concepts por lo que el tipo ES, no por lo que la funcion necesita — usa Sortable no HasLessThan. Los concepts deben capturar categorias semanticas.
- Prefiere los concepts de la biblioteca estandar — antes de escribir los tuyos, verifica si , o ya tiene lo que necesitas.
- Manten los concepts pequenos y componibles — combina concepts pequenos con && en lugar de crear requisitos monoliticos.
- Usa concepts con nombre sobre requires requires ad-hoc — la forma con nombre es mas legible y reutilizable.
- Documenta los requisitos semanticos en comentarios — los concepts verifican sintaxis, no semantica. Si requieres que operator+ sea conmutativo, dilo en un comentario.

Puntos clave
  • Los concepts son predicados booleanos con nombre que restringen parametros de template, proporcionando mensajes de error claros
  • La expresion requires verifica expresiones validas, tipos de retorno y requisitos anidados
  • Cuatro formas de aplicar concepts: restriccion en parametro de template, clausula requires, requires trailing, o Concept auto
  • Los concepts se componen con && y ||; la subsuncion selecciona la sobrecarga mas restringida
  • La biblioteca estandar proporciona concepts en , y
  • requires requires es una restriccion inline ad-hoc — prefiere concepts con nombre para legibilidad

Quiz — Pon a prueba tus conocimientos

(20 XP)

1. Que significa `void print(std::integral auto value)`?

2. Si el concept A se define como `concept A = B<T> && C<T>`, que sucede cuando tanto una sobrecarga restringida por B como una restringida por A coinciden?

3. Por que `requires requires(T a) { a.size(); }` tiene la palabra clave 'requires' dos veces?