Бинарная версия iostream - PullRequest
7 голосов
/ 20 июля 2009

Я пишу двоичную версию iostreams. По сути, он позволяет вам записывать двоичные файлы, но дает вам большой контроль над форматом файла. Пример использования:

my_file << binary::u32le << my_int << binary::u16le << my_string;

Записал бы my_int как 32-разрядное целое число без знака, а my_string как строку с префиксом длины (где префикс u16le.) Чтобы прочитать файл обратно, вы должны щелкнуть стрелками. Работает отлично. Тем не менее, я столкнулся с проблемой в дизайне, и я все еще нахожусь на ограждении по этому поводу. Итак, время спросить ТАК. (Мы делаем пару предположений, таких как 8-битные байты, 2-битные дополнения и IEEE с плавающей точкой на данный момент.)

iostreams, под капотом, используйте streambufs. На самом деле это фантастический дизайн - iostreams кодирует сериализацию 'int' в текст, и позволяет лежащему в основе streambuf обрабатывать все остальное. Таким образом, вы получаете cout, fstreams, stringstream и т. Д. Все они, как iostreams, так и streambufs, шаблонизируются, как правило, на char, но иногда также как wchar. Мои данные, однако, представляют собой поток байтов, который лучше всего представлен как 'unsigned char'.

Моими первыми попытками было шаблонизировать классы на основе unsigned char. std::basic_string шаблонов достаточно хорошо, но streambuf нет. Я столкнулся с несколькими проблемами с классом с именем codecvt, который я никогда не мог проследить за темой unsigned char. Это поднимает два вопроса:

1) Почему стримбуф отвечает за такие вещи? Кажется, что преобразование кода лежит вне ответственности потокового буфера - потоковые буферы должны брать поток и буферизовать данные в / из него. Ничего более. Нечто такое же высокое, как и при преобразовании кода, кажется, что оно должно принадлежать iostreams.

Поскольку я не мог заставить шаблонные потоковые буферы работать с неподписанным символом, я вернулся к типу char и просто преобразовал данные между типом char / unsigned. Я пытался свести к минимуму количество приведений по понятным причинам. Большая часть данных в основном оказывается в функции read () или write (), которая затем вызывает основной поток потоков. (И используйте приведение в процессе.) Функция чтения в основном:

size_t read(unsigned char *buffer, size_t size)
{
    size_t ret;
    ret = stream()->sgetn(reinterpret_cast<char *>(buffer), size);
    // deal with ret for return size, eof, errors, etc.
    ...
}

Хорошее решение, плохое решение?


Первые два вопроса указывают на необходимость дополнительной информации. Во-первых, рассматривались такие проекты, как boost :: serialization, но они существуют на более высоком уровне, поскольку они определяют свой собственный двоичный формат. Это больше для чтения / записи на более низком уровне, где требуется определить формат, или формат уже определен, или объемные метаданные не требуются или не желательны.

Во-вторых, некоторые спрашивают о модификаторе binary::u32le. Это экземпляр класса, который содержит желаемую последовательность и ширину, на данный момент, возможно, подпись в будущем. Поток содержит копию последнего переданного экземпляра этого класса и использовал его в сериализации. Это был обходной путь, я изначально попытался перегрузить оператор << таким образом: </p>

bostream &operator << (uint8_t n);
bostream &operator << (uint16_t n);
bostream &operator << (uint32_t n);
bostream &operator << (uint64_t n);

Однако в то время это, похоже, не работало. У меня было несколько проблем с неоднозначным вызовом функции. Это было особенно верно в отношении констант, хотя, как предлагал один из них, вы могли разыграть или просто объявить его как const <type>. Кажется, я помню, что была еще одна большая проблема.

Ответы [ 4 ]

2 голосов
/ 08 марта 2010

Я согласен с легализацией. Мне нужно было сделать почти точно то, что вы делаете, и посмотрел на перегрузку << / >>, но пришел к выводу, что iostream просто не предназначен для этого. Во-первых, я не хотел создавать подклассы потоковых классов для определения моих перегрузок.

