Является ли GetLastError () своего рода шаблоном дизайна? Это хороший механизм? - PullRequest
19 голосов
/ 01 февраля 2012

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

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() не понадобится.Еще один подход может быть, как показано ниже.Я буду придерживаться вышеупомянутого Win32 API, который является хорошим примером для анализа этого.Здесь я изменяю формат на этот (гипотетически).

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 или даже просто из функции, поскольку они оба одинаковы?

Ответы [ 7 ]

20 голосов
/ 01 февраля 2012

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

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

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

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

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

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

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

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

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

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

  3. Программы, работающие с обратными вызовами (например, обычными указателями на функции или даже системой 'signal & slot', используемой библиотекой Qt), обычно не ожидают, что вызываемая функция (слот) может выдать исключение, чтобы они не пытались его поймать.

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

7 голосов
/ 01 февраля 2012

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

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

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

3 голосов
/ 01 февраля 2012

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

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

Check(pIntf->DoSomething());

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

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

Или, что еще хуже, в Win32:

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

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

3 голосов
/ 01 февраля 2012

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

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

В этом отношении глобальный обработчик ошибок гораздо более прост:

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

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

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

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

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

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

3 голосов
/ 01 февраля 2012

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

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

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

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

1 голос
/ 06 октября 2013

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

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

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

int a = foo();

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

int a;
HANDLE_ERROR(foo(a));

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

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

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

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

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

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

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

0 голосов
/ 02 февраля 2012

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...