Каков хороший шаблон программирования для обработки возвращаемых значений из функций записи файла stdio

Я работаю над кодом, который генерирует много

ignoring return value of ‘size_t fwrite(const void*, size_t, size_t, FILE*)’, declared with attribute warn_unused_result

предупреждения при компиляции с g ++, и мне интересно узнать о лучшем шаблоне программирования для фактической записи и обработки возвращаемого значения большого количества отдельных последовательных fwrite (т.е. не одинаковых fwrite в цикле)

Допустим, код на данный момент выглядит так:

fwrite (&blah, sizeof (blah), 1, fp);
// ... more code ...
fwrite (&foo, sizeof (foo), 1, fp);
// ... more code ...

Сейчас я думаю о чем-то вроде этого, но у меня могут возникнуть проблемы с очисткой указателя файла:

if (fwrite (&blah, sizeof (blah), 1, fp) != 1) return someerrorcode;
// ... more code ...
if (fwrite (&foo, sizeof (foo), 1, fp) != 1) return someerrorcode;
// ... more code ...

Я думаю, что этот подход явно лучше, чем вложение, которое слишком быстро станет слишком сумасшедшим:

if (fwrite (&blah, sizeof (blah), 1, fp) == 1) {
   // ... more code ...
   if (fwrite (&foo, sizeof (foo), 1, fp) == 1) {;
      // ... more code ...
   }
}

Конечно, для такого рода вещей уже существует устоявшаяся передовая практика?

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

dummy = fwrite (&blah, sizeof (blah), 1, fp);
// ... more code ...
dummy = fwrite (&foo, sizeof (foo), 1, fp);
// ... more code ...

Обновление: я удалил тег c ++, так как этот код на самом деле компилируется с использованием g ++, поэтому необходимы решения на основе c, чтобы сохранить остальную часть кода.


person David Dean    schedule 20.02.2009    source источник


Ответы (13)


Я бы сделал что-нибудь в этом роде:

FILE * file = fopen("foo", "wb");
if(!file) return FAILURE;

// assume failure by default
_Bool success = 0;

do
{
    if(!fwrite(&bar, sizeof(bar), 1, file))
        break;

    // [...]

    if(!fwrite(&baz, sizeof(baz), 1, file))
        break;

    // [...]

    success = 1;
} while(0);

fclose(file);

return success ? SUCCESS : FAILURE;

С небольшой магией макросов C99

#define with(SUBJECT, FINALIZE, ...) do { \
    if(SUBJECT) do { __VA_ARGS__ } while(0); if(SUBJECT) FINALIZE; \
} while(0)

и используя ferror() вместо нашего собственного флага ошибки, как предложил Джонатан Леффлер, это можно записать как

FILE * file = fopen("foo", "wb");
with(file, fclose(file),
{
    if(!fwrite(&bar, sizeof(bar), 1, file))
        break;

    // [...]

    if(!fwrite(&baz, sizeof(baz), 1, file))
        break;

    // [...]
});

return file && !ferror(file) ? SUCCESS : FAILURE;

Однако, если есть другие условия ошибки, помимо ошибок io, вам все равно придется отслеживать их с помощью одной или нескольких переменных ошибки.

Кроме того, ваша проверка на sizeof(blah) неверна: fwrite() возвращает количество записанных объектов!

person Christoph    schedule 20.02.2009
comment
Обратите внимание, что ferror () сообщает вам, была ли ошибка в потоке. - person Jonathan Leffler; 20.02.2009
comment
Одна неприятная проблема с этим подходом на основе макросов заключается в том, что ошибки компилятора не сообщают правильные номера строк для кода внутри оператора with - в зависимости от компилятора он сообщит либо строку, содержащую ключевое слово with, либо строку с закрывающей круглой скобкой вызов макроса для любых ошибок. - person Adam Rosenfield; 10.06.2011

Обработка исключений C для бедняков, основанная на goto (фактически, единственный и единственный экземпляр goto, НЕ являющийся вредоносным):

int foo() {
    FILE * fp = fopen(...);
    ....

    /* Note: fwrite returns the number of elements written, not bytes! */
    if (fwrite (&blah, sizeof (blah), 1, fp) != 1) goto error1;

    ...

    if (fwrite (&foo, sizeof (foo), 1, fp) != 1) goto error2;

    ...

ok:
    /* Everything went fine */
    fclose(fp);
    return 0;

error1:
    /* Error case 1 */
    fclose(fp);
    return -1;

error2:
    /* Error case 2 */
    fclose(fp);
    return -2;
}

