c++ string c++11 stdstring c++03

¿Por qué C++ 11 hizo que std:: string:: data() agregara un carácter de terminación nulo?



c++11 stdstring (3)

Hay dos puntos para discutir aquí:

Espacio para el terminador nulo.

En teoría, una implementación de C ++ 03 podría haber evitado asignar espacio para el terminador y / o podría haber necesitado realizar copias (por ejemplo, no unsharing ).

Sin embargo, todas las implementaciones sanas asignaron espacio para el terminador nulo con el fin de admitir c_str() para comenzar, porque de lo contrario sería prácticamente inutilizable si no fuera una llamada trivial.

El propio terminador nulo.

Es cierto que algunas implementaciones very (1999), muy antiguas (2001) escribieron la /0 cada llamada c_str() .

Sin embargo, las implementaciones principales changed (2004) o ya eran así (2010) para evitar tal cosa antes de que se lanzara C ++ 11, de modo que cuando llegó el nuevo estándar, para muchos usuarios nada cambió.

Ahora, si una implementación de C ++ 03 debería haberlo hecho o no:

A mi me parece un desperdicio de ciclos de CPU.

Realmente no. Si llama a c_str() más de una vez, ya está perdiendo ciclos escribiéndolo varias veces. No solo eso, usted está jugando con la jerarquía de caché, lo que es importante tener en cuenta en los sistemas multiproceso. Recuerde que las CPUs multi-core / SMT comenzaron a aparecer entre 2001 y 2006 , lo que explica el cambio a implementaciones modernas que no son de CoW (incluso si existían sistemas con varias CPU un par de décadas antes).

La única situación en la que guardaría cualquier cosa es si nunca llamó a c_str() . Sin embargo, tenga en cuenta que cuando está redimensionando la cadena, de todos modos está reescribiendo todo. Un byte adicional será difícilmente medible.

En otras palabras, al no escribir el terminador en un nuevo tamaño, se está exponiendo a un peor rendimiento / latencia. Al escribirlo una vez, al mismo tiempo tiene que realizar una copia de la cadena, el comportamiento del rendimiento es mucho más predecible y evita las dificultades del rendimiento si termina utilizando c_str() , especialmente en sistemas de multiproceso.

Anteriormente, eso era el trabajo de std::string::c_str() , pero a partir de C ++ 11, data() también lo proporciona, por qué se agregó el carácter de terminación nula de c_str() a std::string::data() ? A mí me parece una pérdida de ciclos de CPU, en los casos en que el carácter de terminación nula no es relevante en absoluto y solo se usa data() , un compilador C ++ 03 no tiene que preocuparse por el terminador, y no tiene que escribir 0 en el terminador cada vez que se cambia el tamaño de la cadena, pero un compilador de C ++ 11, debido a la garantía de data() , debe perder los ciclos de escritura 0 cada vez que se cambia el tamaño de la cadena, por lo que dado que potencialmente hace que el código sea más lento, supongo que tenían alguna razón para agregar esa garantía, ¿qué era?


La premisa de la pregunta es problemática.

una clase de cadena tiene que hacer muchas cosas expansivas, como asignar memoria dinámica, copiar bytes de un búfer a otro, liberar la memoria subyacente y así sucesivamente.

¿Qué te molesta es una mala instrucción de montaje? Créeme, esto no afecta su rendimiento, incluso en un 0,5%.

Al escribir un tiempo de ejecución de lenguaje de programación, no puede ser obsesivo con cada instrucción de ensamblaje pequeño. Usted tiene que elegir sus batallas de optimización sabiamente, y la optimización de una terminación nula no notable no es una de ellas.

En este caso específico, ser compatible con C es mucho más importante que la terminación nula.


Ventajas del cambio:

  1. Cuando los data también garantizan el terminador nulo, el programador no necesita conocer detalles oscuros de las diferencias entre c_str y los data y, por lo tanto, evitaría que un comportamiento indefinido pase cadenas sin garantía de terminación nula a funciones que requieran terminación nula. Dichas funciones son ubicuas en las interfaces C, y las interfaces C se usan mucho en C ++.

  2. El operador de subíndices también se cambió para permitir el acceso de lectura a str[str.size()] . No permitir el acceso a str.data() + str.size() sería inconsistente.

  3. Si bien no inicializar el terminador nulo al cambiar el tamaño, etc. puede hacer que la operación sea más rápida, fuerza la inicialización en c_str que hace que la función sea más lenta. El caso de optimización que se eliminó no fue universalmente la mejor opción. Dado el cambio mencionado en el punto 2, esa lentitud también habría afectado al operador del subíndice, lo que ciertamente no habría sido aceptable para el desempeño. Como tal, el terminador nulo iba a estar allí de todos modos, y por lo tanto no habría un inconveniente en garantizar que así sea.

Detalle curioso: str.at(str.size()) todavía lanza una excepción.

PS Hubo otro cambio, que es garantizar que las cadenas tengan almacenamiento contiguo (por lo que los data se proporcionan en primer lugar). Antes de C ++ 11, las implementaciones podrían haber usado cadenas c_str y reasignarse al llamar a c_str . Ninguna implementación importante había elegido explotar esta libertad (que yo sepa).

PPS Las versiones anteriores de libstdc ++ de GCC, por ejemplo, aparentemente establecieron el terminador nulo solo en c_str hasta la versión 3.4. Vea la confirmación relacionada para más detalles.

¹ Un factor a esto es la concurrencia que se introdujo en el estándar de lenguaje en C ++ 11. La modificación no atómica concurrente es un comportamiento indefinido de la carrera de datos, por lo que los compiladores de C ++ pueden optimizar agresivamente y mantener las cosas en los registros. Por lo tanto, una implementación de biblioteca escrita en C ++ ordinario tendría UB para llamadas concurrentes a .c_str()

En la práctica (ver comentarios), tener varios subprocesos que escriban lo mismo no causaría un problema de corrección porque asm para CPU reales no tiene UB. Y las reglas de C ++ UB significan que múltiples subprocesos que realmente modifican un objeto std::string (aparte de llamar a c_str() ) sin sincronización es algo que el compilador + biblioteca puede asumir que no sucede.

Pero ensuciaría el caché y evitaría que otros subprocesos lo leyeran, por lo que sigue siendo una mala elección, especialmente para cadenas que potencialmente tienen lectores concurrentes. También impediría que .c_str() básicamente se optimice debido al efecto secundario de la tienda.