6

Desde el comité de estándares de se puso como meta aprobar un nuevo estándar cada 3 años. Así ha sido hasta la fecha y hemos tenido los siguientes estándares:

La estandarización se divide en cuatro grupos de trabajo:

  • Núcleo (Core): Mantenimiento del lenguaje y estandarización de nuevas propuestas.
  • Evolución: Desarrollo de nuevas características para el lenguaje.
  • Librería: Mantenimiento de la librería estándar y estandarización de nuevas propuestas.
  • Evolución de librería: Desarrollo de nuevas características para la librería estándar.

Estos son los documentos técnicos del nuevo estándar que me han parecido más interesantes:

Documento técnico Título Grupo
P0288R7 move_only_function (antes conocida como any_invocable) Librería
P0447R16 Añadir std::hive (colmena) a la librería estándar Librería
P0627R5 Marcar código inalcanzable Librería
P0849R8 auto(x) Núcleo
P2128R6 Operador de indizado multidimensional Núcleo
P1169R2 Operador paréntesis estático static operator() Evolución
P2036R3 Cambiar el ámbito del tipo del retorno arrastrado de las lambdas Núcleo
P1072R10 basic_string::resize_and_overwrite Librería
P2012R1 Arreglar el bucle for de rango Evolución, Núcleo
P2093R9 Salida con formato Evolución de librería
P2276R1 Arreglar cbegin Librería, Evolución de librería
P2322R4 ranges::fold Evolución de librería
P1240R2 Reflexión escalable Evolución

Los nombres de los documentos técnicos tienen el siguiente significado:

  • PXXXX: P de paper que en este contexto significa "documento técnico", el número a continuación de P es el identificador del documento.
  • RYY: R de revision que en este contexto significa "revisión", el número a continuación de R es la cantidad de revisiones del documento, entendiendo revisiones como versiones del documento, en cada nueva versión se corrigen problemas, añaden, modifican o eliminan detalles, etc...

1Debido a algún extraño despiste, escribí dos hilos respecto C++17.

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82

1 Answers1

4

move_only_function (antes conocida como any_invocable).

Se propone una versión de std::function que:

  1. No sea copialbe (sólo movible).
  2. Resuelva el bug de la correctitud constante de std::function detallado en n4348.
  3. De soporte a los cualificadores de función const, volatile, referencia y noexcept.
  4. Carezca de target_type y target.
  5. Su invocación tenga estrictas precondiciones.

Añadir std::hive (colmena) a la librería estándar.

El objetivo de un contenedor en la librería estándar no puede ser proveer de la solución óptima en todos los escenarios. Existirán campos en que la solución óptima sea un contenedor propio que se ajuste a un escenario concreto. Aún así existen soluciones para casos generalizados que aún no forman parte de la librería estándar.

Una colmena (hive) es la formalización, extensión y optimización de lo que típicamente se conoce como 'bucket array' cuyo concepto es:

Se dispone de múltiples bloques de memoria y una bandera para cada elemento que denota si el elemento está activo o borrado. Si un elemento está borrado es ignorado durante las iteraciones, cuando todos los elementos de un bloque están marcados como borrados dicho bloque se elimina. Si se inserta un nuevo elemento cuando todos los bloques están llenos, se crea un nuevo bloque de memoria.

Marcar código inalcanzable.

Los compiladores no pueden conocer todas las situaciones en que un código puede ejecutarse, siempre existirán programas en que el compilador no pueda determinar que una situación es imposible, por ejemplo:

void f(int numero_que_solo_es_0_1_2_o_3)
{
    switch (numero_que_solo_es_0_1_2_o_3)
    {
    case 0:
    case 2:
        f02();
        break;
    case 1:
        f1();
        break;
    case 3:
        f3();
        break;
    }
}

Ese código puede producir el siguiente código máquina...

cmp eax, 4
jae skip_switch
lea rcx, [jump_table]
jmp qword [rcx + rax*8]

... del que las dos primeras instrucciones son innecesarias. Este tipo de problemas surgen cuando el programador sabe que una situación es imposible pero no es obvio para el compilador; sería útil poder decirle al compilador que evite comprobaciones en tiempo de ejecución para casos que sabemos que son imposibles. Se propone para ello crear una función en la librería estándar:

namespace std {
    …
    [[noreturn]] void unreachable();
    …
}

Dejando el ejemplo inicial así:

