Как обрабатывать ошибки в конструкторе класса C ++?

21

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

Мои вопросы

  1. Разрешено ли выполнять некоторые операции, кроме инициализации членов в конструкторе?

  2. Можно ли сказать вызывающей функции, что некоторые операции в конструкторе потерпели неудачу?

  3. Могу ли я сделать new ClassName()возврат NULL, если в конструкторе возникают ошибки?

MayurK
источник
22
Вы можете выбросить исключение из конструктора. Это полностью действующий шаблон.
Энди
1
Вы, вероятно, должны взглянуть на некоторые творческие модели GoF . Я рекомендую фабричный образец.
SpaceTrucker
2
Типичным примером № 1 является проверка данных. То есть, если у вас есть класс Square, с конструктором, который принимает один параметр, длину стороны, вы хотите проверить, больше ли это значение, чем 0.
Дэвид говорит: «Восстановите Монику
1
Что касается первого вопроса, позвольте мне предупредить вас, что виртуальные функции могут неинтуитивно вести себя в конструкторах. То же самое с деконструкторами. Остерегайтесь называть таких.
1
№ 3 - Почему вы хотите вернуть NULL? Одним из преимуществ ОО НЕ является необходимость проверки возвращаемых значений. Просто перехватите () соответствующие потенциальные исключения.
MrWonderful

Ответы:

42
  1. Да, хотя некоторые стандарты кодирования могут запрещать это.

  2. Да. Рекомендуемый способ - бросить исключение. Кроме того, вы можете хранить информацию об ошибке внутри объекта и предоставлять методы для доступа к этой информации.

  3. Нет.

Себастьян Редл
источник
4
Если объект все еще не находится в допустимом состоянии, даже если некоторая часть аргументов конструктора не соответствует требованиям и, таким образом, помечена как ошибка, 2) действительно не рекомендуется делать. Лучше, когда объект либо существует в допустимом состоянии, либо вообще не существует.
Энди
@DavidPacker Согласен, см. Здесь: stackoverflow.com/questions/77639/… Но некоторые руководящие принципы кодирования запрещают исключения, что проблематично для конструкторов.
Себастьян Редл
Почему-то я уже дал вам ответ на этот вопрос, Себастьян. Интересный. : D
Энди
10
@ooxi Нет, это не так. Ваш переопределенный new вызывается для выделения памяти, но вызов конструктора выполняется компилятором после возврата вашего оператора, что означает, что вы не можете перехватить ошибку. Это при условии, что новое называется вообще; это не для объектов, размещенных в стеке, которых должно быть большинство из них.
Себастьян Редл
1
Для # 1, RAII является распространенным примером, где может потребоваться сделать больше в конструкторе.
Эрик
20

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

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

Косвенный вызов конструктора часто называют «фабрикой».

Это также позволит вам вернуть нулевой объект, что может быть лучшим решением, чем возвращение нулевого.

