La expression requires es el átomo fundamental de la metaprogramación de tipos en C++20. A diferencia de lo que su nombre sugiere, no es una sentencia que ejecute código, sino una expresión que evalúa si un conjunto de operaciones es sintácticamente válida y semánticamente aceptable para un tipo dado, devolviendo un bool en tiempo de compilación. Mientras que en C++17 dependíamos de la tortuosa técnica de SFINAE (Substitution Failure Is Not An Error) mediante std::enable_if, las expresiones requires permiten realizar introspección sobre la interfaz de un tipo de forma directa y legible.
El compilador implementa esto mediante un mecanismo de validación de la gramática: cuando evalúa una expresión requires, el compilador “simula” la llamada a los métodos o la aplicación de operadores. Si la expresión es válida para el tipo, la expresión devuelve true; si no, devuelve false sin necesidad de disparar un error de compilación inmediato (lo que permite que el motor de resolución de sobrecarga continúe buscando otras candidatas).
Debes usar expresiones requires simples cuando solo te interese la existencia de una sintaxis (por ejemplo, si un tipo tiene un método foo()). Debes escalar a expresiones requires compuestas cuando necesites validar la semántica: si el método devuelve un tipo específico, si el resultado es convertible a otro, o si la operación es noexcept. El uso de estas expresiones es el corazón de la definición de Concepts [C++20].
Si intentas usar una expresión requires dentro de una cláusula de restricción de forma incorrecta, o si confías en que una expresión será válida solo por su sintaxis sin verificar su contrato de excepciones (noexcept), podrías encontrarte con fallos de compilación crípticos en el punto de uso, lejos de donde definiste el error, o peor aún, errores de diseño donde tu código es sintácticamente correcto pero semánticamente inválido para el contrato que esperabas.
#include <iostream>
#include <concepts>
#include <vector>
#include <string>
#include <type_traits>
// Concepto simple: Solo verifica que el tipo tenga un método .size()
template<typename T>
concept HasSize = requires(T t) {
t.size();
};
// Concepto compuesto: Verifica sintaxis, tipo de retorno y especificación de excepción
template<typename T>
concept FastSizeable = requires(T t) {
// La expresión debe ser válida, devolver algo convertible a size_t
// y la llamada no debe poder lanzar excepciones.
{ t.size() } noexcept -> std::convertible_to<std::size_t>;
};
// Concepto anidado: Combinación de otros conceptos
template<typename T>
concept Container = HasSize<T> && requires(T t) {
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
};
// Uso de 'requires requires' (anti-patrón si se usa sin nombrar)
// En su lugar, definimos un concepto nombrado para mayor claridad.
template<typename T>
concept Printable = requires(T t) {
std::cout << t;
};
// Ejemplo de uso en una clase con restricciones (Constraints on classes)
template<typename T>
requires Container<T> && FastSizeable<T>
class DataBuffer {
private:
T data;
public:
explicit DataBuffer(T d) : data(std::move(d)) {}
const T& get() const { return data; }
};
// Función con cláusula 'requires' simple
void print_info(Printable auto const& item) {
std::cout << "Elemento: " << item << "\n";
}
// Función con expresión 'requires' anidada directamente (no recomendado para lógica compleja)
template<typename T>
void process_complex(T&& t) {
if constexpr (requires(T a) { { a.size() } -> std::same_as<std::size_t>; }) {
std::cout << "El tamaño es exactamente std::size_t\n";
}
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::string str = "Hola C++20";
// Validado por HasSize y Container
DataBuffer buffer(vec);
std::cout << "Buffer size: " << buffer.get().size() << "\n";
// Validado por Printable
print_info(str);
print_info(vec[0]); // Error si intentamos pasar un entero sin sobrecarga,
// pero funciona si el tipo tiene operator<<
// Demostración de validación de tipo de retorno
process_complex(vec);
return 0;
}
Desglose del concepto
En el ejemplo, hemos visto tres niveles de complejidad:
HasSize(Simple): La expresiónt.size();dentro derequires(T t)comprueba si el compilador puede derivar una llamada válida. No nos importa qué devuelvasize(), solo que exista.FastSizeable(Compuesto): Aquí la sintaxis{ t.size() } noexcept -> std::convertible_to<std::size_t>;es clave. El uso de llaves{}encapsula la expresión para que podamos aplicar operadores sobre su resultado.noexceptverifica que la expresión sea marcada como no-lanzadora. Sit.size()lanzara una excepción, el concepto resultaríafalse.std::convertible_toes un concepto de la biblioteca estándar que valida que el tipo devuelto sea compatible constd::size_tmediante una conversión implícita.Container(Anidado/Combinado): EnContainer, combinamos la validación de existencia desize()con la validación de quebegin()yend()devuelvan tipos que satisfagan el conceptostd::input_iterator[C++20].DataBuffer(Constraints en clases): Hemos aplicado la restricción directamente en la definición de la clase. Esto asegura que no se pueda instanciarDataBuffer<int>, porqueintno cumpleContainer. Esto es preferible a usarstatic_assertdentro del constructor, ya que permite una mejor selección de sobrecargas en plantillas.process_complex(Nested requires): Aunque el código muestra unif constexpr (requires(...)), esto es una expresiónrequiresactuando como un valor booleano en tiempo de compilación. Se utiliza para “feature detection” (detección de características) sin necesidad de definir un concepto formal.
El error frecuente
Un error muy común al trabajar con expresiones compuestas es olvidar que la validación de noexcept es extremadamente estricta.
template<typename T>
concept StrictNoexcept = requires(T t) {
{ t.do_something() } noexcept;
};
struct Safe {
void do_something() noexcept {}
};
struct Unsafe {
void do_something() {} // No tiene noexcept
};
// Safe funciona, pero Unsafe hace que StrictNoexcept sea false.
Si tu código depende de una optimización basada en que una función no lanza excepciones (por ejemplo, para usar std::is_nothrow_move_constructible), pero tu concepto requires no verifica explícitamente la cláusula noexcept, podrías estar permitiendo tipos que rompen tus garantías de seguridad de excepciones, lo cual es un bug de diseño difícil de rastrear. Además, recuerda que el uso de requires requires (una cláusula requires seguida de una expresión requires) es sintácticamente válido pero es un anti-patrón de legibilidad; siempre es mejor extraer la lógica a un concept nombrado.
Para compilar el ejemplo:
g++ -std=c++20 -Wall -Wextra -Wpedantic -o example example.cpp
N° 56