Как правильно использовать std::string для хранения байтов (беззнаковых символов)?

Я кодирую алгоритм сжатия LZ77, и у меня возникают проблемы с сохранением беззнаковых символов в строке. Чтобы сжать любой файл, я использую его двоичное представление, а затем читаю его как chars (поскольку 1 символ равен 1 байту, афаик) в std::string. С chars все отлично работает. Но после некоторого времени гугления я узнал, что char не всегда 1 байт, поэтому решил поменять его на unsigned char. И тут начинаются сложности:

  • При сжатии простого .txt все работает как положено, я получаю одинаковые файлы до и после распаковки (я предполагаю, что так и должно быть, потому что мы в основном работаем с текстом до и после преобразования байтов)
  • Однако при попытке сжать .bmp распакованный файл теряет 3 байта по сравнению с входным файлом (я теряю эти 3 байта при попытке сохранить неподписанные символы в std::string)

Итак, мой вопрос: есть ли способ правильно сохранить беззнаковые символы в строку?

Я пытался использовать typedef basic_string<unsigned char> ustring и поменять все связанные функции на их основные альтернативы для использования с unsigned char, но все равно теряю 3 байта.

ОБНОВЛЕНИЕ: я обнаружил, что 3 байта (символа) теряются не из-за std::string, а из-за std::istream_iterator (который я использую вместо std::istreambuf_iterator) для создания строки беззнаковых символов (потому что аргумент std::istreambuf_iterator это char, а не unsigned char)

Итак, есть ли решения этой конкретной проблемы?

Пример:

std::vector<char> tempbuf(std::istreambuf_iterator<char>(file), {}); // reads 112782 symbols

std::vector<char> tempbuf(std::istream_iterator<char>(file), {}); // reads 112779 symbols

Образец кода:

void LZ77::readFileUnpacked(std::string& path)

{


std::ifstream file(path, std::ios::in | std::ios::binary);

if (file.is_open())
{
    // Works just fine with char, but loses 3 bytes with unsigned
    std::string tempstring = std::string(std::istreambuf_iterator<char>(file), {});
    file.close();
}
else
    throw std::ios_base::failure("Failed to open the file");
}

person asymmetriq    schedule 30.11.2019    source источник
comment
Когда char не 1 байт?   -  person Eljay    schedule 30.11.2019
comment
Почему не std::vector?   -  person Igor R.    schedule 30.11.2019
comment
@Eljay Я читал, что на некоторых платформах char не может быть равен 1 байту (потому что, по-видимому, стандарт не определяет его точный размер). В любом случае, что вы думаете о хранении байтовых данных с использованием обычного char в целом? Достаточно ли он оптимален?   -  person asymmetriq    schedule 30.11.2019
comment
@ИгорьР. Это наихудший сценарий, поскольку потребуется переписать большую часть логики алгоритма.   -  person asymmetriq    schedule 30.11.2019
comment
Я бы использовал std::vector<std::byte> для хранения байтовых данных. Если вы хотите избежать переписывания такого большого количества кода, тогда std::basic_string<std::byte> (возможно, вам также придется предоставить свои собственные черты).   -  person Eljay    schedule 30.11.2019
comment
Это не просто хранение — мне нужно сжать эти данные, а std::byte не обеспечивает достаточного интерфейса (также я читал, что std::byte — это замаскированный unsigned char)   -  person asymmetriq    schedule 30.11.2019
comment
Я думаю, что вы ищете std::vector‹uint8_t›. Я не уверен, что поддержка uint8_t гарантирована стандартом, но если это так, то она будет иметь ровно 8 бит.   -  person Uri Raz    schedule 30.11.2019
comment
Вы не хотите использовать std::string - во многих местах предполагается, что строки заканчиваются нулем, а это не то, что вам нужно. Используйте std::vector. Не нужно беспокоиться о том, что char не является 8-битным. Такие системы редки, и наверняка на них не будет работать что-то еще, кроме кода вашего маленького архива.   -  person ALX23z    schedule 30.11.2019
comment
@ ALX23z небольшие шансы, что unsigned char будет отличаться от uint8_t, но с точки зрения разработки программного обеспечения лучше явно указать компилятору, чего вы пытаетесь достичь, поэтому в редких случаях можно получить явную ошибку компиляции, а не неясную ошибка выполнения.   -  person Uri Raz    schedule 30.11.2019
comment
std::vector<std::byte>> . Сделанный. Обсуждение окончено.   -  person Jesper Juhl    schedule 30.11.2019
comment
@asymmetriq Я читал, что на некоторых платформах char не может быть равен 1 байту (поскольку, очевидно, стандарт не определяет его точный размер) - в стандарте явно указано, что char имеет размер в один байт (что sizeof(char) всегда возвращает 1). Чего он не говорит, так это того, насколько велик байт. И да, есть платформы (хоть и редкие в наше время), где размер байта не 8 бит. См. CHAR_BIT фактический размер для данной платформы.   -  person Remy Lebeau    schedule 30.11.2019