void f(int numero_que_solo_es_0_1_2_o_3)
{
    switch (numero_que_solo_es_0_1_2_o_3)
    {
    case 0:
    case 2:
        f02();
        break;
    case 1:
        f1();
        break;
    case 3:
        f3();
        break;
    default:
        std::unreachable();
    }
}

auto(x).

Se propone auto(x) y auto{x} para convertir x en un prvalue (valor del lado derecho puro) como si se hubiera pasado x como argumento a una función por valor. Uno de los casos de uso se ilustra pasando el siguiente código:

void pop_front_alike(Container auto& x) {
    auto a = x.front();
    std::erase(x.begin(), x.end(), a);
}

A este código:

void pop_front_alike(Container auto& x) {
    std::erase(x.begin(), x.end(), auto(x.front()));
}

En el segundo caso queda explicitado que se quiere pasar una copia anónima. Sería una estandarización en el lenguaje de algo que a día de hoy es engorroso de usar:

template<class T>
constexpr decay_t<T> decay_copy(T&& v) noexcept(
    is_nothrow_convertible_v<T, decay_t<T>>) {
    return std::forward<T>(v);
}

Operador de indizado multidimensional.

Se propone que el operador de indizado (operator []) pueda aceptar entre cero o múltiples argumentos (incluidos argumentos variádicos). No se propone cambiar el funcionamiento de las formaciones:

struct S {
    S operator[](auto ...);
} s;
s[1, 2, 3] = 7; // Correcto, llama a s.operator[](1, 2, 3)

int a[10];
a[1, 2, 3] = 7; // Error, no se permiten expresiones con coma en el indizado de formaciones

Operador paréntesis estático static operator().

Se propone permitir que el operador de llamada (también conocido como operador paréntesis operator()) sea estático en lugar de requerir que sea una función miembro no estática. Esto permitirá evitar el problema de pasar un this implícito cuando se usan objetos función sin miembros (como std::less).

Cambiar el ámbito del tipo del retorno arrastrado de las lambdas.

La manera en que funciona la búsqueda de símbolos en las lambdas es sorprendente: funciona de manera diferente en el cuerpo de la lambda y en el tipo del retorno arrastrado. Por ejemplo este código no compila:

auto lambda = [j = 0]() mutable -> decltype(j) {
    return j++;
};

Esto sucede porque la variable j que se declara en la captura no es visible en el momento de preguntar por su tipo: la j de la captura forma parte del cuerpo de la lambda mientras que la j del tipo de retorno arrastrado se busca fuera, provocando el error de compilación, pero podría ser peor:

double j = 42.0;
// ...
// Varios centenares de líneas de código
// ...
auto lambda = [j = 0]() mutable -> decltype(j) {
    return j++;
};

El código anterior sí que compila pero hace que el retorno de la lambda sea double cuando probablemente la intención era que fuese int. Para evitar estos problemas se propone que la búsqueda de símbolos empiece la búsqueda en el retorno arrastrado de la lambda en lugar de buscar directamente en el ámbito externo a la lambda.

basic_string::resize_and_overwrite.

La clase basic_string tiene ciertos problemas de rendimiento al inicializar o manipular; para esas tareas se debe tomar una difícil decisión:

  • Inicializar espacio de más: resize inicializa a cero los datos y después los copia.
  • Hacer copias de más: Rellenar un búfer temporal que será copiado a la cadena.
  • Reservar por si acaso: reserve seguido de append, en cada operación de anexado se verifica la capacidad y de basic_string.

Para solucionar este problema se propone una nueva función miembro para la clase basic_string:

template<class Operation>
constexpr void resize_and_overwrite(size_type n, Operation op);

Cuyo comportamiento sería:

  • Si n <= size(), borra los últimos size() - n elements.
  • Si n > size(), anexa n - size() elementos inicializados por defecto.
  • Llama erase(begin() + op(data(), n), end()).

Arreglar el bucle for de rango.

En C++ es posible crear referencias colgantes (dangling references) por descuido:

// Funciones que devuelven temporales creados al vuelo
std::tuple<std::string, int, int> dame_tupla() { return {}; }
std::vector<std::string> dame_textos() { return {}; }

int main()
{
    // Referencias colgantes
    const auto &str1 = std::get<0>(dame_tupla());   // Referencia a sub-elemento de temporal.
    const std::string &str2 = dame_textos()[0];     // Referencia a sub-elemento de temporal.

    std::cout << str1 << ' ' << str2 << '\n';
    return 0;
}

