Подтвердить что ты не робот

Является ли шаблон дизайна GetLastError()? Это хороший механизм?

API Windows использует механизм GetLastError() для получения информации об ошибке или ошибке. Я рассматриваю тот же механизм обработки ошибок, поскольку я пишу API для проприетарного модуля. Мой вопрос в том, что лучше ли API возвращать код ошибки напрямую? Имеет ли GetLastError() какое-либо особое преимущество? Рассмотрим простой пример API Win32 ниже:

HANDLE hFile = CreateFile(sFile,
    GENERIC_WRITE, FILE_SHARE_READ,
    NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);

if (hFile == INVALID_HANDLE_VALUE)
{
    DWORD lrc = GetLastError();

    if (lrc == ERROR_FILE_EXISTS)
    {
          // msg box and so on
    }
}

Как я писал свои собственные API, я понял, что механизм GetLastError() означает, что CreateFile() должен установить последний код ошибки во всех точках выхода. Это может быть небольшая ошибка, если есть много точек выхода, и один из них может быть пропущен. Тупой вопрос, но так ли это, или это какой-то шаблон дизайна для него?

Альтернативой было бы предоставить дополнительный параметр функции, которая может непосредственно заполнить код ошибки, поэтому отдельный вызов GetLastError() не понадобится. Еще один подход может быть следующим. Я буду придерживаться вышеупомянутого API Win32, который является хорошим примером для анализатора этого. Здесь я меняю формат на это (гипотетически).

result =  CreateFile(hFile, sFile,
    GENERIC_WRITE, FILE_SHARE_READ,
    NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);

if (result == SUCCESS)
{
   // hFile has correct value, process it
}
else if (result == FILE_ALREADY_EXIT )
{
   // display message accordingly
  return;
}
else if ( result == INVALID_PATH )
{
   // display message accordingly.
  return;
}

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

4b9b3361

Ответ 1

В целом, это плохой дизайн. Это не относится к функции Windows GetLastError, системы Unix имеют одинаковую концепцию с глобальной переменной errno. Это потому, что это вывод функции, которая является неявной. Это имеет несколько неприятных последствий:

  • Две функции, выполняемые одновременно (в разных потоках), могут перезаписывать глобальный код ошибки. Поэтому вам может потребоваться код ошибки для каждого потока. Как было отмечено различными комментариями к этому ответу, это именно то, что делают GetLastError и errno - и если вы рассматриваете использование глобального кода ошибки для вашего API, тогда вам нужно будет сделать то же самое, если ваш API должен быть можно использовать из нескольких потоков.

  • Два вложенных вызова функций могут вызывать коды ошибок, если внешняя функция перезаписывает код ошибки, заданный внутренним.

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

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

Обычно условия ошибки являются исключительными. Они происходят не очень часто, но они могут. Конфигурационный файл, который вам нужен, может быть недоступен для чтения - но большую часть времени он есть. Для таких исключительных ошибок вам следует использовать исключения С++. Любая книга на С++, достойная ее соли, даст список причин, по которым исключения на любом языке (а не только на С++) хороши, но есть одна важная вещь, которую нужно рассмотреть, прежде чем все взволнованы:

Исключения разворачивают стек.

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

  • Весь код вызывающего абонента должен знать о наличии исключений, поэтому весь код, который приобретает ресурсы, должен иметь возможность выпускать их даже перед исключениями (в С++ метод "RAII" обычно используется для решайте их).

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

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

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

Ответ 2

Шаблон GetLastError на сегодняшний день наиболее подвержен ошибкам и наименее предпочтительным.

Возврат кода состояния enum является лучшим выбором.

Другой вариант, который вы не упоминали, но довольно популярен, заключался бы в том, чтобы бросать исключения для случаев сбоев. Это требует очень тщательного кодирования, если вы хотите сделать это правильно (а не утечка ресурсов или оставить объекты в полунастраиваемых состояниях), но приводит к очень элегантному коду, где вся основная логика находится в одном месте и обработка ошибок аккуратно отделяется.

Ответ 3

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

