Cómo convertir números en texto con std::to_chars en C++17

Cómo convertir números en texto con std::to_chars en C++17

En esta publicación, le mostraré cómo usar las rutinas de conversión más nuevas y de bajo nivel de C++ 17. Con la nueva funcionalidad, puede transformar rápidamente números en texto y obtener un rendimiento excelente en comparación con las técnicas anteriores.

Antes de C++17

Hasta C++17, teníamos varias formas de convertir números en cadenas:

  • sprintf / snprintf
  • stringstream
  • to_string
  • itoa
  • y bibliotecas de terceros como boost - lexical cast

Y con C++17 tenemos otra opción:std::to_chars (junto con el método correspondiente from_chars ) ! Ambas funciones residen en el <charconv> encabezado.

¿Por qué necesitamos nuevos métodos? ¿No era lo suficientemente buena la vieja técnica?

En resumen:porque to_chars y from_chars son de bajo nivel y ofrecen el mejor rendimiento posible.

Las nuevas rutinas de conversión son:

  • no lanzar
  • sin asignación
  • sin compatibilidad con la configuración regional
  • seguridad de la memoria
  • el informe de errores brinda información adicional sobre el resultado de la conversión
  • encuadernado marcado
  • garantías explícitas de ida y vuelta:puede usar to_chars y from_chars para convertir el número de un lado a otro, y le dará las representaciones binarias exactas. Esto no está garantizado por otras rutinas como printf/sscanf/itoa, etc.

Un ejemplo sencillo:

std::string str { "xxxxxxxx" };
const int value = 1986;
std::to_chars(str.data(), str.data() + str.size(), value);

// str is "1986xxxx"

Las nuevas funciones están disponibles en los siguientes compiladores:

  • Visual Studio 2019 16.4:soporte completo y soporte inicial de punto flotante de VS 2017 15.7
  • GCC - 11.0 - soporte completo, y desde GCC 8.0 - solo soporte de enteros
  • Clang 7.0:aún en progreso, solo admite números enteros

La serie

Este artículo es parte de mi serie sobre las utilidades de biblioteca de C++17. Aquí está la lista de los artículos:

  • Refactorización con std::optional
  • Usando std::optional
  • Manejo de errores y std::optional
  • Todo lo que necesitas saber sobre std::variant de C++17
  • Todo lo que necesitas saber sobre std::any de C++17
  • std::string_view Rendimiento y seguimiento
  • Buscadores de cadenas C++17 y seguimiento
  • Utilidades de conversión:en std::from_chars - de una cadena a un número y en std::to_chars - de números a cadenas
  • ¿Cómo obtener el tamaño de archivo en C++? y std:filesystem::file_size Ventajas y diferencias
  • Cómo iterar a través de directorios en C++17

Recursos sobre C++17 STL:

  • C++17 en detalle por Bartek!
  • C++17:la guía completa de Nicolai Josuttis
  • Fundamentos de C++, incluido C++ 17 por Kate Gregory
  • Características prácticas de C++14 y C++17:por Giovanni Dicanio
  • Libro de cocina C++17 STL de Jacek Galowicz

Usando to_chars

to_chars es un conjunto de funciones sobrecargadas para tipos integrales y de punto flotante.

Para tipos enteros hay una declaración:

std::to_chars_result to_chars(char* first, char* last,
                              TYPE value, int base = 10);

Donde TYPE se expande a todos los tipos de enteros con y sin signo disponibles y char .

Desde base puede variar de 2 a 36, ​​los dígitos de salida que son mayores que 9 se representan como letras minúsculas:a...z .

Para números de coma flotante, hay más opciones.

En primer lugar, hay una función básica:

std::to_chars_result to_chars(char* first, char* last, FLOAT_TYPE value);

FLOAT_TYPE se expande a float , double o long double .

La conversión funciona igual que con printf y en la configuración regional predeterminada ("C"). Utiliza %f o %e especificador de formato que favorece la representación más corta.

La próxima sobrecarga de función agrega std::chars_format fmt que te permite especificar el formato de salida:

std::to_chars_result to_chars(char* first, char* last, 
                              FLOAT_TYPE value,
                              std::chars_format fmt);

chars_format es una enumeración con los siguientes valores:scientific , fixed , hex y general (que es una composición de fixed y scientific ).

Luego está la versión "completa" que también permite especificar precision :

std::to_chars_result to_chars(char* first, char* last, 
                              FLOAT_TYPE value,
                              std::chars_format fmt, 
                              int precision);

La Salida

Cuando la conversión es exitosa, el rango [first, last) se llena con la cadena convertida.

El valor devuelto para todas las funciones (para compatibilidad con enteros y puntos flotantes) es to_chars_result , se define de la siguiente manera:

struct to_chars_result {
    char* ptr;
    std::errc ec;
};

El tipo contiene información sobre el proceso de conversión:

Condición de devolución Estado de from_chars_result
Éxito ec es igual al valor inicializado std::errc y ptr es el puntero uno más allá del final de los caracteres escritos. Tenga en cuenta que la cadena no termina en NULL.
Fuera de rango ec es igual a std::errc::value_too_large el rango [first, last) en estado no especificado.