значение NULL
источник
Спасибо @null! К сожалению, не могу принять два ответа здесь :( В противном случае я бы тоже принял этот ответ !!
Еще
Не @MayurK никаких забот, то общепринятый ответ не отметить на правильный ответ, но тот , который работает для вас.
ноль
3
@null: В C ++ вы не можете просто вернуться NULL. Например, int foo() { return NULLвы бы на самом деле возвращали 0(ноль) целочисленный объект. В std::string foo() { return NULL; }вы случайно называют std::string::string((const char*)NULL)что Неопределенное поведение (NULL не указывает на \ 0 с завершающим строкой).
MSalters
3
std :: option может быть далеко, но вы всегда можете использовать boost :: option, если вы хотите пойти по этому пути.
Шон Бертон,
1
@Vld: В C ++ объекты не ограничены типами классов. И с общим программированием, весьма обычно заканчивать с фабриками для int. Например std::allocator<int>, это совершенно нормальная фабрика.
MSalters
5

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

TL; DR = есть правило стиля, чтобы конструкторы были простыми, есть причины для этого, но эти причины в основном связаны с историческим (или просто плохим) стилем кодирования. Обработка исключений в конструкторах четко определена, и деструкторы все равно будут вызываться для полностью сконструированных локальных переменных и членов, что означает, что в идиоматическом коде C ++ не должно быть никаких проблем. Правило стиля в любом случае сохраняется, но обычно это не проблема - не вся инициализация должна быть в конструкторе, и, в частности, не обязательно в этом конструкторе.


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

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

Причины связаны с автоматической очисткой деструктора, особенно перед лицом исключений. По сути, должен быть четко определенный момент, когда компилятор становится ответственным за вызов деструкторов. Пока вы все еще находитесь в вызове конструктора, объект не обязательно полностью построен, поэтому недопустимо вызывать деструктор для этого объекта. Следовательно, ответственность за уничтожение объекта переходит к компилятору только после успешного завершения конструктора. Это называется RAII (Resource Allocation Is Initialization), который на самом деле не лучшее название.

Если в конструкторе происходит выброс исключительной ситуации, все, что частично сконструировано, должно быть явно очищено, обычно в try .. catch.

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

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

Тело этого конструктора пусто. До тех пор , как конструкторы base1, member2и member3являются безопасными исключением, нет ничего страшного. Например, если конструктор member2throws, этот конструктор отвечает за очистку себя. База base1была уже полностью построена, поэтому ее деструктор будет вызываться автоматически. member3никогда не был даже частично построен, поэтому не нуждается в очистке.

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

Есть и другие причины, по которым люди приводят предпочтение конструкторов «сделай по минимуму». Во-первых, просто потому, что существует правило стиля, многие люди считают, что вызовы конструктора дешевы. Один из способов получить это, но при этом иметь сильные инварианты, - это иметь отдельный класс фабрики / компоновщика, который имеет вместо этого ослабленные инварианты и который устанавливает необходимое начальное значение, используя (потенциально много) обычных вызовов функций-членов. Получив начальное состояние, которое вам нужно, передайте этот объект в качестве аргумента конструктору класса с сильными инвариантами. Это может "украсть кишки" объекта слабых инвариантов - семантики перемещения - что является дешевой (и обычно noexcept) операцией.

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

Steve314
источник
Абзац, где вы пишете «Пока вы все еще в вызове конструктора, объект не обязательно полностью построен, поэтому недопустимо вызывать деструктор для этого объекта. Следовательно, ответственность за разрушение объекта переносится только на компилятор когда конструктор успешно завершает работу "может действительно использовать обновление, касающееся делегирования конструкторов. Объект полностью построен , когда любая наиболее полученные из конструктора закончена, и деструктора будет называться , если исключение происходит внутри конструктора делегирующий.
Бен Фойгт
Таким образом, конструктор «сделай минимум» может быть закрытым, а функция «make_whwhat ()» может быть другим конструктором, который вызывает закрытый.
Бен Фойгт
Это не определение RAII, с которым я знаком. Мое понимание RAII состоит в том, чтобы преднамеренно получить ресурс в (и только в) конструкторе объекта и освободить его в деструкторе. Таким образом, объект может использоваться в стеке для автоматического управления получением и освобождением ресурсов, которые он инкапсулирует. Классический пример - замок, который получает мьютекс при его создании и освобождает его при разрушении.
Эрик
1
@Eric - Да, это абсолютно стандартная практика - стандартная практика, которая обычно называется RAII. Это не только я, что расширяет определение - это даже Страуструп, в некоторых беседах. Да, RAII о связывании жизненных циклов ресурсов с жизненными циклами объектов, ментальной моделью является владение.
Steve314
1
@Eric - предыдущие ответы удалены, потому что они плохо объяснены. В любом случае, сами объекты являются ресурсами, которыми можно владеть. Все должно иметь владельца, в цепочке вплоть до mainфункции или статических / глобальных переменных. Объект, выделенный с использованием new, не принадлежит, пока вы не назначите эту ответственность, но умные указатели владеют объектами, выделенными в куче, на которые они ссылаются, и контейнеры владеют их структурами данных. Владельцы могут решить удалить рано, владельцы деструктор несет полную ответственность.
Steve314