Это злоупотребление макросом?

Я перепроектировал какой-то код и наткнулся на это ...

/************************************************************************/
/*                                                                      */
/*                          MACRO CHECK_FREAD                           */
/*                                                                      */
/*  CHECK_FREAD is used to check the status of a file read.  It         */
/*   is passed the return code from read and a string to print out if   */
/*   an error is detected.  If an error is found, an error message is   */
/*   printed out and the program terminates.  This was made into a      */
/*   macro because it had to be done over and over and over . . .       */
/*                                                                      */
/************************************************************************/

#define CHECK_FREAD(X, msg)  if (X==-1) \
                 { \
                return(DCD_BADREAD); \
                 }

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

e.g.

/*  Read in an 4                */
    ret_val = read(fd, &input_integer, sizeof(int32));

    CHECK_FREAD(ret_val, "reading an 4");
    CHECK_FEOF(ret_val, "reading an 4");

    if (input_integer != 4)
    {
        return(DCD_BADFORMAT);
    }

    /*  Read in the number of atoms         */
    ret_val = read(fd, &input_integer, sizeof(int32));
    *N = input_integer;

    CHECK_FREAD(ret_val, "reading number of atoms");
    CHECK_FEOF(ret_val, "reading number of atoms");

Сейчас я только возвращаюсь к программированию на c / c ++ и вообще никогда не использовал много макросов, но разве это злоупотребление макросами?

Я читал это ... Когда макросы C ++ полезны?

... и это не похоже ни на один из кошерных примеров, поэтому я предполагаю, что "ДА". Но я хотел получить более информированное мнение, а не просто делать обоснованные предположения ... :)

... эээ, а не лучше ли использовать какую-нибудь обертку?


person Jason R. Mick    schedule 02.09.2010    source источник
comment
Этот макрос не выводит сообщение об ошибке, как следует в соответствии с его описанием в комментариях. Это полный код?   -  person Kirill V. Lyadvinsky    schedule 02.09.2010
comment
Да, это полный код. Что касается ChrisF, посмотрите пример, который я добавил.   -  person Jason R. Mick    schedule 02.09.2010
comment
И почему аргумент msg не используется? Комментарии не соответствуют реализации, что означает, что по крайней мере один из них содержит ошибки, но, вероятно, оба. Было бы лучше написать size_t checked_fread(void *buffer, size_t itemsize, size_t numitems, FILE *fp, const char *msg) функцию и использовать ее последовательно. Также интересно порассуждать, почему он проверяет по -1; fread() возвращает 0 или короткий счетчик ошибок, а тип возврата - size_t, что исключает возвращаемое значение -1 почти для всех целей.   -  person Jonathan Leffler    schedule 02.09.2010
comment
Но считается ли это злоупотреблением? Это мой вопрос. @Jonathan Я понятия не имею, почему они не используют msg ... забывчивость? лень? спутанность сознания? Вы выбираете! Но вы совершенно правы, это странно ....   -  person Jason R. Mick    schedule 02.09.2010
comment
Вероятно, они использовали атрибут msg при тестировании кода и убрали код печати перед его выпуском. Сначала я подумал об этом как о способе вставки комментариев в код, но почему бы вместо этого не использовать честные / * комментарии * /?   -  person teukkam    schedule 02.09.2010
comment
Злоупотребление против злоупотребления? Сложно сказать ... поскольку msg не используется, это несколько бессмысленно. Ясно, что комментарии по поводу fread() не имеют отношения к read(). Поскольку мы не видим CHECK_FEOF (), мы не можем сказать, что он делает. Однако мне кажется, что простая функция для обработки обоих условий была бы лучше: она могла бы обернуть функцию read() (системный вызов) и отделить статус ошибки от всего остального: if ((retval = wrapped_read(fd, &input_integer, sizeof(int32), "reading a 4")) != DCD_NOERROR) return retval;. И если тест на 4 будет повсеместным, я бы закончил это еще дальше. Я считаю, что это злоупотребление макросами.   -  person Jonathan Leffler    schedule 02.09.2010


Ответы (8)


