Если make_shared/make_unique может генерировать bad_alloc, то почему использование для него блока try catch не является обычной практикой?

Страница CppReference для make_shared говорит (то же самое с make_unique)

Может генерировать std::bad_alloc или любое исключение, генерируемое конструктором T. Если генерируется исключение, функции не действуют.

Это означает, что в случае сбоя может быть запущено исключение std::bad_alloc. «функции не имеют никакого эффекта» неявно означает, что он не может возвращать nullptr. Если это так, то почему не всегда писать make_shared/make_unique в блоке try catch?

Как правильно использовать make_shared? Внутри блока try catch? или Проверка на nullptr?


person Boanerges    schedule 23.07.2019    source источник
comment
Одна из приятных особенностей исключений (в сочетании с RAII) заключается в том, что вам не нужно иметь дело с ошибками, когда и где они происходят, но вы можете сделать это в стеке вызовов, в том месте, где это удобно. Или никак, если ошибка фатальная.   -  person Some programmer dude    schedule 23.07.2019


Ответы (2)


Я вижу две основные причины.

  1. Сбой динамического выделения памяти часто считается сценарием, который не допускает корректного лечения. Программа завершается, и все. Это означает, что мы часто не проверяем все возможные std::bad_alloc. Или вы заключаете std::vector::push_back в блок try-catch, потому что базовый распределитель может выбросить?

  2. Не каждое возможное исключение должно быть перехвачено непосредственно на стороне вызова. Есть рекомендации, что отношение throw к catch должно быть намного больше единицы. Это означает, что вы перехватываете исключения на более высоком уровне, «собирая» несколько путей ошибок в один обработчик. Случай, который выдает конструктор T, также может быть обработан таким же образом. Ведь исключения бывают исключительными. Если при построении объектов в куче вероятность возникновения ошибок настолько велика, что вам приходится проверять каждый такой вызов, вам следует рассмотреть возможность использования другой схемы обработки ошибок (std::optional, std::expected и т. д.).

В любом случае, проверка nullptr определенно не правильный способ убедиться, что std::make_unique успешно выполнено. Он никогда не возвращает nullptr — либо он преуспевает, либо выбрасывает.

person lubgr    schedule 23.07.2019
comment
Это отвечает на мой вопрос @lubgr. Спасибо. Значит ли это, что ожидать исключения bad_alloc не обязательно, поскольку оно встречается редко? - person Boanerges; 23.07.2019
comment
Я бы сказал, что это зависит от платформы, на которую вы ориентируетесь. На обычном 64-битном настольном компьютере у вас обычно не бывает сбоев распределения, если вы не имеете дело с большими данными в памяти. Однако на некоторых встроенных устройствах все может быть совершенно иначе. - person lubgr; 23.07.2019
comment
Хорошо! Даже на таких встроенных устройствах с малым объемом памяти имеет смысл иметь универсальный механизм try-catch вместо использования блоков try/catch для каждого использования make_shared. Правильно? - person Boanerges; 23.07.2019
comment
Вы не можете обобщать это, я думаю, что на некоторых платформах даже нет кучи. Если выделение является проблемой на таком устройстве, вы должны в любом случае централизовать обработку ошибок распределения, используя настраиваемые распределители. - person lubgr; 23.07.2019

Бросок bad_alloc имеет два эффекта:

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

По умолчанию для этого четко определенного поведения процесс завершается ускоренным, но упорядоченным образом путем вызова std::terminate(). Обратите внимание, что это определяется реализацией (но, тем не менее, для данной реализации четко определено), будет ли раскручен стек перед вызовом terminate().

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

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

В большинстве случаев ответ заключается в том, что не должно.

Что будет делать обработчик? На самом деле есть два варианта:

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

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

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

И если ни один из этих подходов не дает никаких преимуществ, то лучший подход — просто включить обработку по умолчанию std::terminate().

person Jeremy    schedule 23.07.2019
comment
Просто включить обработку по умолчанию. Что это на самом деле означает, @Jeremy? - person Boanerges; 23.07.2019