Почему вы не можете просто проверить, равно ли errno ERANGE?

Я пытался правильно преобразовать массив char в long с strtol, проверить, было ли переполнение или недостаточное заполнение, а затем выполнить int cast на long. Попутно я заметил много кода, который выглядит так

if ((result == LONG_MAX || result == LONG_MIN) && errno == ERANGE)
{
   // Handle the error
}

Почему ты не можешь просто сказать

if(errno == ERANGE)
{
    // Handle the error
}

Насколько я понимаю, если происходит переполнение или переполнение, в обоих случаях для errno устанавливается значение ERANGE. Так действительно ли необходимо первое? Может ли быть проблематичной проверка только ERANGE?

Вот как мой код выглядит сейчас

 char *endPtr;
 errno = 0;
 long result = strtol(str, &endPtr, 10);

 if(errno == ERANGE)
 {
     // Handle Error
 }
 else if(result > INT_MAX || result < INT_MIN)
 {
    // Handle Error
 }
 else if(endPtr == str || *endPtr != '\0')
 {
     // Handle Error
 }

 num = (int)result;
 return num;

Если для первого есть причина, пожалуйста, дайте мне знать.


person Luis Averhoff    schedule 18.03.2016    source источник
comment
Я никогда не видел хорошего объяснения, почему необходимо проверять и LONG_MAX/LONG_MIN, и ERANGE. Помимо того факта, что на странице руководства это показано в качестве примера. Единственный разумный вариант использования, который я могу придумать, - это различать переполнение и потерю значимости. Мне тоже было бы интересно узнать, есть ли другие причины.   -  person kaylum    schedule 18.03.2016
comment
@kaylum Я не знаю, я считаю, что мой пример верен в основном потому, что я не собираюсь различать, произошло ли переполнение или опустошение, и что для errno установлено значение ERANGE в обоих случаях. Если один из них произошел, результат недействителен.   -  person Luis Averhoff    schedule 18.03.2016
comment
@LuisAverhoff Может быть, я не понял. Но я согласился с вами, что ваша версия проверки ошибок мне нравится.   -  person kaylum    schedule 18.03.2016
comment
@kaylum Opps, прости за этого человека.   -  person Luis Averhoff    schedule 18.03.2016
comment
@kaylum Есть одна действительная проверка, которую я забыл сделать, и я думаю, что справочная страница даже говорит о ней, и это если errno != 0 && result == 0. Хотя, честно говоря, я не уверен, что могло вызвать это, потому что если strtol возвращает 0, то я уверен, что strtol не устанавливает errno ни на что, если изначально он был установлен в ноль.   -  person Luis Averhoff    schedule 18.03.2016
comment
Ваш 2-й фрагмент кода более правильный, чем 2 ответа. ИМО, это правильно и оптимально, за исключением того, что я бы просто сделал if(errno)   -  person chux - Reinstate Monica    schedule 18.03.2016


Ответы (2)


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

errno - это локальная переменная потока. Он устанавливается в ненулевое значение, когда системный вызов или определенные библиотечные функции терпят неудачу. Он остается неизменным при успешном выполнении системного вызова. Таким образом, он всегда содержит номер ошибки последнего неудачного вызова.

Это означает, что у вас есть два варианта. Либо установите errno в 0 перед каждым вызовом, либо используйте стандартную идиому для errno. Вот псевдокод стандартной идиомы

if ( foo() == some_value_that_indicates_that_an_error_occurred )
    then the value in errno applies to foo
else
    foo succeeded and the errno must be ignored because it could be anything

Большинство программистов будут использовать стандартную идиому, потому что установка errno в 0 перед каждым системным вызовом раздражает и повторяется. Не говоря уже о том, что вы можете забыть установить errno в 0 в том месте, где это действительно важно.


Вернемся к первому фрагменту кода. Это неверно, потому что от strtol не возвращается значение, однозначно указывающее на сбой strtol. Если strtol возвращает LONG_MAX, возможно, произошла ошибка или строка действительно содержала число LONG_MAX. Невозможно узнать, был ли вызов strtol успешным или неудачным. Это означает, что стандартная идиома (которую пытается реализовать первый фрагмент кода) не может использоваться с strtol.

Чтобы правильно использовать strtol, вам нужно установить errno в 0 перед вызовом, например

errno = 0;
result = strtol( buffer, &endptr, 10 );
if ( errno == ERANGE )
{
    // handle the error
    // ERANGE is the only error mentioned in the C specification
}
else if ( endptr == buffer )
{
    // handle the error
    // the conversion failed, i.e. the input string was empty,
    // or only contained whitespace, or the first non-whitespace 
    // character was not valid
}

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