Потому что это не то же самое. Если вы поместите то же самое в функцию, у вас будет короткая функция, которая возвращает DCD_BADWRITE, если X равно -1. Однако рассматриваемый макрос возвращается из содержащей функции. Пример:

int foobar(int value) {
    CHECK_FREAD(value, "Whatever");
    return 0;
}

расширится до:

int foobar(int value) {
    if (value == -1) {
        return (DCD_BADWRITE);
    };
    return 0;
}

Однако если бы это была функция, внешняя функция (foobar) с радостью проигнорировала бы результат и вернула бы 0.

Макрос, который так похож на функцию, но ведет себя по-разному в решающей степени, является основным IMO. Я бы сказал, да, это злоупотребление макросами.

person tdammers    schedule 02.09.2010
comment
Какая альтернатива, если они читают массу значений и хотят избавиться от повторяющегося кода? Есть ли хороший способ делать то, что они делают (см. Мой отредактированный пост для примера)? Единственное, что я могу придумать навскидку, это то, что они должны были разделить процесс чтения на разные части, поскольку он очень долго, поскольку файл имеет разные разделы ... - person Jason R. Mick; 02.09.2010
comment
В C ++ исключения. Поместите код в небольшую функцию, которая вместо злоупотребления возвращаемыми значениями для сообщения об ошибке выдает ошибку. Затем в коде using поместите все это в try / catch и верните DCD_BADWRITE из catch. Если это простой C, то обработка ошибок в стиле setjmp, вероятно, будет лучшим решением. И тогда, возможно, для гипотетической программы было бы лучше вообще использовать цикл. - person tdammers; 02.09.2010
comment
@ Джейсон: Я просто не считаю законным желание избавляться от повторяющегося кода здесь. Особенно, если учесть тот факт, что все сообщения полностью избыточны, их код после исключения даже не намного короче, чем if (retval != -1) return DCD_BADREAD. Конечно, писать или понимать не легче. - person Steve Jessop; 03.09.2010
comment
Сказать не использовать макрос в only случае, когда его стоит использовать (когда вам нужна текстовая подстановка, а не функция), немного смешно. - person alternative; 03.09.2010

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

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

person Mark Ransom    schedule 02.09.2010
comment
В C ++ для этого следует использовать throw. В языке C макрос с разумным названием часто является лучшей альтернативой. См. Мой полный ответ для получения дополнительной информации. - person supercat; 03.09.2010

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

Я бы сказал, что в целом вам следует избегать использования return в макросе. Трудно создать макрос, который правильно обрабатывает возврат, не сделав поток управления вызывающей его функцией неудобным или трудным для понимания. Вы определенно не хотите «случайно» вернуться из функции, даже не осознавая этого. Отладка этого может раздражать.

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

if (some_condition)
    CHECK_FREAD(val, "string")
else
    something_else();

не будет делать то, что вы думаете (else становится ассоциированным с if внутри макроса). Макрос должен быть заключен в { }, чтобы убедиться, что он не изменяет окружающие условные выражения.

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

char* messages[4];          // Array of output messages
char* pmsg = &messages[0];
....
CHECK_FREAD(val, pmsg++);

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

Вместо того, чтобы использовать макрос для всего этого, почему бы не написать функцию для инкапсуляции read и проверки ошибок? Он может вернуть ноль в случае успеха или код ошибки. Если код обработки ошибок является общим для всех блоков read, вы можете сделать что-то вроде этого:

int your_function(whatever) {
    int ret;
    ...

    if ((ret=read_helper(fd, &input_integer)) != 0)
        goto error_case;

    ...

    // Normal return from the function
    return 0;

error_case:
    print_error_message_for_code(ret);
    return ret;
}

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

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

person bta    schedule 02.09.2010

Является ли это злоупотреблением или нет - дело вкуса. Но я вижу в этом некоторые принципиальные проблемы.

  1. документация полностью неверна
  2. реализация очень опасна из-за if и возможной else проблемы с зависанием
  3. название не предполагает, что это предварительный возврат окружающей функции

так что это определенно очень плохой стиль.

person Jens Gustedt    schedule 02.09.2010

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

