Guía completa de C++17 - std::string_view de componentes

En C++17, C++la biblioteca estándar introduce una clase de cadena especial: std::string_view, ubicada en el archivo de encabezado <string_view>, que nos permite procesar secuencias de caracteres como cadenas sin asignarles espacio de memoria . Es decir, std::string_viewlos objetos de tipo solo se refieren a una secuencia externa de caracteres , sin contenerlos. Por lo tanto, un objeto de vista de cadena se puede ver como una referencia a una secuencia de cadenas .
El uso de una vista de cadena tiene poca sobrecarga y es rápido (pasar uno por valor string_viewsiempre tiene poca sobrecarga). Sin embargo, también tiene algunos peligros potenciales Al igual que los punteros en bruto, depende del programador asegurarse de que la secuencia de cadenas a la que se hace referencia sea válidastring_view cuando se use.

cebador - cadena estática

Las cadenas estáticas son cadenas fijas en tiempo de compilación , se almacenan en el área de almacenamiento estático del archivo binario , y el programa solo puede leer y no se puede cambiar .

#include <iostream>
#include <string_view>
using namespace std;
int main() {
    
    
    const char* str_ptr = "this is a static string";
    // 字符串数组
    char str_array[] = "this is a static string";
    // std::string
    std::string str = "this is a static string";
    // std::string_view
    std::string_view sv = "this is a static string";
}

El código ensamblador es el siguiente:

  • Establezca directamente el puntero de cadena , str_ptrel código de ensamblaje es el siguiente, la cadena estática apuntará el puntero al área de almacenamiento estático y la cadena es de solo lectura. Si intenta modificarlo, fallará
      mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC0
  • String array , aquí hay una forma complicada, en lugar de usar un bucle, use varias movdeclaraciones para establecer la cadena en la pila. Asigne un espacio en la pila con una longitud igual a la longitud de la cadena + 1 (porque se debe incluir el carácter '\0' al final), y luego copie la cadena en el búfer. El compilador del código anterior divide una cadena larga en varios 64bitenteros largos y movlos coloca en el búfer de pila uno por uno. Esos enteros largos son en realidad: 0x2073692073696874=[ si siht],$0x6369746174732061=[citats a],0x21676e6972747320=[gnirts], que resulta ser el orden inverso de la cadena. El compilador usa este método para mejorar la eficiencia operativa.
		movabs  rax, 2338328219631577204
        movabs  rdx, 7163384644223836257
        mov     QWORD PTR [rbp-64], rax
        mov     QWORD PTR [rbp-56], rdx
        movabs  rax, 29113321772053280
        mov     QWORD PTR [rbp-48], rax
        lea     rax, [rbp-33]
        mov     QWORD PTR [rbp-32], rax
  • usar std::string_ El siguiente código ensamblador esiguarda la dirección inicial de la cadena y llama std::stringal constructor. basic_string( const CharT* s,const Allocator& alloc = Allocator() )Solo el puntero de inicio de la cadena se establece en el registro y se llama al constructor . En resumen, la asignación dinámica de memoria y la copia de cadenas definitivamente sucederán. En el constructor, se realizarán al menos las siguientes operaciones: determinar la longitud del cadena (por ejemplo strlen, recorrer String una vez), crear un nuevo espacio de memoria de acuerdo con la longitud de la cadena (o reservar más longitud), copiar la cadena en el espacio de memoria recién creado (recorrer la cadena por segunda vez).
        lea     rdx, [rbp-33]
        lea     rax, [rbp-96]
        mov     esi, OFFSET FLAT:.LC0
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
  • Use std::string_view, el siguiente código ensamblador establece directamente la longitud de la cadena 0x18, es decir 24Bytes, también está el puntero de inicio de la cadena y no hay asignación de memoria en montón. Simplemente establece el puntero de inicio y la longitud de la cadena estática, sin otras llamadas, ¡e incluso la asignación de memoria está en la pila! Por el std::stringcontrario, al crear std::string_viewun objeto, no hay una asignación de memoria dinámica y no hay un recorrido redundante de la cadena.
        mov     QWORD PTR [rbp-112], 23
        mov     QWORD PTR [rbp-104], OFFSET FLAT:.LC0

Complete de la siguiente manera:

.LC0:
        .string "this is a static string"
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 104
        mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC0
        movabs  rax, 2338328219631577204
        movabs  rdx, 7163384644223836257
        mov     QWORD PTR [rbp-64], rax
        mov     QWORD PTR [rbp-56], rdx
        movabs  rax, 29113321772053280
        mov     QWORD PTR [rbp-48], rax
        lea     rax, [rbp-33]
        mov     QWORD PTR [rbp-32], rax
        nop
        nop
        lea     rdx, [rbp-33]
        lea     rax, [rbp-96]
        mov     esi, OFFSET FLAT:.LC0
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
        lea     rax, [rbp-33]
        mov     rdi, rax
        call    std::__new_allocator<char>::~__new_allocator() [base object destructor]
        nop
        mov     QWORD PTR [rbp-112], 23
        mov     QWORD PTR [rbp-104], OFFSET FLAT:.LC0
        lea     rax, [rbp-96]
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        mov     eax, 0
        jmp     .L10
        mov     rbx, rax
        lea     rax, [rbp-33]
        mov     rdi, rax
        call    std::__new_allocator<char>::~__new_allocator() [base object destructor]
        nop
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L10:
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
.LC1:
        .string "basic_string: construction from null is not valid"

y std::stringla diferencia

Mira el siguiente std::stringejemplo de una cadena constante

#include <iostream>
#include <string>
using namespace std;
 
int main()
{
    
    
    char str_1[]{
    
     "Hello !!, string view" };
 
    string str_2{
    
     str_1 };
    string str_3{
    
     str_2 };
 
    return 0;
}

El código ensamblador es el siguiente