Ответы (2)


char во всех его формах (и std::byte, который изоморфен unsigned char) всегда является наименьшим возможным типом, поддерживаемым системой. Стандарт C++ определяет, что sizeof(char) и его варианты всегда должны быть ровно 1.

"Один" что? Это определяется реализацией. Но каждый тип в системе будет иметь размер, кратный sizeof(char).

Поэтому вам не следует слишком беспокоиться о системах, где char не является одним байтом. Если вы работаете в системе, где CHAR_BITS не равно 8, то эта система вообще не может напрямую обрабатывать 8-битные байты. Так что unsigned char ничем не отличается/лучше для этой цели.


Что касается особенностей вашей проблемы, istream_iterator принципиально отличается от итератора istreambuf_iterator. Цель последнего — предоставить итератору доступ к реальному потоку в виде последовательности значений. Целью istream_iterator<T> является предоставление доступа к потоку, как если бы выполнялась повторяющаяся последовательность вызовов operator >> со значением T.

Итак, если вы делаете istream_iterator<char>, то вы говорите, что хотите прочитать поток, как если бы вы делали переменную stream >> some_char; для каждого доступа к итератору. На самом деле это не изоморфно прямому доступу к символам потока. В частности, FormattedInputFunctions, такие как operator>>, могут выполнять такие действия, как пропуск пробелов, в зависимости от того, как вы настроить свой поток.

person Nicol Bolas    schedule 30.11.2019
comment
Чтобы немного пояснить... В разделе 4.4 С++ 17 (IIRC это новое для этого стандарта) говорится, что основной единицей хранения в модели памяти С++ является байт. Байт должен быть достаточно большим, чтобы содержать любой член основного набора символов выполнения (5.3) и восьмибитных кодовых единиц формы кодирования Unicode UTF-8, и состоит из непрерывной последовательности битов, количество которых реализация определена. Я так понимаю, что, начиная с C++17, значение CHAR_BITS должно быть не менее 8. - person Uri Raz; 30.11.2019
comment
@UriRaz, CHAR_BITS должен быть не менее 8, начиная с C90. - person AProgrammer; 30.11.2019
comment
@UriRaz - CHAR_BITS всегда должно быть не менее 8 (посмотрите его определение в библиотечной части стандарта C). Но их может быть (а иногда и больше) больше 8. В некоторых (древних) системах наименьшая адресуемая единица памяти имела ширину 9 бит (так что CHAR_BIT было бы 9), а целые числа были 36-битными. В некоторых современных системах (в частности, DSP) наименьшая адресуемая единица хранения имеет ширину 32 бита, поэтому CHAR_BIT равно 32. - person Pete Becker; 30.11.2019

istream_iterator читает с помощью operator>>, которые обычно пропускают пробелы как часть своей функции. Если вы хотите отключить это поведение, вам нужно будет сделать

#include <ios>

file >> std::noskipws;
person AProgrammer    schedule 30.11.2019