Al menos estas referencias colgantes son visibles en el código, pero si formasen parte de un bucle for de rango, quedarían ocultas:

for (auto elemento : get<0>(dame_tupla()))
    ...

for (auto elemento : dame_textos()[0])
    ...

Se propone modificar la implementación interna del bucle for de rango para esquivar este problema, pasando de su implementación actual:

{
    init-statement
    auto && __range = range-expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {
        range-declaration = *__begin;
        loop-statement
    }
}

A esta:

{
    init-statementopt
    auto && range = for-range-initializer;
    [&](auto&& range) {
        auto begin = begin-expr ;
        auto end = end-expr ;
        for ( ; begin != end ; ++begin ) {
            for-range-declaration = * begin ;
            statement // return o goto o instrucciones co- afectan a todo el bucle
        }
    }( for-range-initializer );
}

Salida con formato.

La función std::format de C++20 tiene problemas de rendimiento:

std::cout << std::format("Hola, {}!", nombre);

El código anterior crea una cadena temporal que requiere de su constructor y destructor para después pasar dicha cadena al operador de escritura con formato de std::cout cuando dicha cadena ya había sido formateada. Se propone crear una función que de formato al texto directamente en la salida:

std::print("Hola, {}!", nombre);

Arreglar cbegin.

Las funciones cbegin() y cend() permiten acceder a una colección de datos con iteradores constantes, esto permite evitar que se puedan modificar los elementos iterados. Antes de C++20 este comportamiento estaba garantizado ya que las colecciones de datos propagaban la propiedad de sólo lectura a sus elementos (si la colección es de sólo lectura, también lo serán sus elementos). Pero desde C++20 se han proveído contenedores/rangos que no propagan la propiedad de sólo lectura.

Para tipos como std::span o vistas, se pueden modificar los elementos incluso aunque el contenedor/rango se declare como constante. Para esos tipos el código genérico std::cbegin(), std::cend() y similares está roto: siempre llama a los miembros begin() y end() (aunque los miembros cbegin() y cend() existan) por lo que es posible modificar elementos aún iterando con cbegin() y cend().

Idealmente esto se debería solucionar, sin embargo otras propuestas opinan lo contrario: P2276R0 y P2278R0, por ello se proponen ciertos cambios para minimizar las consecuencias de este comportamiento:

  • Proveer los miembros cbegin() y cend() para std::span.
  • Proveer un mecanismo (conceptos) para verificar si un contenedor propaga o no la propiedad de sólo lectura.
  • Deshabilitar std::cbegin(), std::cend(), etc... cuando una colección de datos no propague la propiedad de sólo lectura.
  • Deshabilitar std::ranges::cbegin(), std::ranges::cend(), etc... cuando el rango no propague la propiedad de sólo lectura.

ranges::fold.

Se propone crear un algoritmo fold en la cabecera <algorithm> ya que la librería estándar carece del dicho algoritmo sobre rangos. Aunque existe una versión de fold basada en iteradores, actualmente recibe el nombre de std::accumulate, por defecto suma elementos y se aloja en la cabecera <numeric>. Pero fold es mucho más que sumar, así que es importante darle un nombre más genérico y evitar que la operación por defecto.

Reflexión escalable.

Se propone dotar al lenguaje de mecanismos de reflexión estática, la propuesta incluye la creación de un nuevo operador unario (^):

constexpr std::meta::info refl = ^nombre_o_expresion;

Dando como resultado un valor conocido en tiempo de compilación que identifica el elemento de manera dependiente de implementación. El operador de reflexión se puede usar sobre:

  • Un nombre de tipo.
  • Un nombre de plantilla.
  • Un espacio de nombres.
  • El espacio de nombres raíz ::.
  • Una expresión.

Se propone que el tipo std::meta::info sea un nuevo tipo de escalar que sólo soporte conversión a bool y equidad/inequidad. Se usará como parámetro para diferentes funciones de reflexión como por ejemplo:

consteval bool is_namespace(info entity) {...};
consteval bool is_template(info entity) {...};
consteval bool is_type(info entity) {...};

Pero se puede usar también directamente:

typedef int I1;
typedef int I2;
static_assert(^I1 == ^I2);  // Correcto
static_assert(^I1 == ^int); // Correcto
PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82