Como puede ver, solo tenemos dos opciones:éxito o fuera de rango, ya que existe la posibilidad de que su búfer no tenga el tamaño suficiente para contener el resultado.

Un Ejemplo - Tipos enteros

En resumen, aquí hay una demostración básica de to_chars .

#include <iostream>
#include <charconv> // from_chars, to_chars
#include <string>

int main() {
    std::string str { "xxxxxxxx" };
    const int value = 1986;

    const auto res = std::to_chars(str.data(), 
                                   str.data() + str.size(), 
                                   value);

    if (res.ec == std::errc())    {
        std::cout << str << ", filled: "
            << res.ptr - str.data() << " characters\n";
    }
    else if (res.ec == std::errc::value_too_large) {
        std::cout << "value too large!\n";
    }
}

A continuación puede encontrar un resultado de muestra para un conjunto de números:

value valor salida
1986 1986xxxx, filled: 4 characters
-1986 -1986xxx, filled: 5 characters
19861986 19861986, filled: 8 characters
-19861986 value too large! (el búfer tiene solo 8 caracteres)

Un Ejemplo - Punto Flotante

En MSVC (a partir de 15.9, soporte completo en 16.0 + mejoras posteriores) y GCC 11.0 también podemos probar el soporte de punto flotante:

std::string str{ "xxxxxxxxxxxxxxx" }; // 15 chars for float

const auto res = std::to_chars(str.data(), str.data() + str.size(),  value);

if (res.ec == std::errc())     {
    std::cout << str << ", filled: "
              << res.ptr - str.data() << " characters\n";
}
else if (res.ec == std::errc::value_too_large)     {
    std::cout << "value too large!\n";
}

Y aquí hay una demostración de trabajo bajo GCC 11.0:

#include <iostream>
#include <charconv> // from_chars, to_chars
#include <string>

int main() {
    std::string str { "xxxxxxxx" };
    const double value = 1986.10;
 
    const auto res = std::to_chars(str.data(), str.data() + str.size(), value);
    if (res.ec == std::errc()) {
        std::cout << str << ", filled: " << res.ptr - str.data() << " characters\n";
    }
    else {
        std::cout << "value too large!\n";
    }
}

Juega con el código @Compiler Explorer

A continuación puede encontrar un resultado de muestra para un conjunto de números:

value valor formato salida
0.1f - 0.1xxxxxxxxxxxx, filled: 3 characters
1986.1f generales 1986.1xxxxxxxxx, filled: 6 characters
1986.1f científico 1.9861e+03xxxxx, filled: 10 characters

Benchmark y algunos números

En mi libro, C++17 en detalle, hice algunos experimentos de rendimiento para conversiones de enteros, y la nueva funcionalidad es varias veces más rápida que to_string o sprintf y más de 10... ¡o incluso 23 veces más rápido que las versiones stringstream!

También tengo que verificar el soporte de coma flotante, pero los resultados que veo en varios lugares también afirman una aceleración de orden de magnitud con respecto a las técnicas más antiguas.

Vea la charla de Stephan T. Lavavej (en las referencias) sobre la implementación de charconv en MSVC donde compartió algunos resultados de referencia de punto flotante.

C++20

En C++20, tenemos más métodos que nos permiten convertir datos en cadenas y formatearlas.

La biblioteca se llama std::format y se basa en un marco popular {fmt}

Eche un vistazo:https://en.cppreference.com/w/cpp/utility/format

A partir de hoy (junio de 2021), puede jugar con la biblioteca en MSVC 16.10 (VS 2019):

std::vector<char> buf;
std::format_to(std::back_inserter(buf), "{}", 42);
 // buf contains "42"

También puede consultar esta publicación de blog que le presenta los conceptos de std::format. :
Una guía extraterrestre para el formato de texto C++20 - Historias de C++

En cuanto a los puntos de referencia, puede leer este:Convertir cien millones de enteros en cadenas por segundo; incluye una comparación con to_chars y muestra varios resultados para conversiones de enteros.

Resumen

Con C++17, obtuvimos una nueva funcionalidad que permite conversiones fáciles y de bajo nivel entre números y texto. Las nuevas rutinas son potentes y exponen toda la información que necesita para crear analizadores o serializaciones avanzados. No lanzan, no asignan, marcan cheques y ofrecen un rendimiento excelente.

Lea aquí sobre el from_chars correspondiente método

Extra :desde que CppCon 2019, Microsoft abrió su implementación STL, ¡así que incluso puede echar un vistazo al código de charconv!

También sugiero encarecidamente ver la charla de Stephan sobre el progreso y los esfuerzos para obtener soporte completo de charconv. La función parecía muy simple a primera vista, pero parecía ser muy complicada de admitir ya que no se podía usar la biblioteca C y todo tenía que hacerse desde cero.

Punto flotante <charconv> :Haciendo su código 10 veces más rápido con el jefe final de C++ 17 por Stephan T. Lavavej

Tu turno

¿Qué opinas de las nuevas rutinas de conversión? ¿Los has probado?
¿Qué otras utilidades de conversión utiliza?