main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 120
        movabs  rax, 2387224940415575368
        movabs  rdx, 7382362297425403948
        mov     QWORD PTR [rbp-64], rax
        mov     QWORD PTR [rbp-56], rdx
        movabs  rax, 32487705556775535
        mov     QWORD PTR [rbp-48], rax
        lea     rax, [rbp-25]
        mov     QWORD PTR [rbp-24], rax
        nop
        nop
        lea     rdx, [rbp-25]
        lea     rcx, [rbp-64]
        lea     rax, [rbp-96]
        mov     rsi, rcx
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
        lea     rax, [rbp-25]
        mov     rdi, rax
        call    std::__new_allocator<char>::~__new_allocator() [base object destructor]
        nop
        lea     rdx, [rbp-96]
        lea     rax, [rbp-128]
        mov     rsi, rdx
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) [complete object constructor]
        mov     ebx, 0
        lea     rax, [rbp-128]
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        lea     rax, [rbp-96]
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        mov     eax, ebx
        jmp     .L12
        mov     rbx, rax
        lea     rax, [rbp-25]
        mov     rdi, rax
        call    std::__new_allocator<char>::~__new_allocator() [base object destructor]
        nop
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
        mov     rbx, rax
        lea     rax, [rbp-96]
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L12:
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
.LC0:
        .string "basic_string: construction from null is not valid"

Como puede ver en el código ensamblador: para ver "Hola !!, vista de cadena" dos veces, la sobrecarga se realiza dos veces std ::string en la memoria . Pero la tarea aquí es leer la cadena ("¡¡Hola!!, vista de cadena"), no es necesario escribir en ella. Por lo tanto, se realizan múltiples asignaciones de memoria solo para mostrar una cadena.

En std::stringcambio, std::string_viewlos objetos tienen las siguientes características:

  • La secuencia de caracteres subyacente es de solo lectura . Ninguna operación modifica los caracteres subyacentes. Solo puede asignar un nuevo valor, intercambiar valores y reducir la vista a una subsecuencia de secuencias de caracteres.
  • No se garantiza que las secuencias de caracteres finalicen con un carácter nulo . Por lo tanto, una vista de cadena no es un flujo de bytes terminado en nulo (NTBS) .
  • data()El valor devuelto puede ser nullptr. data()Por ejemplo, la llamada volverá después de inicializar una vista de cadena con el constructor predeterminado nullptr.
  • No hay soporte de asignador .
    Debido a que puede devolver nullptr, y puede que no termine en nulo, siempre debe usar getlength antes de usar operator[]o (a menos que ya sepa la longitud).data()size()
#include <iostream>
using namespace std;
#include <string_view>
 
// Driver code
int main()
{
    
    
    string_view str_1{
    
     "Hello !!, string view" };
    string_view str_2{
    
     str_1 };
    string_view str_3{
    
     str_2 };
    return 0;
}

Miremos el código ensamblador nuevamente, si se sentirá mucho más refrescante. El resultado es el mismo, pero ya no se crea una copia de la cadena "Hola !!, vista de cadena". std::string_viewes una vista de una cadena, no es necesario crear una copia. Cuando copiamos std::string_view, la nueva observación es la misma cadena std::string_viewque se copió , lo que significa que no se crean más copias de la cadena, son solo vistas de la cadena existente "¡¡Hola!!, vista de cadena".std::string_viewstr

.LC0:
        .string "Hello !!, string view"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48
        lea     rax, [rbp-16]
        mov     esi, OFFSET FLAT:.LC0
        mov     rdi, rax
        call    std::basic_string_view<char, std::char_traits<char> >::basic_string_view(char const*) [complete object constructor]
        mov     rax, QWORD PTR [rbp-16]
        mov     rdx, QWORD PTR [rbp-8]
        mov     QWORD PTR [rbp-32], rax
        mov     QWORD PTR [rbp-24], rdx
        mov     rax, QWORD PTR [rbp-32]
        mov     rdx, QWORD PTR [rbp-24]
        mov     QWORD PTR [rbp-48], rax
        mov     QWORD PTR [rbp-40], rdx
        mov     eax, 0
        leave
        ret

Escenario de aplicación

Las vistas de cadenas tienen dos aplicaciones principales :

  • Es posible que haya asignado o asignado secuencias de caracteres o datos de cadena y desee utilizar estos datos sin asignar más memoria . Los ejemplos típicos son los archivos mapeados en memoria o el manejo de subcadenas de texto largo.
  • Es posible que desee mejorar el rendimiento de las funciones/operaciones que toman cadenas como parámetros y las usan de manera de solo lectura , y estas funciones/operaciones no requieren un carácter nulo final.
    Una forma especial de esto es querer manejar objetos literales de cadena stringcomo :API
std::string_view hello{
    
    "hello world"};

La primera aplicación generalmente significa que solo se deben pasar vistas de cadena, sin embargo, la lógica del programa debe garantizar que la secuencia de caracteres subyacente aún sea válida (es decir, los archivos asignados a la memoria no se desasignan a mitad de camino ). También puede utilizar una vista de cadena para inicializar o asignar en cualquier momento std::string.

Tenga cuidado de no usar vistas de cadenas como "mejores string". Esto puede causar problemas de rendimiento y algunos errores de tiempo de ejecución.

buen lugar

El siguiente es un ejemplo del uso de una vista de cadena como una cadena de solo lectura. Este ejemplo define una función que antepone la vista de cadena entrante y luego imprime los elementos en una colección:

#include <string_view>

template<typename T>
void printElems(const T& coll, std::string_view prefix = {
    
    })
{
    
    
    for (const auto& elem : coll) {
    
    
        if (prefix.data()) {
    
        // 排除nullptr
            std::cout << prefix << ' ';
        }
        std::cout << elem << '\n';
    }
}

