¿Cuándo debo usar string_view en una interfaz?

16

Estoy usando una biblioteca interna que fue diseñada para imitar una biblioteca C ++ propuesta , y en algún momento en los últimos años veo que su interfaz cambió de usar std::stringa string_view.

Así que obedientemente cambio mi código, para adaptarme a la nueva interfaz. Desafortunadamente, lo que tengo que pasar es un parámetro std :: string, y algo que es un valor de retorno std :: string. Entonces mi código cambió de algo como esto:

void one_time_setup(const std::string & p1, int p2) {
   api_class api;
   api.setup (p1, special_number_to_string(p2));
}

a

void one_time_setup(const std::string & p1, int p2) {
   api_class api;
   const std::string p2_storage(special_number_to_string(p2));
   api.setup (string_view(&p1[0], p1.size()), string_view(&p2_storage[0], p2_storage.size()));
}

Yo realmente no veo lo que este cambio me compró como el cliente de API, que no sea más código (posiblemente a meter la pata). La llamada a la API es menos segura (debido a que la API ya no posee el almacenamiento para sus parámetros), probablemente guardó el trabajo de mi programa 0 (debido a las optimizaciones de movimiento que los compiladores pueden hacer ahora), e incluso si ahorrara trabajo, eso solo sería un par de asignaciones que no se realizarán y nunca se realizarán después del inicio o en un gran bucle en alguna parte. No es para esta API.

Sin embargo, este enfoque parece seguir los consejos que veo en otros lugares, por ejemplo, esta respuesta :

Por otro lado, desde C ++ 17 debe evitar pasar un const std :: string & a favor de un std :: string_view:

Encuentro sorprendente este consejo, ya que parece estar abogando por el reemplazo universal de un objeto relativamente seguro por uno menos seguro (básicamente un puntero y una longitud glorificados), principalmente con fines de optimización.

Entonces, ¿cuándo se debe usar string_view y cuándo no?

TED
fuente
1
nunca debería tener que llamar al std::string_viewconstructor directamente, solo debe pasar las cadenas al método que toma un std::string_viewdirectamente y se convertirá automáticamente.
Mgetz
@Mgetz - Hmmm. No estoy (todavía) usando un compilador de C ++ 17 completo, así que tal vez esa sea la mayor parte del problema. Aún así, el código de muestra aquí parece indicar que es obligatorio, al menos al declarar uno.
TED
44
Vea mi respuesta, el operador de conversión está en el <string>encabezado y sucede automáticamente. Ese código es engañoso e incorrecto.
Mgetz
1
"con uno menos seguro" ¿cómo es un segmento menos seguro que una referencia de cadena?
CodesInChaos
3
@TED ​​La persona que llama puede liberar tan fácilmente la cadena a la que apunta su referencia como puede liberar la memoria a la que apunta el segmento.
CodesInChaos

Respuestas:

18
  1. ¿La funcionalidad que toma el valor necesita tomar posesión de la cadena? Si es así, use std::string(no const, no ref). Esta opción le da la opción de moverse explícitamente en un valor también si sabe que nunca más se usará en el contexto de la llamada.
  2. ¿La funcionalidad solo lee la cadena? Si es así, use std::string_view(const, no ref) esto se debe a que string_viewpuede manejar std::stringy char*fácilmente sin problemas y sin hacer una copia. Esto debería reemplazar todos los const std::string&parámetros.

Finalmente, nunca deberías necesitar llamar al std::string_viewconstructor como eres. std::stringtiene un operador de conversión que maneja la conversión automáticamente.

Mgetz
fuente
Solo para aclarar un punto, creo que un operador de conversión de este tipo también se ocuparía de los peores problemas de la vida, asegurándose de que el valor de la cadena RHS permanezca durante toda la llamada.
TED
3
@TED ​​si solo está leyendo el valor, el valor durará más que la llamada. Si toma posesión, entonces debe durar más que la llamada. De ahí por qué abordé ambos casos. El operador de conversión solo se ocupa de hacer que std::string_viewsea ​​más fácil de usar. Si un desarrollador lo usa mal en una situación de propiedad, es un error de programación. std::string_viewEs estrictamente no propietario.
Mgetz
¿Por qué const, non-ref? El parámetro const depende del uso específico, pero en general es razonable como no const. Y te perdiste 3. Puede aceptar rebanadas
v.oddou
¿Cuál es el problema de pasar const std::string_view &en lugar de const std::string &?
ceztko
@ceztko es completamente innecesario y agrega una indirección adicional al acceder a los datos.
Mgetz
15

A std::string_viewtrae algunos de los beneficios de a const char*a C ++: a diferencia de std::string, string_view

  • no posee memoria,
  • no asigna memoria,
  • puede apuntar a una cadena existente en algún desplazamiento, y
  • tiene un nivel menos de puntero indirecto que a std::string&.

Esto significa que string_view a menudo puede evitar copias, sin tener que lidiar con punteros sin formato.

En el código moderno, std::string_viewdebería reemplazar casi todos los usos de los const std::string&parámetros de función. Esto debería ser un cambio compatible con la fuente, ya que std::stringdeclara un operador de conversión a std::string_view.

El hecho de que una vista de cadena no ayude en su caso de uso específico en el que necesita crear una cadena de todos modos no significa que sea una mala idea en general. La biblioteca estándar de C ++ tiende a optimizarse para generalidad más que para conveniencia. El argumento "menos seguro" no se cumple, ya que no debería ser necesario crear la vista de cadena usted mismo.

