Можно ли безопасно использовать std::string для двоичных данных в С++ 11?
В Интернете есть несколько сообщений, в которых предлагается использовать std::vector<unsigned char>
или что-то подобное для двоичных данных.
Но я предпочел бы вариант std::basic_string
для этого, поскольку он предоставляет множество удобных функций манипуляции с строкой. И AFAIK, так как С++ 11, стандарт гарантирует, что все известные реализации С++ 03 уже сделали: std::basic_string
хранит его содержимое в памяти.
На первый взгляд тогда std::basic_string<unsigned char>
может быть хорошим выбором.
Я не хочу использовать std::basic_string<unsigned char>
, потому что почти все функции операционной системы принимают только char*
, что делает явным приведение. Кроме того, строковые литералы const char*
, поэтому мне потребуется явный приведение к const unsigned char*
каждый раз, когда я назначил строковый литерал для моей двоичной строки, чего я также хотел бы избежать. Кроме того, функции для чтения и записи в файлы или сетевые буферы аналогично принимают указатели char*
и const char*
.
Это оставляет std::string
, что в основном является typedef для std::basic_string<char>
.
Единственная потенциальная оставшаяся проблема (которую я вижу) с использованием std::string
для двоичных данных заключается в том, что std::string
использует char
(который может быть подписан).
char
, signed char
и unsigned char
- три разных типа, а char
может быть либо без знака, либо подписанным.
Итак, когда фактическое значение байта 11111111b
возвращается из std::string:operator[]
как char, и вы хотите проверить его значение, его значение может быть либо 255
(если char
не указано), либо это может быть "что-то отрицательное" (если char
подписано, в зависимости от вашего числа).
Аналогично, если вы хотите явно добавить фактическое значение байта 11111111b
в std::string
, просто добавление (char) (255)
может быть определено реализацией (и даже поднять сигнал), если char
подписан, а int
to char
приводит к переполнению.
Итак, есть ли безопасный способ обойти это, что делает std::string
двоично-безопасным снова?
В § 3.10/15 говорится:
Если программа пытается получить доступ к сохраненному значению объекта через значение gl другого, чем одно из следующих типов, поведение undefined:
- [...]
- тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
- [...]
- a char или неподписанный char тип.
Что, если я правильно понимаю, похоже, позволяет использовать указатель unsigned char*
для доступа и управления содержимым std::string
и делает это также хорошо определенным. Он просто переинтерпретирует бит-шаблон как unsigned char
без каких-либо изменений или потери информации, а именно потому, что для представления значения должны использоваться все биты в char
, signed char
и unsigned char
.
Затем я мог бы использовать эту интерпретацию std::string
std::string
в качестве средства доступа и изменения значений байтов в диапазоне [0, 255]
в четко и переносимом виде независимо от подписанности char
.
Это должно решить любые проблемы, связанные с потенциально подписанным char
.
Правильны ли мои предположения и выводы?
Кроме того, интерпретация unsigned char*
одного и того же шаблона бита (т.е. 11111111b
или 10101010b
) гарантирована одинаково для всех реализаций? Иначе говоря, стандартная ли гарантия гласит, что "просматривая глаза unsigned char
", один и тот же шаблон бит всегда приводит к одному и тому же числовому значению (если число бит в байте одинаковое)?
Можете ли я безопасно (т.е. без каких-либо undefined или определенных реализацией) использовать std::string
для хранения и обработки двоичных данных в С++ 11?
Ответы
Ответ 1
Преобразование static_cast<char>(uc)
, где uc
имеет тип unsigned char
, всегда справедливо: согласно 3.9.1 [basic.fundamental] представление char
, signed char
и unsigned char
идентично с char
, идентичным одному из двух других типов:
Объекты, объявленные как символы (char), должны быть достаточно большими, чтобы хранить любой элемент базового набора символов реализаций. Если символ из этого набора сохраняется в символьном объекте, целочисленное значение этого символьного объекта равно значению односимвольной литеральной формы этого символа. Определяется реализация, может ли объект char содержать отрицательные значения. Символы могут быть явно объявлены без знака или подписаны. Обычная char, подписанная char и unsigned char - это три различных типа, которые в совокупности называются узкими типами символов. A char, подписанный char и unsigned char занимают одинаковое количество хранилищ и имеют одинаковые требования к выравниванию (3.11); то есть они имеют одно и то же представление объекта. Для узких типов символов в представлении значения участвуют все биты представления объекта. Для беззнаковых узких типов символов все возможные битовые шаблоны представления значений представляют числа. Эти требования не подходят для других типов. В любой конкретной реализации простой объект char может принимать один и тот же значения в виде подписанного char или unsigned char; какой из них определяется реализацией.
Преобразование значений вне диапазона от unsigned char
до char
будет, конечно, проблематичным и может вызвать поведение undefined. То есть, пока вы не пытаетесь сохранить смешные значения в std::string
, все будет в порядке. Что касается битовых шаблонов, вы можете полагаться на бит n
th для перевода в 2 n
. Не должно быть проблем с сохранением двоичных данных в std::string
при тщательной обработке.
Тем не менее, я не покупаю ваше предположение: обработка двоичных данных в основном требует обработки байтов, которые лучше всего обрабатываются с использованием значений unsigned
. В нескольких случаях, когда вам нужно будет конвертировать между char*
и unsigned char*
, создайте удобные ошибки, если они не будут обработаны явно, а использование char
случайно будет отключено! То есть работа с unsigned char
предотвратит ошибки. Я также не покупаю в предположении, что вы получаете все эти прекрасные строковые функции: во-первых, вы, как правило, лучше используете алгоритмы, но двоичные данные не являются строковыми данными. Вкратце: рекомендация для std::vector<unsigned char>
не просто выходит из воздуха! Преднамеренно избегать строительства трудно найти ловушки в дизайн!
Единственным умеренно обоснованным аргументом в пользу использования char
может быть тот, который содержит строковые литералы, но даже не содержит воды с пользовательскими строковыми литералами, введенными в С++ 11:
#include <cstddef>
unsigned char const* operator""_u (char const* s, size_t)
{
return reinterpret_cast<unsigned char const*>(s);
}
unsigned char const* hello = "hello"_u;
Ответ 2
Да, ваши предположения верны.
Храните двоичные данные в виде последовательности без знака char в std::string.
Ответ 3
У меня возникли проблемы с использованием std::string для обработки двоичных данных в Microsoft Visual Studio. Я видел, что строки становятся необъяснимо усеченными, поэтому я не сделал бы этого, независимо от того, что говорят документы стандартов.