четверг, 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) значениям.

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