Вы уловили идею. Реструктурируйте по своему усмотрению (однократный / многократный возврат, однократная очистка, настраиваемые сообщения об ошибках и т. Д.). По моему опыту, это наиболее распространенный шаблон обработки ошибок C. Ключевой момент: НИКОГДА, НИКОГДА не игнорируйте коды возврата stdlib, и любая веская причина для этого (например, удобочитаемость) недостаточно хороша.

person fbonnet    schedule 20.02.2009

Вы можете написать функцию-оболочку

void new_fwrite(a, b, c, d) {
    if (fwrite (a, b, c, b) != b) 
       throw exception;
}

а затем замените все вызовы fwrite на new_fwrite

person Patrick McDonald    schedule 20.02.2009
comment
Мне нравится эта идея, но я хочу оставить ее c-only, поэтому никаких исключений. - person David Dean; 20.02.2009

Игнорировать ошибки - плохая идея. Гораздо лучше сделать что-нибудь неприятное, например, вывести программу из строя, чтобы хотя бы вы знали, что что-то пошло не так, чем продолжать молча. Еще лучше хорошая проверка ошибок и восстановление.

Если вы используете C ++, вы можете создать оболочку RAII для ФАЙЛА *, чтобы он всегда закрывался. Посмотрите std :: auto_ptr для идей. Затем вы можете вернуть полезный код ошибки или из функции, когда захотите, или вызвать исключение и не беспокоиться о забытых элементах очистки.

person Mr Fooz    schedule 20.02.2009
comment
Верно, и (как вы подразумеваете) используйте это в сочетании с серией тестов, каждый из которых возвращает ошибку, если есть проблема. - person Sol; 20.02.2009
comment
@Sol: добавлено больше ясности - person Mr Fooz; 21.02.2009

Вы можете удалить предупреждения следующим образом:

(void) fwrite ( ,,,, );

Обращаясь к вашему основному вопросу, если какой-либо из вызовов fwrite () завершится неудачно, я бы предположил, что нет смысла продолжать, поскольку вывод предположительно поврежден. В этом случае, когда вы отметили этот C ++, я бы выбросил исключение.

person Community    schedule 20.02.2009

Вложенность - это плохо, и многократный возврат - тоже нехорошо.

Раньше я использовал следующий шаблон:

#define SUCCESS (0)
#define FAIL    (-1)
int ret = SUCCESS;

if (!fwrite(...))
    ret = FAIL;
if (SUCCESS == ret) {
    do_something;
    do_something_more;
    if (!fwrite(...))
        ret = FAIL;
}
if (SUCCESS == ret)
    do_something;

return ret;

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

person qrdl    schedule 20.02.2009
comment
«Единая точка возврата» - это очень субъективное правило, которому нужно следовать. Здесь не следует упоминать об этом. - person Bombe; 20.02.2009
comment
Я согласен с Бомбе. Заявление о многократном возврате нехорошо, поскольку в целом факт не является кошерным. - person Greg D; 20.02.2009
comment
Кроме того, возврат fwrite может быть ненулевым и по-прежнему указывать на сбой, вам необходимо сравнить его с размером записываемых данных. - person David Dean; 20.02.2009
comment
Единая точка возврата позволяет произвести очистку только один раз. Я знаю, что уборка нужна не всегда, но имеет смысл выработать хорошую привычку. Это окупается. - person qrdl; 20.02.2009
comment
@ Дэвид Дин - это просто пример для иллюстрации концепции. Таким образом очень легко выполнить правильную обработку ошибок. - person qrdl; 20.02.2009
comment
@ Дэвид Дин: если вы всегда пишете 1 элемент указанного размера, fwrite () возвращает либо 0, либо 1. - person Jonathan Leffler; 20.02.2009

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

int safe_fwrite(FILE *file, const void *data, size_t nbytes, unsigned int retries);
void print_and_exit(const char *message);

Тогда ваш основной код можно было бы записать как

#define RETRIES 5
if(!safe_fwrite(fp, &blah, sizeof blah, RETRIES))
  print_and_exit("Blah writing failed, aborting");
if(!safe_fwrite(fp, &foo, sizeof foo, RETRIES))
  print_and_exit("Foo writing failed, aborting");
