Показаны сообщения с ярлыком Библиотеки. Показать все сообщения
Показаны сообщения с ярлыком Библиотеки. Показать все сообщения

четверг, 23 апреля 2015 г.

C/C++. Преобразование строки в целое число с помощью strtol и strtoll

Дополнение к справочной информации по strtol и strtoll

Программисты C/C++, работающие со стандартными библиотеками, знакомы с использованием функций strtol() и strtoll(), которые можно использовать для преобразования строк в целые числа. Заголовки этих функций доступны в заголовочных файлах stdlib.h или cstdlib (для C++).

Мне тоже приходилось сталкиваться с этими функциями в экспериментальных задачах. В своих серьезных проектах я, как правило, использую собственные функции преобразования строк в числа, так как они и работают быстрее и максимально адаптированы к требованиям задачи. Учитывая, что написание и тестирование таких простейших конечных автоматов не составляет труда, то вопрос часто решается в пользу собственных функций.

Однако, в одном из проектов я столкнулся с жесткой рекомендацией использовать именно функции strtol() и strtoll() и это потребовало провести некоторые эксперименты, которые бы дали более полное представление о работе данных функций, кроме того, что было изложено на странице мануала (man 3 strtol).

Я решил опубликовать результаты проделанных экспериментов, так как посчитал их полезными для тех, кто собирается использовать эти стандартные функции. Если кому-то интересны только результаты экспериментов, имеет смысл сразу перейти к таблице результатов.

Начать я хочу с того, что эксперименты были проведены на платформе "Ubuntu 14.04 (x64)". Платформа имеет значение по причине того, что стандартные целые типы в C/C++ являются логическими типами и отображаются на разные физические типы в зависимости от используемой платформы. Так, на моей платформе, были получены следующие результаты по интересующим нас логическим целым типам.

  sizeof(int):           4
  sizeof(long int):      8
  sizeof(long long int): 8
  sizeof(size_t):        8

Напомним сигнатуру функций strtol() и strtoll().

    long int strtol(const char *nptr, char **endptr, int base);

    long long int strtoll(const char *nptr, char **endptr, int base);

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

Напомним, что для анализа успеха выполненного преобразования нам доступны две величины:

  1. endptr - указатель на символ, на котором преобразование было закончено. Преобразование останавливается на символе, который бы не соответствовал строковому представлению целого числа в указанной системе счисления. Таким образом, для большинства случаев, таким символом должен быть символ конца строки ASCIIZ - '\0'. Для отдельных случаев, это могут быть какие-то другие символы. Например, вполне ожидаемым, при совершенно корректной записи литерала, может стать останов на символах '\r', '\n'. Вполне реальным был случай обработки строкового значения типа "123\r\n", которая была извлечена из потока в виде строки до символа '\n' и, соответственно, строка "123\r" была передана на преобразование.
  2. errno - глобальная переменная стандартной библиотеки, которая хранит в себе значение последней ошибки. Интересующие нас функции strtol() и strtoll() могут устанавливать эту переменную в значение ERANGE, что соответствует переполнению в большую или меньшую сторону.

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

void test(const std::string & num, int base = 10)
{
    const char * s = num.c_str();
    char *ptr = 0;
    errno = 0;
    const int64_t h = strtoll(s, &ptr, base);

    std::cout << "original string:  " << num << " start with 0x" 
              << std::hex << (long int)s << std::dec << std::endl;
    std::cout << "length of string: " << num.size() << std::endl;
    std::cout << "parsed length:    " << (((long int)ptr)-((long int)s)) << std::endl;
    std::cout << "end of parsing:   " << "0x" 
              << std::hex << (long int)ptr << std::dec << std::endl;
    std::cout << "*ptr == 0: " << ((*ptr == 0)?"true":"false") << std::endl;
    std::cout << "ptr>start: " << ((ptr > s)?"true":"false") << std::endl;
    std::cout << "LLONG_MIN: " << ((h == LLONG_MIN)?"true":"false") << std::endl;
    std::cout << "LLONG_MAX: " << ((h == LLONG_MAX)?"true":"false") << std::endl;
    std::cout << "ERANGE:    " << ((errno == ERANGE)?"true":"false") << std::endl;
}

По реализации данной функции следует сделать следующие пояснения.

  1. Во-первых, так как указатель на строку преобразования получается из типа std::string, то он всегда корректен и всегда указывает на строку ASCIIZ. Строка может быть пустой или не являться изображением целочисленного литерала, но строка будет ASCIIZ-корректна. Поэтому, нет смысла устраивать соответствующих проверок. В других случаях следует быть уверенным в этом, прежде, чем отдавать строку на преобразование.
  2. Во-вторых, во многом благодаря первой причине, указатель, переданный в функцию strtoll() вторым параметром, всегда будет указывать на какой-то элемент переданной строки ASCIIZ и, также, не требует каких-то проверок своей корректности перед использованием, при анализе результатов функции strtoll().
  3. В-третьих, не следует пренебрегать обнулением глобальной переменной errno. В противном случае, переменная будет хранить результат последней неудачной операции, сбивая с толку анализ последующих успешных операций. Это можно легко проверить запустив функцию test() сначала на строку, где переменная errno получит значение ERANGE, а потом вызвать любое количество раз функцию test() с корректными данными.