amon
fuente
2
El gran inconveniente de std::string_viewes la ausencia de un c_str()método, lo que resulta en std::stringobjetos intermedios innecesarios que deben construirse y asignarse. Esto es especialmente un problema en las API de bajo nivel.
Matthias
1
@Matthias Ese es un buen punto, pero no creo que sea un gran inconveniente. Una vista de cadena le permite apuntar a una cadena existente en algún desplazamiento. Esa subcadena no puede ser terminada en cero, necesita una copia para eso. Una vista de cadena no le prohíbe hacer una copia. Permite muchas tareas de procesamiento de cadenas que se pueden realizar con iteradores. Pero tiene razón en que las API que necesitan una cadena C no se beneficiarán de las vistas. Una referencia de cadena puede ser más apropiada.
amon
@Matthias, ¿string_view :: data () no coincide con c_str ()?
Aelian
3
@Jeevaka, una cadena C debe tener terminación cero, pero los datos de una vista de cadena generalmente no tienen terminación cero porque apunta a una cadena existente. Por ejemplo, si tenemos una cadena abcdef\0y una vista de cadena que apunta a la cdesubcadena, no hay ningún carácter cero después de la ecadena original tiene un fallí. El estándar también señala: “data () puede devolver un puntero a un búfer que no tiene terminación nula. Por lo tanto, generalmente es un error pasar data () a una función que solo toma un const charT * y espera una cadena con terminación nula. ”
amon
1
@kayleeFrye_onDeck Los datos ya son un puntero de caracteres. El problema con las cadenas C no es obtener un puntero de caracteres, sino que una cadena C debe tener terminación nula. Vea mi comentario anterior para un ejemplo.
amon
8

Encuentro sorprendente este consejo, ya que parece estar abogando por el reemplazo universal de un objeto relativamente seguro por uno menos seguro (básicamente un puntero y una longitud glorificados), principalmente con fines de optimización.

Creo que esto es un poco malentendido el propósito de esto. Si bien es una "optimización", realmente deberías pensar en ello como liberarte de tener que usar a std::string.

Los usuarios de C ++ han creado docenas de diferentes clases de cadenas. Clases de cadenas de longitud fija, clases optimizadas para SSO con el tamaño del búfer como un parámetro de plantilla, clases de cadenas que almacenan un valor hash utilizado para compararlas, etc. Algunas personas incluso usan cadenas basadas en COW. Si hay algo que a los programadores de C ++ les encanta hacer, es escribir clases de cadena.

Y eso ignora las cadenas que son creadas y propiedad de las bibliotecas C. Desnudo char*, tal vez con un tamaño de algún tipo.

Entonces, si está escribiendo alguna biblioteca y toma una const std::string&, el usuario ahora tiene que tomar la cadena que estaba usando y copiarla en a std::string. Tal vez docenas de veces.

Si desea acceder a std::stringla interfaz específica de la cadena, ¿por qué debería copiar la cadena? Eso es un desperdicio.

Las razones principales para no tomar a string_viewcomo parámetro son:

  1. Si su objetivo final es pasar la cadena a una interfaz que toma una cadena terminada en NUL ( fopen, etc.). std::stringestá garantizado para ser NUL terminado; string_viewno lo es Y es muy fácil substraer una vista para que no tenga terminación NUL; sub-encadenar un std::stringcopiará la subcadena a cabo en un rango NUL terminados.

    Escribí un tipo especial de estilo string_view terminado en NUL para exactamente este escenario. Puede realizar la mayoría de las operaciones, pero no las que rompen su estado terminado por NUL (recorte desde el final, por ejemplo).

  2. Problemas de por vida. Si realmente necesita copiar eso std::stringo si la matriz de caracteres sobrevive a la llamada a la función, es mejor declarar esto por adelantado tomando un const std::string &. O simplemente std::stringcomo un parámetro de valor. De esa manera, si ya tienen una cadena de este tipo, puede reclamar su propiedad de inmediato, y la persona que llama puede moverse a la cadena si no necesita guardar una copia.

Nicol Bolas
fuente
¿Es esto cierto? La única clase de cadena estándar que conocía en C ++ antes de esto era std :: string. Hay algún soporte para usar char * 's como "cadenas" para la compatibilidad con C, pero casi nunca necesito usar eso. Claro, hay muchas clases de terceros definidas por el usuario para casi cualquier cosa que puedas imaginar, y las cadenas probablemente están incluidas en eso, pero casi nunca tengo que usarlas.
TED
@TED: El hecho de que "casi nunca tenga que usarlos" no significa que otras personas no los usen habitualmente. string_viewes un tipo de lengua franca que puede funcionar con cualquier cosa.
Nicol Bolas
3
@TED: Por eso dije "C ++ como entorno de programación", en lugar de "C ++ como lenguaje / biblioteca".
Nicol Bolas
2
@TED: " Entonces, ¿podría decir igualmente" C ++ como un entorno de programación tiene miles de clases de contenedor "? " Y lo hace. Pero puedo escribir algoritmos que funcionan con iteradores, y cualquier clase de contenedor que siga ese paradigma funcionará con ellos. Por el contrario, los "algoritmos" que pueden tomar cualquier conjunto contiguo de caracteres fueron mucho más difíciles de escribir. Con string_view, es fácil.
Nicol Bolas
1
@TED: Las matrices de caracteres son un caso muy especial. Son extremadamente comunes, y los diferentes contenedores de caracteres contiguos difieren solo en la forma en que administran su memoria, no en cómo iterar a través de los datos. Por lo tanto, tener un solo tipo de rango de lengua franca que pueda cubrir todos estos casos sin tener que emplear una plantilla tiene sentido. La generalización más allá de esto es la provincia de Range TS y las plantillas.
Nicol Bolas