char *buf = malloc(1000);
if (buf == 0) return ENOMEM;

ret_val = read(fd, buf, 1000);
CHECK_READ(ret_val, "buffer read failed")

// do something with buf

free(buf);

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

Чтобы сделать то, что написано в документации, я бы предпочел написать что-то вроде:

check(retval != -1, "buffer read failed");

где check - функция. Или используйте assert. Конечно, assert делает что-либо, только если NDEBUG не установлен, поэтому он не годится для обработки ошибок, если вы планируете отдельные сборки отладки и выпуска.

Чтобы сделать то, что на самом деле делает макрос, я бы предпочел полностью пропустить макрос и написать:

if (retval == -1) return DCD_BADREAD;

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

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

Макросы, которые выполняют поток управления, могут быть неприемлемыми, но я думаю, только в тех случаях, когда они выглядят как поток управления и точно задокументированы. На ум приходят макросы для совместной подпрограммы Саймона Тэтхема - некоторым людям в первую очередь не нравятся совместные подпрограммы, но, если вы принимаете эту предпосылку, макрос с именем crReturn по крайней мере выглядит так, как будто он вернется. .

person Steve Jessop    schedule 02.09.2010
comment
Скорее всего, во время отладки полезно включить код, который записывает значение в msg и возвращает. Во время производства код регистрации не нужен. Если оставить макрос на месте, можно снова включить ведение журнала, если в этом возникнет необходимость. - person supercat; 03.09.2010
comment
@supercat: Итак, документация описывает поведение отладки, для которого в файле нет кода, но который программист отладки должен реализовать и вставить при необходимости? Вместо производственного поведения, для которого существует код? Тогда да, это, несомненно, колоссальная ругань ;-) - person Steve Jessop; 03.09.2010
comment
Скорее всего, код регистрации действительно существовал в какой-то момент в прошлом, но был удален. Я бы предпочел использовать вложенный макрос для части ведения журнала и использовать флаг LOGGING_ENABLE, чтобы либо определить этот вложенный макрос, чтобы что-то регистрировать, либо ничего не делать. - person supercat; 03.09.2010

Я бы рассмотрел пример злоупотребления макросом по двум причинам: (1) имя макроса не проясняет, что он делает, в первую очередь то, что он может возвращать; (2) Макрос синтаксически не эквивалентен оператору. Код вроде

  if (someCondition)
    CHECK_FREAD(whatever);
  else
    do_something_else();

не удастся. Мое предпочтительное изменение:

#define LOG_AND_RET_ERROR(msg) do {LOG_ERROR(msg); return DCD_BADREAD;} while(0)
  if (x==-1) LOG_AND_RET_ERROR(msg);
person supercat    schedule 02.09.2010

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

CHECK_FREAD(X, msg)

vs.

if (check_fread(X, msg)) return DCD_BADREAD;
person maxschlepzig    schedule 02.09.2010

В ограниченном пространстве это было реализовано, возможно, все было хорошо (хотя в C ++ это обычно не одобряется).

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

if (CHECK_FREAD(...)) {
   /* do stuff here */
} else {
   /* error handle */
}

Что явно неверно.

Кроме того, кажется, что msg не используется, если не используется / не ожидается в DCD_BADREAD, что ухудшает ситуацию ...

person ezpz    schedule 02.09.2010
comment
Недавно представленные разработчики будут делать глупые ошибки, например, использовать возвращаемое значение функции (или результат выражения), не утруждая себя поиском того, что это такое на самом деле. Нет особого смысла пытаться это объяснить. - person Steve Jessop; 02.09.2010
comment
@Steve: С другой стороны, представление им запутанных макросов кажется недобрым и контрпродуктивным. Особенно когда макрос плохо сформирован и документация вводит в заблуждение. - person David Thornley; 02.09.2010
comment
Верно, но будь то хороший макрос или плохой (и я согласен, что это плохой), то же самое можно сказать о любом макросе, который не расширяется, чтобы дать выражение: если кто-то пишет if (MACRO(...)) { ... } else { ... }, тогда пойдет не так. - person Steve Jessop; 03.09.2010