value len ptr-s *ptr == '\0' ptr>s LLONG_MIN LLONG_MAX ERANGE
263-2
"9223372036854775806"
19 0xbbb06b-0xbbb058=19 true true false false false
263-1
"9223372036854775807"
19 0xbbb06b-0xbbb058=19 true true false true false
-263
"-9223372036854775808"
20 0xbbb06c-0xbbb058=20 true true true false false
263
"9223372036854775808"
19 0xbbb06b-0xbbb058=19 true true false true true
-263-1
"-9223372036854775809"
20 0xbbb06c-0xbbb058=20 true true true false true
"xyz" 3 0x1ab6028-0x1ab6028=0 false false false false false
"123xyz" 6 0x232c02b-0x232c028=3 false true false false false

В таблице рассмотрены следующие основные варианты значений.

  1. (263-2): значение внутри диапазона LLONG_MIN..LLONG_MAX: Крайние три правых флага таблицы сброшены.
  2. (263-1) и (-263): значения на краях диапазона LLONG_MIN..LLONG_MAX - сброшен флаг переполнения, но установлены соответствующие флаги крайних значений.
  3. (263) и (-263-1): значения за краями диапазона LLONG_MIN..LLONG_MAX - установлен флаг переполнения и установлены соответствующие флаги крайних значений.
  4. "xyz": строка не являющаяся изображением строкового литерала - последний символ парсинга соответствует началу строки.
  5. "123xyz": строка начинающаяся изображением строкового литерала, но содержащая недопустимые символы - указатель парсинга сдвинут на три символа и указывает на первый невалидный символ

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

Для большинства случаев, можно предложить следующий вариант проверок.

  if ((ptr > s) && (*ptr == '\0') && (errno != ERANGE)) {
    // Ok!
  }
  1. Разобран как минимум один символ строки.
  2. Изображение целочисленного литерала разобрано до конца (при условии, что он заканчивается на '\0').
  3. Нет переполнения ни в большую ни в меньшую сторону.

Если кроме факта переполнения необходимо контролировать сторону, в которую было сделано переполнение, то вместе с анализом errno необходимо анализировать результат преобразования на соответствие крайнему нижнему (LLONG_MIN) и крайнему верхнему (LLONG_MAX) значениям.

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


вторник, 27 ноября 2012 г.

Основы protobuf

Если вы еще используете JSON, то Google protobuf идет к вам!

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

Поставленные цели перекликаются с тем, что знаем по технологиям JSON и оригинального XML, однако технология protobuf имеет ряд преимуществ в ключе своей завершенности. Т.е. если средства, обычно предоставляемые для работы с JSON или XML являются некоторыми низкоуровневыми инструментами, то protobuf обеспечивается инструментами самого высокого уровня.

Надо отметить, что, наверное единственный недостаток технологии заключается в необходимости тянуть в проект дополнительные связи с внешней библиотекой. Так, например, работая с JSON или XML, я часто использую свои парсеры и синтезаторы пакетов, чтобы не использовать сторонних библиотек, усложняющих сборку проекта на стороне. Здесь же вся соль технологии заключается в обработке метаописаний и, поэтому, без установки и использования специальных инструментов просто не обойтись.

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

На момент написания этой статьи, Google предоставлял технологию protobuf для языков C++, Java и Python. Кроме того, сторонними заинтересованными компаниями, группами и лицами предоставлена реализация этой технологии для пары десятков других известных языков и, даже, специально для сред разработки.

Чтобы начать использование технологии protobuf с помощью средств предоставляемых Google следует скачать необходимый установочный пакет (под вашу операционную систему) со страницы code.google.com/p/protobuf/downloads/list. Для своего Linux Ubuntu 11.10 я взял вот такой пакет с исходными кодами продукта - protobuf-2.4.1.tar.bz2. Это пакет собран в классическом GNU toolchain и для его сборки и установки надо в директории разархивированного пакета выполнить следующий набор команд в консоли.

$ ./configure
$ make
$ make check
$ sudo make install

Подробности сборки читайте в файле README.txt. Тем кто не знаком с такой системой сборки можно дать только один совет - как можно скорее познакомьтесь с ней. Обратите внимание на цель make check. По этой цели вызывается система автоматических тестов собранного пакета.

После установки пакета можно начинать эксперименты по его использованию. Здесь нам, прежде всего, помогут следующие ссылки на оригинальные ресурсы Goggle.

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

Я начал с того, что доверился показательности примера метаописания адресной книги, приведенной в учебнике к C++, и, на его основе, написал простую программку на C++, которая исполняет сериализацию и десериализацию простейшей адресной книги.

package tutorial;

message Person {
    required string name = 1;
    required uint32 id = 2;
    optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
    repeated Person person = 1;
}

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

Объявление пакета (package) вводит в контексте C++ одноименное пространство имен, что предотвращает возможные коллизии данных.