person user3386109    schedule 18.03.2016
comment
Хороший ответ, но одно замечание: строго говоря, strtol - это библиотечная функция, а не системный вызов. И это необычно тем, что манипулирует errno. Большинство (все?) Системных вызовов устанавливают errno, когда они терпят неудачу, но немногие библиотечные функции это делают. - person Steve Summit; 18.03.2016
comment
Спасибо за отличный ответ. Если вы этого не сделаете, я могу спросить, но возможно ли это для errno != 0 && result == 0? Я видел код, в котором они это включали, и if((result == LONG_MAX || result == LONG_MIN) && errno == ERANGE) Например, если вы установили errno в ноль прямо перед вызовом strol для преобразования A123. Вы ожидаете, что результат будет 0, а errno останется равным нулю. Что могло привести к тому, что errno не будет равным нулю в этой ситуации? - person Luis Averhoff; 18.03.2016
comment
@SteveSummit Это правда, но я не уверен, как включить это в ответ, не добавляя ненужных сложностей. Надеюсь, можно оставить это как комментарий. - person user3386109; 18.03.2016
comment
@LuisAverhoff strtol вернет 0, если преобразование не может быть выполнено. В этом случае значение в errno определяется реализацией. В спецификации C не указано, какое значение должно быть сохранено в errno в этом случае. Так что да, вы можете получить возвращаемое значение 0 и ненулевое значение errno. - person user3386109; 18.03.2016
comment
Извините за придирку, но я был удивлен, прочитав, что при сбое системного вызова устанавливается ненулевое значение, как если бы strtol был системным вызовом. Вы можете изменить это значение на «Он установлен на ненулевое значение при сбое системного вызова или некоторых библиотечных функций, а также изменить set errno на 0 перед каждым системным вызовом, чтобы установить errno на 0 перед каждым вызовом». - person Steve Summit; 18.03.2016
comment
@ user3386109 Итак, если это определено реализацией, то нет смысла проверять это, или если errno = ERANGE, если вы проверяете, было ли переполнение или потеря значимости. - person Luis Averhoff; 18.03.2016
comment
@LuisAverhoff Ага, мне пришлось переосмыслить эту часть. Я обновил ответ. - person user3386109; 18.03.2016
comment
Не согласен с Правильное использование strtol, поскольку он не проверяет случай отсутствия преобразования. Необходимо добавить if (buffer == endptr) Handle_NoConverisonError(); - person chux - Reinstate Monica; 18.03.2016
comment
@SteveSummit: Большинство библиотечных функций не указаны стандартом C для установки errno, но многие из них все равно это делают (или требуются POSIX для этого). fopen является примером. - person Keith Thompson; 18.03.2016

Если вы позвоните

result = strtol("-2147483648", NULL, 0);

or

result = strtol("2147483647", NULL, 0);

на 32-битной машине вы получите LONG_MIN или LONG_MAX в result, даже если ошибки не было.

Как объяснил user3386109, один из способов обнаружения ошибок из strtol - сначала установить errno в 0. Другой способ - дать ему конечный указатель и посмотреть на него. Есть три или четыре случая:

char *endptr;
long int result = strtol(str, &endptr, 10);
if(*str == '\0') {
    /* str was empty */
} else if(endptr == str) {
    /* str was completely invalid */
} else if(*endptr != '\0') {
    /* numeric result followed by trailing nonnumeric character(s) */
} else {
    /* str was a completely valid number (perhaps with leading whitespace) */
}

В зависимости от ваших потребностей первые два или три случая могут быть свернуты вместе. Затем вам может потребоваться беспокоиться (а) о том, было ли представимо «полностью действительное число» (что вы можете проверить с помощью errno), и (б) были ли какие-либо «конечные нечисловые символы» безобидными пробелами (что, увы, strtol не проверяет вас, поэтому, если вам не все равно, вам придется проверить себя).

person Steve Summit    schedule 18.03.2016
comment
поэтому нет смысла говорить `if (result› INT_MAX || result ‹INT_MIN), потому что результат всегда будет равен INT_MAX или INT_MIN, если есть переполнение или потеря значимости в отношении int правильно? Я должен просто проверить, равен ли результат одному из них. - person Luis Averhoff; 18.03.2016
comment
@LuisAverhoff Извините, я сказал INT_MAX, когда имел в виду LONG_MAX. (Сейчас исправлено.) Но в целом я бы сказал, что нет никаких причин явно проверять возвращаемое значение strtol на соответствие минимальному или максимальному значению. (Единственная причина может заключаться в сравнении с INT_MIN и INT_MAX, если вы собирались сохранить значение в простом int.) - person Steve Summit; 18.03.2016
comment
Да, я собирался сохранить значение в простом int. - person Luis Averhoff; 18.03.2016
comment
@SteveSummit Моя очередь придираться;) Конечные пробелы не будут использоваться, поэтому *endptr может указывать на пробел или новую строку. В частности, слово в конце последнего комментария неверно. - person user3386109; 18.03.2016
comment
@ user3386109 Спасибо. Я мог бы поклясться, что читал о версии, в которой убраны конечные пробелы, и с тех пор это было у меня в голове, но вы правы, в документации об этом не упоминается. Фиксированный. - person Steve Summit; 18.03.2016
comment
if(*str == '\0') как особый случай на самом деле не нужен, так как пустая строка попадет в if(endptr == str) - person chux - Reinstate Monica; 18.03.2016