Núcleo.
Los conceptos son una mejora al sistema de plantillas de C++, son un tipo especial de función evaluable en tiempo de compilación que debe devolver verdadero para considerar que el concepto se cumple. Su función es controlar las características que deben cumplir los parámetros de una plantilla. Pero su funcionalidad no queda limitada al ámbito de las plantillas, también pueden ser usados en otros contextos.
Los conceptos son una funcionalidad de C++ muy esperada que lleva 10 años en desarrollo (quedó fuera de C++11 por falta de tiempo, fuera de C++14 por considerarse incompleta y fuera de C++17 por falta de consenso).
Ejemplo.
Podemos definir un concepto de la siguiente manera:
template <typename tipo_t>
concept bool es_sumable()
{
return requires(tipo_t a, tipo_t b)
{
{ a + b } -> tipo_t;
};
};
El concepto anterior se cumple si la expresión a + b
es posible y su tipo es tipo_t
, entonces podemos usar el concepto como parámetro:
auto suma(const es_sumable &a, const es_sumable &b) { return a + b; }
Y la función suma
sólo aceptará tipos que cumplan con el concepto es_sumable
. Se puede usar también como variable:
es_sumable resultado = "Hola "s + "mundo!"s;
La variable resultado
tendrá que cumplir es_sumable
o el código no compilará. Por último, se puede requerir que una plantilla cumpla con uno o varios conceptos:
template <typename tipo_t>
concept bool entero() { return std::is_integral<tipo_t>::value; }
template <typename tipo_t>
concept bool de_4_bytes() { return sizeof(tipo_t) == 4; }
template <typename T>
T dobla(const T &a) requires entero<T>() && de_4_bytes<T>()
{
return a + a;
}
En la función anterior, el parámetro plantilla T
tiene que ser un entero de 32bits, en caso de no serlo se mostrará un error claro y conciso:
auto dobla(111ull);
note: constraints not satisfied
T dobla(const T &a) requires entero<T>() && de_4_bytes<T>()
^~~~~
note: within 'template<class tipo_t> concept bool de_4_bytes() [with tipo_t = long long unsigned int]'
concept bool de_4_bytes()
^~~~~~~~~~
note: 'sizeof (long long unsigned int) == 4' evaluated to false
Inicializadores por nombre en agregados (DT P0329R0).
Propuesta para permitir especificar los nombres en la inicialización de objetos de tipo agregado, como ya se permite en C (desde C99).
Ejemplo.
Suponiendo que tenemos el siguiente objeto:
struct punto { float x, y, z; };
La siguiente inicialización sería correcta en a partir de C++20:
punto p { .x{}, .y{}, .z = 100.f };
Hay ciertas diferencias respecto a C99 que se deben tener en cuenta:
Se deben inicializar todos los miembros:
punto p { .x = .0f, .z = 100.f }; // Correcto en C, incorrecto en C++
Los miembros deben estar en el orden de definición:
punto p { .z = 100.f, .y = .0f, .x = .0f }; // Correcto en C, incorrecto en C++
Los miembros no deben estar duplicados:
punto p { .x = 100.f, .x = .0f }; // Correcto en C, incorrecto en C++
No se permite la inicialización de formación:
struct punto { float v[3]; };
p { .v[1] = 100.f }; // Correcto en C, incorrecto en C++
No se permite la inicialización anidada:
struct punto { float x, y, z; };
struct linea { punto a, b; };
linea l { .a.x = 100.f }; // Correcto en C, incorrecto en C++
Lambdas plantilla (DT P0428R2).
Desde C++14 el lenguaje permite Lambda Genéricas; pero pese a que hacen las Lambdas de C++ más útiles, las Lambda genéricas no ofrecen toda la flexibilidad que podría esperarse, hasta el punto que en algunos contextos sigue siendo más útil utilizar una función plantilla antes que una Lambda Genérica. Este documento técnico propone la siguiente nueva sintaxis para Lambdas:
[captura(s)]<parámetro(s) plantilla>(parámetro(s) Lambda){ código; }
Es decir, se añaden los paréntesis angulares (<
y >
) a la Lambda para poder gestionar parámetros de plantilla.
Ejemplo.
Lambda que trabaja sólo con std::vector
versión Genérica:
auto g = [](auto v)
{
static_assert(std::is_std_vector<decltype(v)>::value, "necesito vector!");
// Obtenemos el tipo almacenado en el vector
using T = typename decltype(vector)::value_type;
// ... código ...
};
Versión plantilla:
auto p = []<typename T>(std::vector<T> v)
{
// ... código ...
};
Lambda que llama una función estática del tipo pasado y accede a tipos anidados versión Genérica:
auto g = [](const auto &t)
{
/* Necesitamos decay porque si no deduce referencia (&)
y no podríamos usar el operador de contexto (::) para
acceder a funciones estáticas o tipos anidados. */
using T = std::decay_t<decltype(t)>;
T copia = t;
T::f_estatica();
using Iterator = typename T::iterator;
// ... código ...
};
Versión plantilla:
auto p = []<typename T>(const T &t)
{
/* Obtenemos el tipo como parámetro de plantilla, esto
nos evita complicar el código con decay. */
T copia = t;
T::f_estatica();
using Iterator = typename T::iterator;
// ... código ...
};
Comparación a tres bandas (documento técnico P0515R3).
Esta propuesta es conocida con el pseudónimo de "El operador nave espacial" por el parecido del operador propuesto (operator <=>
) con un Tie-Fighter:
La propuesta sugiere añadir un operador nuevo al lenguaje que realice una comparación entre objetos pudiendo obtener tres resultados distintos, para la comparación a tres bandas a <=> b
se obtendría:
- Un valor menor a cero si
a
fuese menor que b
.
- Un valor equivalente a cero si
a
y b
fuesen equivalentes.
- Un valor mayor a cero si
a
fuese mayor que b
.
La comparación a tres bandas permitiría reducir la cantidad de código necesario para permitir que un dato sea estrictamente ordenable, que requiere varias comparaciones que suelen implementar en base a las anteriores para ahorrar código:
struct coordenada { int x, y; };
bool operator==(const coordenada &a, const coordenada &b) { return a.x == b.x && a.y == b.y; }
bool operator< (const coordenada &a, const coordenada &b) { return a.x < b.x || (a.x == b.x && a.y < b.y); }
bool operator!=(const coordenada &a, const coordenada &b) { return !(a==b); }
bool operator<=(const coordenada &a, const coordenada &b) { return !(b<a); }
bool operator> (const coordenada &a, const coordenada &b) { return b<a; }
bool operator>=(const coordenada &a, const coordenada &b) { return !(a<b); }
La propuesta sugiere permitir al compilador generar por defecto la comparación a tres bandas, de hacerse así realizaría una comparación a tres bandas miembro a miembro de manera recursiva entre los operadores del operador.
Ejemplo.
Comparación a tres bandas (generada por el compilador) del objeto del código anterior:
struct coordenada
{
int x, y;
auto operator<=>(const coordenada &) const = default;
};
El tipo de retorno será auto
porque el tipo retornado es dependiente del compilador, la única restricción que debe seguir es que se pueda comparar contra cero (mayor, menor o igual a cero).
for
de rango con inicializador (documento técnico P0614R1).
En C++11 se añadió el bucle for
de rango con la siguiente sintáxis:
for (declaración : expresión) {
Código
}
Permite crear código más fácil de entender y mantener, pero carece de la flexibilidad del bucle for
tradicional al no permitir inicializadores. La propuesta consiste en flexibilizar el bucle for
de rango con un inicializador:
for (inicialización; declaración : expresión) {
Código
}
Ejemplo.
Obtenemos un objeto temporal del cual iteramos una de sus propiedades internas:
for (Coche c = fabricar_coche(); auto &rueda : c.ruedas()) {
std::cout << comprobar_presion(rueda) << '\n';
}
Lambdas sin estado asignables y construibles por defecto (DT P0624R2).
Una lambda sin capturas recibe el nombre de "Lambda sin estado", este tipo de lambdas es implícitamente convertible a puntero a función:
using funcion_void = void();
función_void *fv = []{ std::cout << "Hola mundo!\n"; };
Pero en algunas situaciones no se comportan como punteros a función. Los punteros a función son copiables, asignables y construibles mientras que las lambdas sin estado no lo son.
Ejemplo.
Esta propuesta hace que las lambda sin estado se comporten como punteros a función:
using funcion_void = void();
void a(){}
void b(){}
void f(funcion_void *){}
funcion_void *fv; // Correcto, construcción por defecto.
fv = a; // Correcto, asignación.
fv = b; // Correcto, reasignación.
f(fv); // Correcto, copia.
auto lambda = []{};
decltype(lambda) l; // Construible por defecto? Error (Correcto en C++20).
decltype(lambda) m = lambda; // Asignable? Error (Correcto en C++20).
decltype(lambda) m = l; // Copiable? Error (Correcto en C++20).
Permitir la expansión de paquetes de parámetros en la captura de lambdas (DT P0780R0)
En una lambda se pueden capturar los parámetros por copia:
template <typename ... paquete_de_parametros>
void f(paquete_de_parametros ... parametros)
{
auto lambda = [parametros ...]{};
// ^^^^^^^^^^^^^^ <--- captura por copia.
}
Pero el estándar previo a C++20 prohíbe explícitamente expandir paquetes de parámetros en las capturas.
Ejemplo.
C++20 levanta esta prohibición:
template <typename ... paquete_de_parametros>
void f(paquete_de_parametros ... parametros)
{
auto lambda = [std::move(parametros) ...]{};
// ^^^^^^^^^^^^^^^^^^^^^ <--- Expande con std::move
}
Literales de cadena de caracteres como parámetros no-tipo en plantillas (DT P0424R2).
El estándar previo a C++20 establece explícitamente que los literales de cadena de caracteres no pueden ser usados como parámetros de plantilla:
template <auto T> void f() {}
f<"Hola">(); // Error!
f<"Hola">(); // Error!
El motivo de esta limitación es que dos literales de texto iguales (como en el ejemplo anterior) no tienen garantías de ser el mismo literal (se almacenan como punteros) y en consecuencia no hay manera de saber si dos funciones con el mismo literal de texto son la misma. En C++20 se permite la verificación del parámetro literal de texto en tiempo de compilación pudiendo comprobar si dos literales de texto tienen el mismo contenido, esto permite que los literales de texto puedan ser usados como parámetros no-tipo en plantillas.
Macros de verificación de funcionalidades (DT P0941R1).
Si el compilador dispone de una funcionalidad determinada, ésta podrá ser verificada usando la macro __has_cpp_attribute
. La macro recibirá un único parámetro que será un identificador asociado a una funcionalidad determinada.
Ejemplo.
Si el compilador dispone del bucle for
de rango (C++11) lo usamos, usamos el bucle tradicional en caso contrario:
int datos[100];
#if __has_cpp_attribute(__cpp_range_based_for)
for (int &i : datos)
{
i = 0;
}
#else
for(int *i = datos; i != datos + 100; ++i)
{
*i = 0;
}
#endif
Explícito condicional explicit(bool)
(documento técnico P0892R2).
En el estándar C++11 se permitió controlar las conversiones implícitas al construir o convertir objetos al permitir marcar como explícitos los constructores u operadores. En C++20 se amplía esta funcionalidad pudiendo pasar un parámetro al especificador explicit
para hacer que éste se aplique condicionalmente.
Ejemplo.
El objeto O
tiene un constructor explícito cuando el tipo usado para construirlo sea de mayor tamaño que el tipo almacenado, constructor implícito en caso contrario:
template <typename T>
struct O
{
T t{};
template <typename U>
explicit(sizeof(U) > sizeof(T))
O(U u) :
t{static_cast<T>(u)} {}
};
La palabra clave explicit
esperará una expresión convertible a booleano a partir de C++20, así que habrá que corregir algunas líneas de código:
struct S
{
explicit(true) S(int) {} // Constructor explícito
explicit(false) S(std::string) {} // Constructor implícito
};
Permitir que las llamadas a funciones virtuales sean constexpr
(DT P1064R0).
Las llamadas a funciones virtuales están prohibidas en expresiones constantes en estándares previos a C++20.
Ejemplo.
El compilador conoce los tipos implicados en algunas llamadas virtuales y podría resolver la llamada en tiempo de compilación.
struct X1
{
constexpr virtual X1 const* f() const { return this; }
// ~~~~~~~~~~~~~~~~~ <--- Error! Correcto a partir de C++20.
};
struct Y
{
int m = 0;
};
struct X2: public Y, public X1
{
constexpr virtual X2 const* f() const { return this; }
// ~~~~~~~~~~~~~~~~~ <--- Error! Correcto a partir de C++20.
};
constexpr X1 x1;
static_assert( x1.f() == &x1 );
// ~~ <--- 'f' es una función virtual, no se puede usar
// en una verificación estática, salvo en C++20 o superior
Funciones Inmediatas (DT P1073R2).
A partir de C++11 se puede usar la palabra clave constexpr
para marcar valores o funciones que podrían ser válidas dentro de una expresión constante (expresión calculada en tiempo de compilación). Las funciones marcadas como constexpr
pueden también ser llamadas en tiempo de ejecución.
Esta propuesta añade la palabra clave consteval
para marcar valores o funciones que obligatoriamente deben ser evaluadas en tiempo de compilación y producir un error en caso contrario.
Las funciones consteval
reciben el nombre de función inmediata porque la función no se compila para ser ejecutada si no que se evalúa en tiempo de compilación y se sustituye su llamada por el resultado de la evaluación, una función inmediata no tiene por qué aparecer en el binario compilado.
Ejemplo.
Las funciones inmediatas provocan un error de compilación si no se pueden calcular en tiempo de compilación:
consteval int cubo(int n) {
return n*n*n;
}
// Correcto: 'cubo' se llama con un literal, es calculable en tiempo de compilación
constexpr int v = cubo(3);
int tres = 3;
// Error: 'cubo' se llama con una variable, NO es calculable en tiempo de compilación
int w = cubo(tres);
constinit
(DT P1143R2).
El lenguaje C++ no especifica el orden en que las variables estáticas deben ser inicializadas, dejando esta decisión en manos de los compiladores. Esto provoca que las variables estáticas con inicializadores dinámicos provoquen errores difíciles de rastrear cuando unas dependen de otras (esto se conoce como Static Initialization Order Fiasco). Las variables con inicializadores constantes evitan este problema, ya que pueden inicializarse en tiempo de compilación y se pueden usar de manera segura para inicializar otras variables.
Para garantizar que una variable se inicializa en tiempo de compilación, se debe obligar al compilador a que la inicialización sea constante, para ello se puede marcar una con constinit
:
Ejemplo.
Tenemos una función (valor_dinamico
) que calcula un valor en tiempo de ejecución y otra (misterio
) que podría ser evaluada en tiempo de compilación o en tiempo de ejecución:
int valor_dinamico() { static int x = 0; return x++; }
constexpr int misterio(bool p) { return p ? 42 : valor_dinamico(); }
constinit const int a = misterio(true); // Correcto.
constinit const int b = misterio(false); // Error
Si misterio
no puede ser evaluada en tiempo de compilación, la expresión marcada con constinit
falla.
using
enumerado (DT P1099r5).
En C++11 se añadieron al lenguaje los enumerados-clase:
enum class Dia : unsigned { L, M, X, J, V, S, D };
Este nuevo tipo de enumerado no exporta sus símbolos al ámbito superior:
enum Vocal { a, e, i, o, u };
enum class Dia : unsigned { L, M, X, J, V, S, D };
int main()
{
// Correcto, 'Vocal' exporta sus símbolos a este ámbito: 'Vocal::a' es accesible.
std::cout << a << '\n';
// Error, 'Dia' NO exporta sus símbolos a este ámbito: 'Día::D' NO es accesible.
std::cout << static_cast<unsigned>(D) << '\n';
return 0;
}
Ejemplo.
A partir de C++20 se permite importar los símbolos de un enumerado-clase mediante la cláusula using
:
enum Vocal { a, e, i, o, u };
enum class Dia : unsigned { L, M, X, J, V, S, D };
int main()
{
// Correcto, 'Vocal' exporta sus símbolos a este ámbito: 'Vocal::a' es accesible.
std::cout << a << '\n';
using enum Dia;
// Correcto, 'Dia' exportó explícitamente sus símbolos a este ámbito: 'Dia::D' es accesible.
std::cout << static_cast<unsigned>(D) << '\n';
return 0;
}
Permitir conversiones a formaciones abiertas (DT P0388R4)
WIP.
Funciones miembro especiales condicionalmente triviales (DT P0848R3)
WIP.
Parcheando la deducción de parámetros plantilla de clase (DT P1021R5)
WIP.
Deprecar volatile
(DT P1152R4)
WIP.