Представленное метаописание содержит три объекта данных: адресная книга (AddressBook), единица записи в адресной книге (Person) и составная часть описания Person - объект телефонного номера (PhoneNumber). По каждому из этих объектов будет формироваться сообщение в пакете сериализации, поэтому для метаописания этих объектов используется ключевое слово message.

Каждый элемент данных сопровождается модификатором: required, optional и repeated.

  1. required - элемент обязательно присутствует в сообщении.
  2. optional - элемент опционален. Может не присутствовать в сообщении.
  3. repeated - элемент может повторяться в сообщении любое количество раз, включая ноль раз.

Таким образом, в нашем описании описана адресная книга (AddressBook), которая может состоять из любого количества элементов Person. Каждый элемент Person, состоит из четрех полей: name, id, email и phone. Поле email - опционально, а поле phone может включать любое число объектов сообщения типа PhoneNamber. Элемент PhoneNumber состоит из двух полей - номера телефона (number) и его типа (type), значение которого определяется специальным типом нумератором PhoneType. Тип номера - опционален.

В метаописании данных допускается использование нескольких примитивных типов, в том числе: bool, int32, uint32, float, double и string. Полный список примитивных типов можно найти в этом разделе руководства метаописания данных. В дополнение к этому, можно составлять типы нумераторы и организовывать иерархию сообщений используя их как типы.

Отдельно следует сказать о тегах, которые определяют уникальность элемента данных внутри записи. Номера тегов записываются в конце каждого элемента сообщения через знак "=". Номера этих тегов делятся на две группы. Группа 1-15 и группа >=16. Различие этих групп в особенностях двоичного кодирования данных, которое мне пока не совсем понятно.

Итак, у нас есть метаописание адресной книги. В нашем случае оно лежит в файле addressbook.proto. Создадим по нему систему классов в пространстве имен tutorial к языку C++. Для этого отдадим файл метаописания утилите protoc следующим образом.

$ protoc --cpp_out=. addressbook.proto

При успешном выполнении этой операции мы получим два файла: addressbook.pb.h и addressbook.pb.cc. Это заголовочный файл и файл реализации для классов составленных по метаописанию. Теперь надо включить эти классы в пространство нашего проекта и можно начинать их использовать в коде проекта.

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

#include "addressbook.pb.h"

int main(int /*argc*/, char */*argv*/[])
{
    // Создаем экземпляр класса адресной книги для сериализации
    tutorial::AddressBook src_book;
    {
        // Создаем и заполняем первую запись в адресной книге
        tutorial::Person * person = src_book.add_person();
        person->set_name("Alexey Knyazev");
        person->set_id(0);
        person->set_email("knzsoft@mail.ru");
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("+7 927-220-35-67");
            pn->set_type(tutorial::Person_PhoneType_MOBILE);
        }
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("+7 962-622-31-67");
            pn->set_type(tutorial::Person_PhoneType_MOBILE);
        }
    }
    {
        // Создаем и заполняем вторую запись в адресной книге
        tutorial::Person * person = src_book.add_person();
        person->set_name("Danilov Dmitry");
        person->set_id(1);
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("8 (8452) 43-96-86");
            pn->set_type(tutorial::Person_PhoneType_HOME);

        }
    }

    std::string msg;
    src_book.SerializeToString(&msg);

    tutorial::AddressBook dst_book;
    dst_book.ParseFromString(msg);

    dst_book.PrintDebugString();

    return 0;
}

Большую часть представленного примера программы составляет заполнение адресной книги. Из кода видно, что API для технологии protobuf достаточно простой и интуитивно понятный.

После заполнения книги мы сериализуем данные созданной адресной книги в строку msg. После чего создаем объект другой адресной книги и заполняем его через десериализацию сообщения msg. В завершении, пользуясь отладочными API, выводим десериализованную адресную книгу в стандартное устройство вывода.

Вот что выводится в консоль в результате выполнения этой программы.

person {
  name: "Alexey Knyazev"
  id: 0
  email: "knzsoft@mail.ru"
  phone {
    number: "+7 927-220-35-67"
    type: MOBILE
  }
  phone {
    number: "+7 962-622-31-67"
    type: MOBILE
  }
}
person {
  name: "Danilov Dmitry"
  id: 1
  phone {
    number: "8 (8452) 43-96-86"
    type: HOME
  }
}

Напомню, что во всем этом есть небольшая ложка дегтя - необходимость линковки с библиотекой обеспечивающей фундамент технологии protobuf, с библиотекой libprotobuf. Я делал пример в QtCreator с использованием системы сборки QMake и мой проектный файл выглядит следующим образом.

TARGET = app-1
CONFIG += console
CONFIG -= app_bundle

LIBS += -lprotobuf

TEMPLATE = app

SOURCES += main.cpp \
           addressbook.pb.cc

HEADERS += addressbook.pb.h

OTHER_FILES += \
    addressbook.proto

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

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

Наверное следует добавить, что предоставленные средства дают возможность не только выполнять сериализацию в строку, но и в поток, что позволяет сразу вывести данные в файл, сеть или куда-то еще, что поддерживает обычные C++ потоки типа std::ostream и std::istream.