person unwind    schedule 20.02.2009

Ваше первое решение выглядит нормально. Обычно goto err; бывает более удобным, так как вам может потребоваться какая-то общая часть очистки (например, перемотка в известное положение).

Чтобы сделать GCC тихим, просто сделайте:

(void)fwrite (&blah, sizeof (blah), 1, fp);
(void)fwrite (&foo, sizeof (foo), 1, fp);
person kmkaplan    schedule 20.02.2009

Почему бы вам не обернуть fwrite в какой-либо объект Writer и не выбросить исключение, если fwrite () возвращает код ошибки? Легко кодировать, легко использовать, легко управлять. ИМХО конечно. :)

person Bombe    schedule 20.02.2009

Может как то так? Вы обнаруживаете ошибки, не делая код слишком нечитабельным, и можете выполнить очистку после завершения ложного цикла.

#define WRITE_ERROR 100
#define WRITE_OK 0

int do_fwrite(void* ptr, size_t bytes, int fp) {
  if ( fwrite(ptr, bytes, 1, fp) != bytes ) return WRITE_ERROR;
  return WRITE_OK;
}

int my_func() {
   int errcode = 0;

   ...
   do {
     if ( errcode = do_fwrite(&blah, sizeof(blah), fp) ) break;
     ....
     if ( errcode = do_fwrite(&foo, sizeof(foo), fp) ) break;
     ....
     etc
   } while( false );

   fclose(fp);
   return errcode;
}
person Eric Petroelje    schedule 20.02.2009

Что-то вроде этого сработает

if (fwrite (&blah, sizeof (blah), 1, fp) != 1) throw SomeException;

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

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

if (fwrite (&blah, sizeof (blah), 1, fp) != 1) {
    //cleanup here
    throw SomeException;
}
person Glen    schedule 20.02.2009

Потенциально элегантным решением C для этого может быть что-то вроде этого (предупреждение - впереди непроверенный, некомпилированный код):


  size_t written;
  int    ok = 1;
  size_t num_elements = x;
  ok = (fwrite(stuff, sizeof(data), num_elements, outfile) == num_elements);

  if (ok) {
    ... do other stuff ...
  }

  ok = ok && (fwrite(stuff, sizeof(data), num_elements, outfile) == num_elements);

  if (ok) {
    ... etc etc ad nauseam ...
  }

  fclose(outfile);

  return ok;

Вышеупомянутое позволяет достичь двух целей одновременно:

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

К сожалению, «уродливые» блоки if (ok) необходимы, если вы не хотите везде использовать оценку короткого замыкания. Я видел, как этот шаблон использовался в сравнительно небольших функциях, использующих оценку короткого замыкания повсюду, и я думаю, что он, вероятно, лучше всего подходит для этого конкретного использования.

person Timo Geusch    schedule 20.02.2009
comment
единственная проблема в том, что ... делать другие вещи ... все еще выполняется, когда программа действительно должна просто завершиться (поскольку другие вещи в основном просто обрабатываются, чтобы выполнить следующую fwrite) - person David Dean; 20.02.2009
comment
Вы правы - я обновил код и сделал небольшую оговорку в ответе. - person Timo Geusch; 20.02.2009
comment
Вы можете написать if ((ok = ok && (fwrite (...)))) {, чтобы избежать блоков if, обернутых вокруг макроса, если вам нужен более читаемый код. - person fbonnet; 20.02.2009

Хорошо, учитывая, что я ищу c решение (без исключений), как насчет:

void safe_fwrite(data,size,count,fp) {
   if (fwrite(data,size,count,fp) != count) {
      printf("[ERROR] fwrite failed!\n");
      fclose(fp);
      exit(4);
   }
}

И тогда в моем коде у меня есть:

safe_fwrite (&blah, sizeof (blah), 1, fp);
// ... more code ...
safe_fwrite (&foo, sizeof (foo), 1, fp);
// ... more code ...
person David Dean    schedule 20.02.2009
comment
Выход из-за ошибки - это немного хардкорно, но если он соответствует потребностям вашего приложения, почему бы и нет. В этом случае не нужно беспокоиться об очистке, так что на самом деле это не отвечает на ваш исходный вопрос, а упаковка только снимает бремя. См. Мой ответ о широко распространенной «лучшей» практике. - person fbonnet; 20.02.2009