Aquí, declarar el parámetro de la función como puede reducir una llamada para asignar memoria de montón en comparación std::string_viewcon declarar como . La situación exacta depende de si se pasan cadenas cortas y si se utiliza la optimización de cadenas cortas (SSO). Por ejemplo, si declaramos así:std::string

template<typename T>
void printElems(const T& coll, const std::string& prefix = {
    
    });

Luego, al pasar un literal de cadena, la llamada creará un temporalstring , que se asignará una vez en el montón, a menos que se use la optimización de cadenas cortas. Al usar una vista de cadena, no se asignará memoria porque la vista de cadena solo apunta al literal de cadena.

Tenga en cuenta quedata() las vistas de cadenas con valores desconocidos deben verificarse para excluirlas antes de usar nullptr. Aquí, para evitar escribir un delimitador de espacio adicional, debe marcar nullptr. Una vista de cadena con un valor nullptrno debe escribir ningún carácter al escribir en el flujo de salida.

Otro ejemplo es usar una vista de cadena como una cadena de solo lectura para mejorar std::optional<>el asInt()ejemplo. El método mejorado es declarar el parámetro como una vista de cadena :

#include <optional>
#include <string_view>
#include <charconv> // for from_chars()
#include <iostream>

// 尝试将string转换为int:
std::optional<int> asInt(std::string_view sv)
{
    
    
    int val;
    // 把字符序列读入int:
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), val);
    // 如果有错误码,就返回空值:
    if (ec != std::errc{
    
    }) {
    
    
        return std::nullopt;
    }
    return val;
}