Мое решение (для которого требовалось только временно сериализовать данные на одном компьютере и, следовательно, не нужно было указывать порядок байтов), было основано на этом шаблоне:

// deducible template argument read
template <class T>
void read_raw(std::istream& stream, T& value,
    typename boost::enable_if< boost::is_pod<T> >::type* dummy = 0)
{
    stream.read(reinterpret_cast<char*>(&value), sizeof(value));
}

// explicit template argument read
template <class T>
T read_raw(std::istream& stream)
{
    T value;
    read_raw(stream, value);
    return value;
}

template <class T>
void write_raw(std::ostream& stream, const T& value,
    typename boost::enable_if< boost::is_pod<T> >::type* dummy = 0)
{
    stream.write(reinterpret_cast<const char*>(&value), sizeof(value));
}

Затем я перегрузил read_raw / write_raw для любых типов, отличных от POD (например, строк). Обратите внимание, что только первая версия read_raw должна быть перегружена; если вы используете ADL правильно , вторая (1-arg) версия может вызвать 2-аргументные перегрузки, определенные позже и в других пространствах имен.

Напишите пример:

int32_t x;
int64_t y;
int8_t z;
write_raw(is, x);
write_raw(is, y);
write_raw<int16_t>(is, z); // explicitly write int8_t as int16_t

Прочитать пример:

int32_t x = read_raw<int32_t>(is); // explicit form
int64_t y;
read_raw(is, y); // implicit form
int8_t z = numeric_cast<int8_t>(read_raw<int16_t>(is));

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

1 голос
/ 20 июля 2009

Насколько я понимаю, свойства потока, которые вы используете для указания типов, были бы более подходящими для указания значений порядка байтов, упаковки или других значений "метаданных". Обработка самих типов должна выполняться компилятором. По крайней мере, так выглядит STL.

Если вы используете перегрузки для автоматического разделения типов, вам нужно будет указывать тип только тогда, когда он отличается от объявленного типа переменной:

Stream& operator<<(int8_t);
Stream& operator<<(uint8_t);
Stream& operator<<(int16_t);
Stream& operator<<(uint16_t);
etc.

uint32_t x;
stream << x << (uint16_t)x;

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

Я считаю, что стандартная версия std :: codecvt ничего не делает, возвращая noconv для всего. Он действительно делает что-либо только при использовании «широких» потоков символов. Не можете ли вы установить подобное определение для codecvt? Если по какой-либо причине нецелесообразно определять неиспользуемый кодекв для вашего потока, то я не вижу никаких проблем с вашим решением для кастинга, тем более что оно изолировано в одном месте.

Наконец, вы уверены, что вам лучше не использовать какой-нибудь стандартный код сериализации, такой как Boost , вместо того, чтобы использовать собственный?

0 голосов
/ 20 июля 2009

Я бы не стал использовать оператор <<, поскольку он слишком тесно связан с вводом-выводом в формате текста. </p>

Я бы вообще не использовал перегрузку оператора для этого. Я бы нашел другую идиому.

0 голосов
/ 20 июля 2009

Нам нужно было сделать что-то похожее на то, что вы делаете, но мы пошли другим путем. Меня интересует, как вы определили свой интерфейс. Часть того, что я не знаю, как вы можете справиться, - это манипуляторы , которые вы определили (binary :: u32le, binaryu16le).

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

binary_istream in;
int i;
int i2;
short s;
in >> binary::u16le >> i >> binary::u32le >> i2 >> s;

В приведенном выше коде может иметь смысл определить, является ли переменная i 32-битной (при условии, что int 32-битная), вы хотите извлечь из сериализованного потока только 16 бит, а вы хотите извлечь 32 биты в i2. После этого либо пользователь вынужден вводить манипуляторы для каждого переданного типа, либо манипулятор все еще действует, и когда передается короткое замыкание, и 32 бита считываются с возможным переполнением, и любым способом. пользователь, вероятно, получит неожиданные результаты.

Размер, по-моему, не относится (на мой взгляд) к манипуляторам.

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

...