Другим раздражением GetLastError является то, что он требует двух уровней тестирования. Сначала вам нужно проверить код возврата, чтобы узнать, указывает ли он на ошибку, а затем вы должны вызвать GetLastError, чтобы получить ошибку. Это означает, что вам нужно сделать одну из двух вещей, не особенно элегантных:

1) Вы можете вернуть логическое значение, указывающее на успех или неудачу. Но тогда почему бы просто не вернуть код ошибки с нулем для успеха?

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

Ответ 4

Если ваш API находится в DLL, и вы хотите поддерживать клиентов, которые используют другой компилятор, тогда вы не сможете использовать исключения. Для исключений нет стандартного двоичного интерфейса.

Таким образом, вы в значительной степени должны использовать коды ошибок. Но не моделируйте систему, используя GetLastError в качестве образца. Если вам нужен хороший пример того, как возвращать коды ошибок, смотрите COM. Каждая функция возвращает HRESULT. Это позволяет вызывающим абонентам писать сжатый код, который может преобразовывать коды ошибок COM в собственные исключения. Вот так:

Check(pIntf->DoSomething());

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

pIntf->DoSomething(&status);
Check(status);

Или, что еще хуже, то, как это делается в Win32:

if (!pIntf->DoSomething())
    Check(GetLastError());

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

Ответ 5

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

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

В этом отношении глобальный обработчик ошибок является более прощающим:

  • Если функция не смогла вернуть логический или некоторый тип ErrorStatus для указания сбоя, нам не нужно изменять его тип подписи или возврата, чтобы указать на отказ и изменить код клиента по всему приложению. Мы можем просто изменить его реализацию, чтобы установить глобальный статус ошибки. Конечно, нам все равно придется добавлять проверки на стороне клиента, но если мы пропустим ошибку сразу на сайте вызова, есть еще возможность поймать ее позже.

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

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

Конечно, обработка исключений с надлежащей безопасностью исключений (RAII) на сегодняшний день является превосходным методом, но иногда обработка исключений не может быть использована (например: мы не должны выбрасывать границы модулей). Хотя глобальный обработчик ошибок, такой как Win API GetLastError или OpenGL glGetError, звучит как низкое решение со строгой инженерной точки зрения, он намного более прощает, чтобы модифицировать систему, а не начинать делать все, возвращая код ошибки, и начинать форсировать все вызовы этих функций чтобы проверить их.

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

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

Ответ 6

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

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

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

int a = foo();

вам нужно будет написать:

int a;
HANDLE_ERROR(foo(a));

Здесь HANDLE_ERROR может быть макросом, который проверяет код, возвращаемый из foo, и если это ошибка, он распространяет его (возвращает его).

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

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

Тогда вы заметите, что даже стека вызовов недостаточно. Во многих случаях код ошибки для "Файл не найден" в строке, которая говорит fopen (путь,...); не дает вам достаточно информации, чтобы узнать, в чем проблема. Какой файл не найден. На этом этапе вы можете расширить свои макросы, чтобы иметь возможность хранить массаж. И тогда вы можете указать фактический путь к файлу, который не был найден.

Вопрос в том, зачем беспокоиться обо всем этом, что вы могли бы сделать с исключениями. Несколько причин:

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

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

Ответ 7

Вы также должны рассмотреть переменную кода ошибки, основанной на объекте/структуре. Подобно библиотеке stdio C, она делает это для потоков FILE.

На некоторых из моих объектов io, например, я просто пропущу все дальнейшие операции, когда состояние ошибки задано так, чтобы пользователь был прав только при проверке ошибки один раз после последовательности операций.

Этот шаблон позволяет значительно улучшить схему обработки ошибок.

Один из плохих дизайнов C/С++ приходит сюда полностью, сравнивая его, например, с языком GO googles. Возврат только одного значения из функции. GO не использует исключения, а всегда возвращает два значения, результат и код ошибки.

Существует небольшая группа людей, которые считают, что исключения в большинстве случаев являются плохими и неправильными, потому что ошибки - это не исключения, а то, что вы должны ожидать. И он не доказал, что программное обеспечение становится более надежным и легким. Особенно в С++, где единственным способом программирования в настоящее время являются методы RIIA.