int main()
{
    
    
    for (auto s : {
    
    "42", "  077", "hello", "0x33"}) {
    
    
        // 尝试把s转换为int,并打印结果:
        std::optional<int> oi = asInt(s);
        if (oi) {
    
    
            std::cout << "convert '" << s << "' to int: " << *oi << "\n";
        }
        else {
    
    
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
}

Cambiar asInt()el parámetro de a una vista de cadena requirió muchas modificaciones. En primer lugar, ya no es necesario std::stoi()convertir a un número entero, porque stoi()los parámetros son relativamente costosos de crear stringbajo . En su lugar, pasamos rangos de caracteres a nuevas funciones de biblioteca estándar . Esta función toma dos punteros de caracteres como parámetros, que representan el inicio y el final de la secuencia de caracteres respectivamente, y los convierte. Tenga en cuenta que esto significa que podemos evitar tratar con vistas de cadenas vacías por separado , en cuyo caso devuelve 0, ya que from to es un rango vacío válido (cualquier tipo de puntero admite la suma con 0 y no tendrá ningún efecto). Devuelve una estructura de tipo que tiene dos miembros: un puntero al primer carácter que no se ha procesado y un miembro de tipo que no representa errores. Por lo tanto, después de la inicialización con miembros en el valor de retorno (usando enlace estructurado), la siguiente verificación regresará en caso de falla de conversión :string viewstringstd::from_chars()data()nullptr
size()nullptrnullptr+0
std::from_chars()std::from_chars_resultptrecstd:errcstd::errc{}ececnullopt

if (ec != std::errc{
    
    }) {
    
    
    return std::nullopt;
}

El uso de vistas de cadenas también puede mejorar significativamente el rendimiento de la ordenación de subcadenas .

lado dañino

A menudo, los "objetos inteligentes", como los punteros inteligentes, son más seguros (al menos no más peligrosos) que las características del lenguaje correspondiente. Por lo tanto, puede tener la impresión de que una vista de cadena es una referencia a una cadena, que debería ser más segura que una referencia de cadena, o al menos igual de segura. Desafortunadamente, este no es el caso, y las vistas de cadenas son mucho más peligrosas que las referencias de cadenas o los punteros inteligentes . Se comportan más como punteros char nativos.

No asigne cadenas temporales a las vistas de cadenas

Considere declarar una función que devuelva una nueva cadena:

std::string retString();

Siempre es seguro usar valores de retorno:

  • Es seguro usar el valor devuelto para inicializar un objeto stringo declarado:auto
std::string s1 = retString();   // 安全
  • La inicialización de una referencia constante stringcon el valor de retorno también es segura solo para uso local. Porque las referencias extienden la vida útil del valor devuelto:
std::string& s2 = retString();  // 编译期ERROR(缺少const)

const std::string& s3 = retString();  // s3延长了返回的string的生命周期
std::cout << s3 << '\n';        // OK
auto&& s4 = retString();        // s4延长了返回的string的生命周期
std::cout << s4 << '\n';        // OK

Las vistas de cadenas no son tan seguras, no copian ni extienden la vida útil del valor devuelto:

std::string_view sv = retString(); // sv不延长返回值的生命周期
std::cout << sv << '\n';           // 运行时ERROR:返回值已经被销毁

Aquí, la cadena devuelta ya se destruye al final de la primera declaración, por lo que usar una vista de cadena que apunte a ella svdará como resultado un error de tiempo de ejecución indefinido.

El problema es similar a llamadas como:

const char* p = retString().c_str();

o:

auto p = retString().c_str();

Por lo tanto, se debe tener mucho cuidado al usar la vista de cadena devuelta:

// 非常危险:
std::string_view substring(const std::string& s, std::size_t idx = 0);

// 因为:
auto sub = substring("very nice", 5); // 返回临时string的视图
                                      // 但是临时string已经被销毁了
std::cout << sub << '\n';     // 运行时ERROR:临时字符串s已经被销毁

No devuelva una cadena cuando el tipo de valor de retorno es una vista de cadena

Es muy peligroso devolver una cadena cuando el tipo de valor devuelto es una vista de cadena. Por lo tanto, no deberías escribir algo como esto:

class Person {
    
    
    std::string name;
public:
    ...
    std::string_view getName() const {
    
      // 不要这么做
        return name;
    }
};

Esto se debe a que el siguiente código generará un error de tiempo de ejecución y dará como resultado un comportamiento indefinido:

Person createPerson();
auto n = createPerson().getName();  // OOPS:delete临时字符串
std::cout << "name: " << n << '\n'; // 运行时错误

Si getName()lo cambia para devolver un valor o referencia de tipo cadena, no habrá tal problema, porque nse convertirá en una copia del valor devuelto.

Las plantillas de función deben usarse autocomo tipo de retorno

Tenga en cuenta que es muy común devolver cadenas sin darse cuenta como vistas de cadena. Por ejemplo, las siguientes dos funciones parecerían útiles individualmente:

// 为字符串视图定义+,返回string:
std::string operator+ (std::string_view sv1, std::string_view sv2) {
    
    
    return std::string(sv1) + std::string(sv2);
}

// 泛型连接函数
template<typename T>
T concat (const T& x, const T& y) {
    
    
    return x + y;
}

Sin embargo, usarlos juntos puede conducir fácilmente a errores de tiempo de ejecución:

std::string_view hi = "hi";
auto xy = concat(hi, hi);   // xy是std::string_view
std::cout << xy << '\n';    // 运行时错误:指向的string已经被销毁了

Es probable que dicho código se escriba sin querer. El verdadero problema es con concat()el tipo de retorno. Si dejó el tipo de retorno en la deducción automática en tiempo de compilación, el ejemplo anterior se xyinicializaría en std::string:

// 改进的泛型连接函数
template<typename T>
auto concat (const T& x, const T& y) {
    
    
    return x + y;
}

No use vistas de cadenas en la cadena de llamadas para inicializar cadenas

El uso de una vista de cadena en una cadena de llamadas donde se necesita una cadena a la mitad o al final puede ser contraproducente. Por ejemplo, si define una clase con el siguiente constructor Person:

class Person {
    
    
    std::string name;
public:
    Person (std::string_view n) // 不要这样做
        : name {
    
    n} {
    
    
    }
    ...
};

Pasar un literal de cadena o una cadena que se usará más adelante está bien:

Person p1{
    
    "Jim"};       // 没有性能开销
std::string s = "Joe";
Person p2{
    
    s};           // 没有性能开销

Sin embargo, el uso movede a stringincurrirá en una sobrecarga innecesaria. Debido a que la stringvista de cadena entrante primero debe convertirse implícitamente en una vista de cadena, y luego se usará para crear una nueva `cadena``, que asignará memoria nuevamente:

Person p3{
    
    std::move(s)};  // 性能开销:move被破坏

No lo use aquí std::string_view. Pasar parámetros por valor y poner valores moveen miembros sigue siendo la mejor solución (a menos que desee duplicar la sobrecarga):

class Person {
    
    
    std::string name;
public:
    Person (std::string n) : name{
    
    std::move(n)} {
    
    
    }
    ...
};

Si tenemos que crear/iniciar uno string, crearlo directamente como parámetro nos permite disfrutar de todas las optimizaciones posibles a la hora de pasar parámetros. Al final, solo necesita movepasar los parámetros, y la sobrecarga de esta operación es muy pequeña. Entonces, si inicializamos una cadena con una función auxiliar que devuelve una cadena temporal:

std::string newName()
{
    
    
    ...
    return std::string{
    
    ...};
}
Person p{
    
    newName()};

La omisión obligatoria del atributo de copia stringaplazará el nuevo proceso de materialización hasta que el valor se pase al constructor. En el constructor tenemos un objeto llamado n, que es un objeto (un lvalue generalizado (glvalue)string ) con una dirección de memoria . Luego ponga el valor de este objeto en el miembro .movename

Resumen de Uso seguro de vistas de cadenas

En resumen, utilícelo con cuidadostd::string_view , lo que significa que debe ajustar su estilo de codificación de la siguiente manera:

  • No lo use en aquellos que pasan argumentos stringa .APIstring view
  • No string viewinicialice stringmiembros con parámetros formales.
  • No establezca stringcomo string viewel final de la cadena de llamadas.
  • No string viewregreses
  • A menos que solo esté reenviando los parámetros de entrada, o puede marcarlo como peligroso, por ejemplo, nombrándolo para reflejar el peligro.
  • Una plantilla de función nunca debe devolver el tipo T de un parámetro genérico .
  • En su lugar, el autotipo de devolución.
  • Nunca inicialice con un valor de retorno string view.
  • No asigne el valor de retorno de una plantilla de función que devuelve un tipo genérico a auto.
  • Esto significa que el principio AAA ( Siempre auto(Casi siempre automático) ) no se aplica string view.
    Si las reglas son demasiado complicadas o demasiado difíciles de seguir, no las use en absoluto std::string_view(a menos que sepa lo que está haciendo).

tipo y funcionamiento

tipo

En el archivo de encabezado <string_view>, C++la biblioteca estándar basic_string_view<>proporciona muchas especializaciones para:

  • Una clase es una plantilla para una especialización del std::string_viewtipo de carácter predefinido :char
namespace std {
    
    
    using string_view = basic_string_view<char>;
}
  • Para las cadenas que utilizan juegos de caracteres amplios, como Unicodealgunos juegos de caracteres asiáticos, se definen tres tipos adicionales:
namespace std {
    
    
      	using u8string_view  = basic_string_view<char8_t>;/*c++20起*/
  		using u16string_view = basic_string_view<char16_t>;
  		using u32string_view = basic_string_view<char32_t>;
  		using wstring_view   = basic_string_view<wchar_t>;
}

En lo que sigue, no importa qué vista de cadena se utilice. El uso y los problemas de estas diversas clases de vistas de cadenas son los mismos, porque todas tienen la misma interfaz. Por lo tanto, "vista de cadena" significa cualquier string viewtipo: string_view, u8string_view , u16string_view, u32string_view, wstring_view.

funcionar

La tabla Operaciones para vistas de cadenas enumera todas las operaciones para vistas de cadenas.

funcionar Efecto
Constructor Crear o copiar una vista de cadena
incinerador de basuras destruir una vista de cadena
= asignar nuevo valor
swap() Intercambia los valores de dos vistas de cadenas
==、!=、<、<=、>,>=、compare() comparar vista de cadena
empty() Devuelve si la vista de cadenas está vacía
size()、length() Devuelve el número de caracteres.
max_size() Devuelve el máximo número de caracteres posible
[]、at() acceder a un personaje
front()、back() acceder al primer o último carácter
<< escribe un valor en el flujo de salida
copy() copiar o escribir contenido en una matriz de caracteres
data() matriz de caracteres return nullptro const (no terminada en nulo)
función de búsqueda Buscar una subcadena o carácter
begin()、end() Proporciona compatibilidad con iteradores comunes.
cbegin()、cend() Proporciona soporte de iterador constante
rbegin()、 rend() Proporciona soporte de iterador inverso
crbegin()、crend() Proporciona soporte de iterador inverso constante
substr() devolver subcadena
remove_prefix() eliminar los primeros caracteres
remove_suffix() eliminar algunos caracteres del final
hash<> El tipo de objeto de función que calcula el valor hash

Todas las operaciones de visualización de cadenas excepto remove_prefixy también están disponibles. Sin embargo, las garantías de las operaciones correspondientes pueden ser ligeramente diferentes, y el valor de retorno puede o no terminar en nulo.remove_suffix()std::stringdata()nullptr

estructura

    constexpr basic_string_view() noexcept;
    constexpr basic_string_view(const basic_string_view&) noexcept = default;
    constexpr basic_string_view(const CharT* str);
    constexpr basic_string_view(const CharT* str, size_type len);

Puede crear una vista de cadena de varias maneras: con el constructor predeterminado, con un constructor de función de copia, desde una matriz de caracteres sin procesar (terminada en nulo o de longitud especificada), desde un constructor o desde un literal con std::stringsufijo sv. Sin embargo, tenga en cuenta lo siguiente:

  • constructor por defecto constexpr basic_string_view() noexcept;. Construye un std::basic_string_view vacío. Después de la construcción, data() es igual a nullptr y size() es igual a 0. Por lo tanto, operator[]la llamada no tendrá efecto.
std::string_view sv;
auto p = sv.data();     // 返回nullptr
std::cout << sv[0];     // ERROR:没有有效的字符
  • Al inicializar una vista de cadena con un flujo de bytes terminado en nulo, el tamaño final es la '\0'cantidad de caracteres no incluidos y no es válido indexar la posición del carácter nulo:
    constexpr basic_string_view(const CharT* str);
    constexpr basic_string_view(const CharT* str, size_type len);
std::string_view sv{
    
    "hello"};
std::cout << sv;        // OK
std::cout << sv.size(); // 5
std::cout << sv.at(5);  // 抛出std::out_of_range异常
std::cout << sv[5];     // 未定义行为
std::cout << sv.data(); // OOPS:恰好sv后边还有个'\0',所以能直接输出字符指针
  • Puede especificar la cantidad de caracteres pasados ​​para inicializar caracteres nulos como parte de la vista de cadena:
#include <iostream>
#include <iterator>  // For std::size
#include <string_view>

int main() {
    
    
    // No null-terminator.
    char vowels[]{
    
    'a', 'e', 'i', 'o', 'u'};
    // 不是以null结尾。我们需要手动传递长度
    // 因为vowels是一个数组,所以可以使用std::size来获取它的长度。
    std::string_view str{
    
    vowels, std::size(vowels)};
    std::cout << str << '\n';  // 这是安全的。std::cout知道如何打印std::string_view。

    std::string_view sv{
    
    "hello", 6};      // NOTE:包含'\0'的6个字符
    std::cout << sv.size() << std::endl;  // 6
    std::cout << sv.at(5) <<std::endl;    // OK,打印出'\0'的值
    std::cout << sv[5] <<std::endl;;      // OK,打印出'\0'的值
    std::cout << sv.data()<<std::endl;;   // OK
    return 0;
}
  • stringPara crear una vista de cadena desde un , hay un std::stringoperador de conversión implícito definido para . Nuevamente, stringhay una garantía de un carácter nulo después del último carácter, las vistas de cadenas no tienen esta garantía:
std::string s = "hello";
std::cout << s.size();      // 5
std::cout << s.at(5);       // 抛出std::out_of_range异常
std::cout << s[5];          // OK,打印出'\0'的值
std::cout << s.data();      // OK

std::string_view sv{
    
    s};
std::cout << sv.size();     // 5
std::cout << sv.at(5);      // 抛出std::out_of_range异常
std::cout << sv[5];         // 未定义行为
std::cout << sv.data();     // OOPS:只有当sv后有'\0'时才能正常工作

El código de preprocesamiento es el siguiente:

  std::basic_string<char> s = std::basic_string<char>("hello", std::allocator<char>());
  ...
  std::basic_string_view<char, std::char_traits<char> > sv = {
    
    static_cast<std::basic_string_view<char, std::char_traits<char> >>(s.operator std::basic_string_view<char, std::char_traits<char> >())};
  ...

El código ensamblador es el siguiente:

;std::string s = "hello";
    	 lea     rdx, [rbp-25]
        lea     rax, [rbp-64]
        mov     esi, OFFSET FLAT:.LC0
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)


;std::string_view sv{
    
    s};
        lea     rax, [rbp-64]
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator std::basic_string_view<char, std::char_traits<char> >() const
        mov     QWORD PTR [rbp-80], rax
        mov     QWORD PTR [rbp-72], rdx
        lea     rax, [rbp-64]
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        mov     eax, 0
        jmp     .L10
  • Compuesto a partir de un literal de matriz de caracteresstring_view
  inline namespace literals {
    
    
  inline namespace string_view_literals {
    
    
    // suffix for basic_string_view literals
    constexpr string_view    operator""sv(const char* str, size_t len) noexcept;
    constexpr u8string_view  operator""sv(const char8_t* str, size_t len) noexcept;
    constexpr u16string_view operator""sv(const char16_t* str, size_t len) noexcept;
    constexpr u32string_view operator""sv(const char32_t* str, size_t len) noexcept;
    constexpr wstring_view   operator""sv(const wchar_t* str, size_t len) noexcept;
  }
  }
  • str- puntero al inicio del literal de matriz de caracteres sin decorar
  • len- La longitud de un literal de matriz de caracteres sin decorar
    Dado que el sufijo svdefine los operadores literales, se puede crear una vista de cadena como esta:
using namespace std::literals;
auto s = "hello"sv;

Tenga en cuenta que std::char_traitslos miembros se han cambiado constexprpara que pueda inicializar la vista de cadena con un literal de cadena en tiempo de compilación:

constexpr string_view hello = "Hello World!";

El código de preprocesamiento es el siguiente:

  std::basic_string_view<char, std::char_traits<char> > s = std::operator""sv("hello", 5UL);

Ejemplo completo:

#include <iostream>
#include <string_view>
#include <typeinfo>
 
void print_each_character(const std::string_view sw)
{
    
    
    for (char c : sw)
        std::cout << (c == '\0' ? '@' : c);
    std::cout << '\n';
}
 
int main()
{
    
    
    using namespace std::literals;
 
    std::string_view s1 = "abc\0\0def";
    std::string_view s2 = "abc\0\0def"sv;
 
    std::cout << "s1.size(): " << s1.size() << "; s1: ";
    print_each_character(s1);
    std::cout << "s2.size(): " << s2.size() << "; s2: ";
    print_each_character(s2);
 
    std::cout << "substr(1, 4): " << "abcdef"sv.substr(1, 4) << '\n';
 
    auto value_type_info = []<typename T>(T)
    {
    
    
        using V = typename T::value_type;
        std::cout << "sizeof " << typeid(V).name() << ": " << sizeof(V) << '\n';
    };
 
    value_type_info("char A"sv);
    value_type_info(L"wchar_t ∀"sv);
    value_type_info(u8"char8_t ∆"sv);
    value_type_info(u"char16_t ∇"sv);
    value_type_info(U"char32_t ∃"sv);
    value_type_info(LR"(raw ⊞)"sv);
}

El resultado de la operación es el siguiente:

s1.size(): 3; s1: abc
s2.size(): 8; s2: abc@@def
substr(1, 4): bcde
sizeof c: 1
sizeof w: 4
sizeof c: 1
sizeof Ds: 2
sizeof Di: 4
sizeof w: 4

carácter nulo final

En general, los valores de vista de cadena no terminan en nulo , e incluso podrían serlo nullptr. Por lo tanto, siempre debe verificar los caracteres antes de acceder a las vistas de cadenas size()(a menos que sepa la longitud). Sin embargo, puede encontrar dos escenarios especiales que pueden ser confusos:

  • Puede asegurarse de que los valores de vista de cadena terminen en nulo, aunque los caracteres nulos no formen parte del valor. Esto sucede cuando inicializa una vista de cadena con un literal de cadena:
std::string_view sv1{
    
    "hello"};      // sv1的结尾之后有一个'\0'

Aquí, el estado de la vista de cadenas puede ser confuso. Este estado está bien definido, por lo que se puede utilizar como una secuencia de caracteres terminada en nulo. Sin embargo, esto solo está bien definido si sabemos explícitamente que esta vista de cadena va seguida de un carácter nulo que no se pertenece a sí mismo.

  • Puede estar seguro '\0'de ser parte de la vista de cadena. Por ejemplo:
std::string_view sv2{
    
    "hello", 6};   // 参数6使'\0'变为值的一部分

Aquí, el estado de la vista de cadena puede ser confuso: la impresión parece que tiene solo 5 caracteres, pero su estado real contiene 6 caracteres (el carácter nulo se convierte en parte del valor, lo que lo hace más como una cadena de dos segmentos ( vista)( binary string(view))).
La pregunta es cuál de los dos es mejor para garantizar que haya un carácter nulo después de la vista de cadena. Tiendo a no usar ninguno de estos enfoques , pero a partir de ahora, C++no hay mejor manera de hacerlo. Parece que también necesitamos un tipo de vista de cadena que esté garantizado que termine en nulo y no necesite copiar caracteres (como std::string). En ausencia de una mejor alternativa, las vistas de cadenas son el único camino a seguir.

picadillo

C++La biblioteca estándar garantiza que las cadenas con el mismo valor y las vistas de cadenas tengan el mismo valor hash.

  template<class T> struct hash;
  template<> struct hash<string_view>;
  template<> struct hash<u8string_view>;
  template<> struct hash<u16string_view>;
  template<> struct hash<u32string_view>;
  template<> struct hash<wstring_view>;

std::hash Las especializaciones de plantilla para varias clases de cadenas permiten a los usuarios obtener hashes de cadenas.
Estos valores hash son iguales a std::basic_string_viewlos valores hash de las clases correspondientes: si Ses uno de estos tipos de cadena, SVes el tipo de vista de cadena correspondiente y ses Sun objeto de tipo std::hash<S>()(s) == std::hash<SV>()(SV(s)).

#include <iostream>
#include <string_view>
#include <unordered_set>
using namespace std::literals;
 
int main()
{
    
    
    std::cout << "\"A\"   #: " << std::hash<std::string_view>{
    
    }("A"sv) << '\n';
    std::cout << "L\"B\"  #: " << std::hash<std::wstring_view>{
    
    }(L"B"sv) << '\n';
    // std::cout << "u8\"C\" #: " << std::hash<std::u8string_view>{}(u8"C"sv) << '\n';/*c++20引入*/
    std::cout << "u\"D\"  #: " << std::hash<std::u16string_view>{
    
    }(u"D"sv) << '\n';
    std::cout << "U\"E\"  #: " << std::hash<std::u32string_view>{
    
    }(U"E"sv) << '\n';
    /*
    string_view类型的std::hash使得我们可以将这些视图类型存储在unordered_*关联容器中,例如unordered_set。
    但是要确保所引用的字符串的生命周期不少于容器的生命周期,即不会出现悬空引用。
    */
    std::unordered_set stars{
    
    "Rigel"sv, "Capella"sv, "Vega"sv, "Arcturus"sv};
 
    for (std::string_view const& s : stars)
        std::cout << s << ' ';
    std::cout << '\n';
}

La salida es la siguiente:

"A"   #: 6919333181322027406
L"B"  #: 11959850520494268278
u"D"  #: 312659256970442235
U"E"  #: 18073225910249204957
Arcturus Vega Capella Rigel 

Revisar

  • Puede asignar nuevos valores o intercambiar los valores de dos vistas de cadena:
#include <iostream>
#include <string_view>
#include <typeinfo>

using namespace std;

int main() {
    
    
    std::string_view sv1 = "hey";
    std::string_view sv2 = "world";
    cout << "before sv1 = " << sv1 << "; sv2 = " << sv2 << endl;
    sv1.swap(sv2); //交换
    cout << "After sv1 = " << sv1 << "; sv2 = " << sv2 << endl;
    sv2 = sv1;
    cout << "assign sv1 = " << sv1 << "; sv2 = " << sv2 << endl;
}

El resultado es el siguiente:

before sv1 = hey; sv2 = world
After sv1 = world; sv2 = hey
assign sv1 = world; sv2 = world
  • Puede omitir los caracteres iniciales o finales (es decir, mover la posición inicial hacia atrás o la posición final hacia adelante).
#include <iostream>
#include <string_view>
using namespace std;

int main() {
    
    
    std::string_view sv = "I like my kindergarten";
    sv.remove_prefix(2);
    sv.remove_suffix(8);
    std::cout << sv <<'\n';  // 打印出:like my kind

     std::string_view str{
    
    "Peach"};
    std::cout << str << '\n';
    // 忽略第一个字符
    str.remove_prefix(1);
    std::cout << str << '\n';
    // 忽略最后两个字符
    str.remove_suffix(2);
    std::cout << str << '\n';
}

producción

like my kind
Peach
each
ea

Tenga en cuenta que no hay operator+un soporte adecuado. por lo tanto:

std::string_view sv1 = "hello";
std::string_view sv2 = "world";
auto s1 = sv1 + sv2;    // ERROR

Un operando debe ser una cadena:

auto s2 = std::string(sv1) + sv2;   // OK

Tenga en cuentastring la conversión de tipo implícita que las vistas de cadenas no tienen , ya que esta operación es costosa porque asigna memoria. Por lo tanto, solo se pueden utilizar conversiones explícitas.

otro apoyo

En teoría, las vistas de cadena se pueden pasar en cualquier lugar donde se necesite pasar un valor de cadena, siempre que el receptor solo lea el valor y no requiera una terminación nula. Sin embargo, hasta ahora C++el estándar solo ha agregado soporte para los escenarios más importantes:

  • Las vistas de cadenas se pueden combinar cuando se trabaja con cadenas:
  • Puede crear uno desde una vista de cadena string(constructor yes explicit). Si la vista de cadena no tiene ningún valor ( data()devuelto nullptr), la cadena se inicializará para quedar vacía.
  • Puede usar vistas de cadenas como parámetros para operaciones de asignación, extensión, inserción, reemplazo, comparación o búsqueda en cadenas.
  • Hay una conversión de tipo implícita de stringa .string view
  • Puede pasarle una vista de cadena std::quotedy genera los parámetros entre comillas dobles. Por ejemplo:
using namespace std::literals;

auto s = R"(some\value)"sv;     // raw string view
std::cout << std::quoted(s);    // 输出:"some\value"
  • Puede inicializar, expandir o comparar rutas de sistemas de archivos usando vistas de cadenas.

C++Todavía falta otro soporte para las vistas de cadenas, como el de la biblioteca de expresiones regulares en la biblioteca estándar.

Uso de vistas de cadenas en la API

Las vistas de cadenas son baratas y cada una std::stringse puede usar como una vista de cadenas. Entonces parece que std::string_viewes un mejor tipo para usar como parámetro de cadena. Sin embargo, hay algunos detalles que son importantes...
Primero, el uso solo tiene sentido si la función usa los parámetros de acuerdo con las siguientes restricciones std::string_view:

  • No requiere un carácter nulo final. Este no es el caso cuando se pasa un argumento a una función C que toma un solo const char*argumento y ningún argumento de longitud.
  • No viola la vida útil de los parámetros pasados. Por lo general, esto significa que la función de recepción solo usará el valor entrante hasta que finalice su vida útil.
  • La función de llamada no debe cambiar la propiedad del carácter subyacente (como destruirlo, cambiar su valor o liberar su memoria).
  • Puede manejar nullptrel caso donde el valor del parámetro es .
    Tenga en cuenta que tener funciones std::stringsobrecargadas std::string_viewpuede causar ambigüedad:
void foo(const std::string&);
void foo(std::string_view);

foo("hello");   // ERROR:歧义

Finalmente, recuerde las advertencias mencionadas anteriormente:

  • No asigne cadenas temporales a las vistas de cadenas.
  • No devuelva una vista de cadena.
  • No utilice vistas de cadenas en la cadena de llamadas para inicializar o restablecer el valor de una cadena.
    Con estas consideraciones en mente, veamos algunos ejemplos de mejoras usando vistas de cadenas.

Usar vista de cadena en lugar de cadena

Considere el siguiente código:

// 带前缀输出时间点:
void print (const std::string& prefix, const std::chrono::system_clock::time_point& tp)
{
    
    
    // 转换为日历时间:
    auto rawtime{
    
    std::chrono::system_clock::to_time_t(tp)};
    std::string ts{
    
    std::ctime(&rawtime)};   // 注意:不是线程安全的

    ts.resize(ts.size()-1); // 跳过末尾的换行符

    std::cout << prefix << ts;
}

puede ser reemplazado con el siguiente código:

void print (std::string_view prefix, const std::chrono::system_clock::time_point& tp)
{
    
    
    auto rawtime{
    
    std::chrono::system_clock::to_time_t(tp)};
    std::string_view ts{
    
    std::ctime(&rawtime)};  // 注意:不是线程安全的

    ts.remove_suffix(1);    // 跳过末尾的换行符

    std::cout << prefix << ts;
}

La primera y más fácil mejora que se me ocurre es reemplazar las referencias de cadenas de solo lectura con prefixvistas de cadenas, siempre que no usemos operaciones que fallarían sin valor o sin terminador nulo. En este ejemplo, solo imprimimos el valor de la vista de cadena, lo cual está bien. Si la vista de cadena no tiene valor ( data()retorno nullptr), no generará ningún carácter. Tenga en cuenta que las vistas de cadena se pasan por valor, ya que copiar vistas de cadena es económico.

También ctime()usamos una vista de cadena para el valor devuelto internamente. Sin embargo, debemos tener cuidado de asegurarnos de que su valor todavía exista cuando lo usamos en una vista de cadena. En otras palabras, este valor solo es válido hasta la próxima vez ctime()o asctime()antes de la llamada. Por lo tanto, en un entorno de subprocesos múltiples, esta función causará problemas (el mismo problema ocurre cuando se usa una cadena). Si la función devuelve una cadena que concatena el prefijo y el punto de tiempo, el código podría verse así:

std::string toString (std::string_view prefix, const std::chrono::system_clock::time_point& tp)
{
    
    
    auto rawtime{
    
    std::chrono::system_clock_to_time_t(tp)};
    std::string_view ts{
    
    std::ctime(&rawtime)};  // 注意:不是线程安全的

    ts.remove_suffix(1);    // 跳过末尾的换行符
    return std::string{
    
    prefix} + ts; // 很不幸没有两个字符串视图的+运算符
}

Tenga en cuenta que no podemos simplemente operator+concatenar dos vistas de cadenas con . Tenemos que convertir uno de ellos a std::string(desafortunadamente, esta operación asigna memoria innecesaria). Si la vista de cadena no tiene ningún valor ( data()devuelto nullptr), la cadena estará vacía.

Otro ejemplo del uso de una vista de cadena es la clasificación de subcadenas mediante una vista de cadena y un algoritmo paralelo:

sort(std::execution::par, coll.begin(), coll.end(),
     // 译者注:此处原文是
     // sort(coll.begin(), coll.end(),
     // 应是作者笔误

     [] (const auto& a, const auto& b) {
    
    
         return std::string_view{
    
    a}.substr(2) < std::string_view{
    
    b}.substr(2);
     });

es mucho más rápido que stringla subcadena usando:

sort(std::execution::par, coll.begin(), coll.end(),
     [] (const auto& a, const auto& b) {
    
    
         return a.substr(2) < b.substr(2);
     });

Esto se debe a que stringla substr()función devuelve una nueva cadena que asigna su propia memoria.

constexpr std::string_view

#include <iostream>
#include <string_view>

int main()
{
    
    
    constexpr std::string_view s{
    
     "Hello, world!" };
    std::cout << s << '\n'; // s will be replaced with "Hello, world!" at compile-time

    return 0;
}

Veamos el código ensamblador.

.LC0:
        .string "Hello, world!"
main:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-16], 13
        mov     QWORD PTR [rbp-8], OFFSET FLAT:.LC0
        mov     eax, 0
        pop     rbp
        ret

Asigne la nueva cadena para std::string_view hacer std::string_view que se vea la nueva cadena.

No hace que la cadena anterior que se está viendo cambie de ninguna manera.

#include <iostream>
#include <string>
#include <string_view>

int main() {
    
    
    std::string name{
    
    "Alex"};
    std::string_view sv{
    
    name};
    std::cout << sv << '\n';  // 打印出 Alex

    sv = "John";  // sv现在是 "John" 的窗口了(不会改变原有的name的值)
    std::cout << sv << '\n';  // 打印出 John

    std::cout << name << '\n';  // 打印出 Alex

    return 0;
}

producción

Alex
John
Alex

std::string_view-> std::stringuna costosa conversión de

std::stringcopiaría su inicializador (que es costoso), por lo que las conversiones C++implícitas std::string_viewa std::string. Sin embargo, podemos std::string_viewcrear uno explícitamente usando un inicializador td::string, o convertir uno static_castexistente std::string_viewastd::string

#include <iostream>
#include <string>
#include <string_view>

void printString(std::string str)
{
    
    
    std::cout << str << '\n';
}

int main()
{
    
    
  std::string_view sv{
    
     "balloon" };

  std::string str{
    
     sv }; // okay, we can create std::string using std::string_view initializer

  // printString(sv);   // compile error: won't implicitly convert std::string_view to a std::string

  printString(static_cast<std::string>(sv)); // okay, we can explicitly cast a std::string_view to a std::string

  return 0;
}

referencia

[1] Análisis de C++17: implementación de string_view y rendimiento
[2] clase std::string_view en C++17
[3] std::string_view (parte 2)

Supongo que te gusta

Origin blog.csdn.net/MMTS_yang/article/details/130773313
Recomendado
Clasificación