Почему С++ 11 заставил std :: string :: data() добавить нулевой завершающий символ? - программирование

Почему С++ 11 заставил std :: string :: data() добавить нулевой завершающий символ?

Раньше это было std::string::c_str(), но на С++ 11 data() также предоставляет его, почему в std::string::data() был c_str() нулевой завершающий символ c_str() std::string::data()? Мне кажется, что это пустая трата циклов ЦП, в тех случаях, когда нулевой завершающий символ вообще не имеет значения и используется только data(), компилятору С++ 03 не нужно заботиться о терминаторе, и не нужно записывать 0 в терминатор каждый раз при изменении размера строки, но компилятор С++ 11 из-за data() -null-гарантированно должен тратить циклы на запись 0 каждый раз при изменении размера строки, поэтому так как это потенциально делает код медленнее, я думаю, у них была причина добавить эту гарантию, что это было?

4b9b3361

Ответ 1

Преимущества изменения:

  1. Когда data также гарантируют нулевой терминатор, программисту не нужно знать непонятные детали различий между c_str и data и, следовательно, избегать неопределенного поведения при передаче строк без гарантии нулевого завершения в функции, которые требуют нулевого завершения. Такие функции вездесущи в интерфейсах C, и интерфейсы C используются в C++ очень часто.

  2. Оператор подписки также был изменен, чтобы разрешить доступ на чтение для str[str.size()]. str.data() + str.size() доступа к str.data() + str.size() будет противоречивым.

  3. Хотя не инициализация нулевого терминатора при изменении размера и т.д. Может ускорить эту операцию, она c_str инициализацию в c_str что c_str эту функцию¹. Случай оптимизации, который был удален, не всегда был лучшим выбором. Учитывая изменение, упомянутое в пункте 2., эта медлительность затронула бы и оператор индекса, что, безусловно, было бы неприемлемо для производительности. Таким образом, нулевой терминатор все равно будет там, и, следовательно, не будет недостатка в том, чтобы гарантировать, что это так.

Любопытная деталь: str.at(str.size()) прежнему выдает исключение.

PS Произошло еще одно изменение, заключающееся в том, что строки хранятся непрерывно (именно поэтому data предоставляются в первую очередь). До C++ 11 реализации могли использовать веревочные строки и перераспределять при вызове c_str. Ни одна крупная реализация не решила использовать эту свободу (насколько мне известно).

PPS Старые версии GCC libstd C++, например, по-видимому, устанавливали нулевой терминатор только в c_str до версии 3.4. Смотрите соответствующий коммит для деталей.


¹ Фактором этого является параллелизм, который был введен в стандарт языка в C++ 11. Одновременная неатомарная модификация - это неопределенное поведение гонки данных, поэтому компиляторам C++ разрешено агрессивно оптимизировать и хранить данные в регистрах. Таким образом, реализация библиотеки, написанная на обычном C++, будет иметь UB для одновременных вызовов .c_str()

На практике (см. Комментарии) наличие нескольких потоков, пишущих одну и ту же вещь, не вызовет проблемы с корректностью, поскольку для реальных процессоров asm не имеет UB. И C++ правила UB означают, что несколько потоков, фактически изменяющих объект std::string (кроме вызова c_str()) без синхронизации, - это то, что компилятор + библиотека может предположить, что этого не произойдет.

Но это могло бы испортить кеш и помешать другим потокам читать его, так что это по-прежнему плохой выбор, особенно для строк, которые могут иметь одновременные читатели. Также это остановило бы .c_str() от .c_str() оптимизации из-за побочного эффекта магазина.

Ответ 2

Здесь нужно обсудить два момента:

Пространство для нулевого терминатора

Теоретически реализация С++ 03 могла бы избежать выделения места для терминатора и/или, возможно, потребовалась бы для выполнения копий (например, без разделения).

Тем не менее, все разумные реализации выделяют пространство для нулевого терминатора для поддержки c_str() для начала, потому что в противном случае он был бы практически непригодным, если бы это не был тривиальный вызов.

Сам нуль-терминатор

Это правда, что некоторые очень (1999), очень старые реализации (2001) написали вызов \0 every c_str().

Тем не менее, основные реализации изменились (2004) или уже были такими (2010), чтобы избежать подобных вещей до выхода С++ 11, поэтому, когда появился новый стандарт, для многих пользователей ничего не изменилось.

Теперь, должна ли реализация С++ 03 сделать это или нет:

Мне это кажется пустой тратой процессорных циклов

На самом деле, нет. Если вы вызываете c_str() более одного раза, вы уже теряете циклы, записывая его несколько раз. Мало того, вы возитесь с иерархией кеша, что важно учитывать в многопоточных системах. Напомним, что многоядерные /SMT-процессоры начали появляться в период с 2001 по 2006 год, что объясняет переход на современные реализации без CoW (даже если за несколько десятилетий до этого существовали многоядерные системы).

Единственная ситуация, когда вы сохраняете что-либо, это если вы никогда не вызывали c_str(). Однако обратите внимание, что когда вы изменяете размер строки, вы все равно переписываете все. Дополнительный байт будет трудно измеримым.

Другими словами, не записывая терминатор при изменении размера, вы подвергаете себя худшей производительности/задержке. Записывая его один раз в одно и то же время, вы должны выполнить копирование строки, поведение производительности будет более предсказуемым, и вы избежите ловушек производительности, если в конечном итоге c_str() использовать c_str(), особенно в многопоточных системах.

Ответ 3

Предпосылка вопроса является проблематичной.

Строковый класс должен выполнять множество функций, таких как выделение динамической памяти, копирование байтов из одного буфера в другой, освобождение базовой памяти и так далее.

что вас огорчает, это одна паршивая инструкция по сборке mov? поверьте мне, это не повлияет на вашу производительность даже на 0,5%.

При написании среды выполнения на языке программирования вы не можете быть одержимы каждой маленькой инструкцией по сборке. Вы должны выбирать свои сражения оптимизации мудро, и оптимизация незаметного нулевого завершения не является одним из них.

В этом конкретном случае совместимость с C намного важнее, чем нулевое завершение.

Ответ 4

Итак, я пришел сюда, чтобы объяснить эту проблему. Эта проблема возникла. Поэтому мы должны избавиться от этой проблемы. Пожалуйста, кто-нибудь ответит на эту проблему. В противном случае вы бы умерли от греха.

Ответ 5

На самом деле, все наоборот.

До С++ 11 c_str() может стоить "дополнительных циклов", а также копии, чтобы обеспечить наличие нулевого терминатора в конце буфера.

Это было неудачно, особенно потому, что это можно исправить очень просто, практически без дополнительных затрат времени выполнения, просто включив нулевой байт в конце каждого буфера для начала. Только один дополнительный байт для выделения (и небольшая запись) без затрат времени выполнения на момент использования.

Как только вы это c_str(), c_str() буквально будет таким же, как data() по определению. Таким образом, "изменение" data() самом деле пришло бесплатно. Никто не добавляет лишний байт к результату data(); это уже там.

Помогает то, что большинство реализаций уже сделали это под С++ 03, чтобы избежать гипотетических затрат времени выполнения, приписываемых c_str().

Короче говоря, это почти наверняка стоило вам буквально ничего.