Выбрасывание исключений из конструкторов
у меня есть дискуссия с коллегой о том, чтобы выбрасывать исключения из конструкторов, и я подумал, что хотел бы получить некоторую обратную связь.
это нормально, бросать исключения из конструкторов, с точки зрения дизайна?
допустим, я обертываю мьютекс POSIX в классе, это будет выглядеть примерно так:
class Mutex {
public:
Mutex() {
if (pthread_mutex_init(&mutex_, 0) != 0) {
throw MutexInitException();
}
}
~Mutex() {
pthread_mutex_destroy(&mutex_);
}
void lock() {
if (pthread_mutex_lock(&mutex_) != 0) {
throw MutexLockException();
}
}
void unlock() {
if (pthread_mutex_unlock(&mutex_) != 0) {
throw MutexUnlockException();
}
}
private:
pthread_mutex_t mutex_;
};
мой вопрос, это стандартный способ сделать это? Потому что если pthread mutex_init
вызов завершается неудачей объект мьютекса непригоден для использования, поэтому возникает исключение гарантирует, что мьютекс не будет создан.
Я должен скорее создать функцию-член init для класса мьютекса и вызвать pthread mutex_init
в пределах которого будет возвращен bool на основе pthread mutex_init
- вернуться? Таким образом, мне не нужно использовать исключения для такого объекта низкого уровня.
10 ответов:
да, выбрасывание исключения из неудачного конструктора является стандартным способом сделать это. Прочитайте этот FAQ о обработка конструктора, который не работает для получения дополнительной информации. Наличие метода init() также будет работать, но каждый, кто создает объект мьютекса, должен помнить, что init () должен быть вызван. Я чувствую, что это идет против RAII принципе.
Если вы создаете исключение из конструктора, имейте в виду, что вам нужно использовать синтаксис функции try/catch, если вам нужно поймать это исключение в списке инициализаторов конструктора.
например
func::func() : foo() { try {...} catch (...) // will NOT catch exceptions thrown from foo constructor { ... } }
и
func::func() try : foo() {...} catch (...) // will catch exceptions thrown from foo constructor { ... }
выбрасывание исключения-лучший способ справиться с ошибкой конструктора. В частности, вы должны избегать полуконструкции объекта, а затем полагаться на пользователей вашего класса для обнаружения сбоя конструкции путем тестирования переменных флага какого-либо рода.
в связи с этим меня немного беспокоит тот факт, что у вас есть несколько различных типов исключений для работы с ошибками мьютекса. Наследование-отличный инструмент, но его можно использовать слишком часто. В этом случае я бы, вероятно, предпочел один Исключение MutexError, возможно содержащее информативное сообщение об ошибке.
это нормально, чтобы бросить из конструктора, но вы должны убедиться, что ваш объект строится после главная началось и до этого отделка:
class A { public: A () { throw int (); } }; A a; // Implementation defined behaviour if exception is thrown (15.3/13) int main () { try { // Exception for 'a' not caught here. } catch (int) { } }
#include <iostream> class bar { public: bar() { std::cout << "bar() called" << std::endl; } ~bar() { std::cout << "~bar() called" << std::endl; } }; class foo { public: foo() : b(new bar()) { std::cout << "foo() called" << std::endl; throw "throw something"; } ~foo() { delete b; std::cout << "~foo() called" << std::endl; } private: bar *b; }; int main(void) { try { std::cout << "heap: new foo" << std::endl; foo *f = new foo(); } catch (const char *e) { std::cout << "heap exception: " << e << std::endl; } try { std::cout << "stack: foo" << std::endl; foo f; } catch (const char *e) { std::cout << "stack exception: " << e << std::endl; } return 0; }
вывод:
heap: new foo bar() called foo() called heap exception: throw something stack: foo bar() called foo() called stack exception: throw something
деструкторы не вызываются, так что если исключение должно быть брошено в конструкторе, много вещей(например, очистить?) делать.
Если ваш проект обычно полагается на исключения, чтобы отличить плохие данные от хороших данных, то выбрасывание исключения из конструктора является лучшим решением, чем не выбрасывание. Если исключение не выбрасывается, то объект инициализируется в состоянии зомби. Такой объект должен выставлять флаг, который говорит, является ли объект правильным или нет. Что-то вроде этого:
class Scaler { public: Scaler(double factor) { if (factor == 0) { _state = 0; } else { _state = 1; _factor = factor; } } double ScaleMe(double value) { if (!_state) throw "Invalid object state."; return value / _factor; } int IsValid() { return _status; } private: double _factor; int _state; }
проблема с этим подходом находится на стороне вызывающего абонента. Каждый пользователь класса должен был бы сделать если раньше фактически используя объект. Это вызов ошибок - нет ничего проще, чем забыть проверить условие перед продолжением.
в случае возникновения исключения из конструктора, сущность, которая создает объект, должна немедленно позаботиться о проблемах. Потребители объекта вниз по потоку могут свободно предположить, что объект на 100% работоспособен от простого факта, что они его получили.
эта дискуссия может продолжаться во многих направление.
например, использование исключений в качестве проверки является плохой практикой. Один из способов сделать это-попробовать шаблон в сочетании с заводским классом. Если вы уже используете фабрики, то напишите два метода:
class ScalerFactory { public: Scaler CreateScaler(double factor) { ... } int TryCreateScaler(double factor, Scaler **scaler) { ... }; }
С помощью этого решения вы можете получить флаг состояния на месте, как возвращаемое значение Заводского метода, никогда не вводя конструктор с плохими данными.
во-вторых, если вы покрываете код с автоматическими тестами. В этом случае каждый фрагмент кода, который использует объект, который не вызывает исключений, должен быть покрыт одним дополнительным тестом - правильно ли он действует, когда метод IsValid() возвращает false. Это хорошо объясняет, что инициализация объектов в состоянии зомби-плохая идея.
помимо то, что вам не нужно бросать из конструктора в вашем конкретном случае, потому что
pthread_mutex_lock
на самом деле возвращает EINVAL если мьютекс не был инициализирован и вы можете бросить после вызоваlock
как это сделано вstd::mutex
:void lock() { int __e = __gthread_mutex_lock(&_M_mutex); // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may) if (__e) __throw_system_error(__e); }
тогда вообще бросок от конструкторов в порядке на приобретение ошибки при строительстве, и в соответствии с RAII ( Resource-acquisition-is-Initialization ) парадигма программирования.
проверить это пример на RAII
void write_to_file (const std::string & message) { // mutex to protect file access (shared across threads) static std::mutex mutex; // lock mutex before accessing file std::lock_guard<std::mutex> lock(mutex); // try to open file std::ofstream file("example.txt"); if (!file.is_open()) throw std::runtime_error("unable to open file"); // write message to file file << message << std::endl; // file will be closed 1st when leaving scope (regardless of exception) // mutex will be unlocked 2nd (from lock destructor) when leaving // scope (regardless of exception) }
сосредоточьтесь на этих утверждениях:
static std::mutex mutex
std::lock_guard<std::mutex> lock(mutex);
std::ofstream file("example.txt");
первое утверждение-RAII и
noexcept
. В (2) ясно, что RAII применяется наlock_guard
и это на самом деле можетthrow
, тогда как в (3)ofstream
кажется, нет RAII, так как состояние объектов должно быть проверено путем вызоваis_open()
проверкиfailbit
флаг.на первый взгляд кажется, что он не определился, на что он свой стандартным способом в первом случае
std::mutex
не бросает в инициализации, *в отличие от реализации OP * . Во втором случае он будет бросать все, что создается отstd::mutex::lock
, а в третьем вообще нет броска.обратите внимание на различия:
(1) может будет объявлен статическим и фактически будет объявлен как переменная-член (2) фактически никогда не будет объявлено как переменная-член (3) ожидается, что он будет объявлен как переменная-член, и базовый ресурс не всегда может быть доступен.
все эти формы RAII; чтобы решить эту проблему, необходимо анализировать RAII.
- ресурс : объект
- приобретение (распределение): вы возражаете быть создано
- инициализации : ваш объект находится в его инвариантное состояние
это не требует от вас, чтобы инициализировать и подключить все по строительству. Например, когда вы создаете объект сетевого клиента, вы фактически не подключаете его к серверу при создании, поскольку это медленная операция со сбоями. Вместо этого вы бы написали
connect
функции для этого. С другой стороны, вы можете создать буферы или просто установить его государство.таким образом, ваш вопрос сводится к определению вашего начального состояния. Если в вашем случае ваше начальное состояние мьютекс должен быть инициализирован тогда вы должны бросить из конструктора. Напротив, это просто прекрасно не инициализировать тогда ( как это делается в
std::mutex
), и определить инвариантное состояние как мьютекс создан . Во всяком случае, инвариант не обязательно компрометируется состоянием объекта-члена, посколькуmutex_
объект мутирует междуlocked
иunlocked
доMutex
методыMutex::lock()
иMutex::unlock()
.class Mutex { private: int e; pthread_mutex_t mutex_; public: Mutex(): e(0) { e = pthread_mutex_init(&mutex_); } void lock() { e = pthread_mutex_lock(&mutex_); if( e == EINVAL ) { throw MutexInitException(); } else (e ) { throw MutexLockException(); } } // ... the rest of your class };
единственный раз, когда вы не будете выбрасывать исключения из конструкторов, если ваш проект имеет правило против использования исключений (например, Google не любит исключений). В этом случае вы не захотите использовать исключения в своем конструкторе больше, чем где-либо еще, и вместо этого вам нужно будет иметь какой-то метод init.
добавляя ко всем ответам здесь, я подумал упомянуть, очень конкретную причину / сценарий, где вы можете предпочесть выбросить исключение из класса
Init
метод, а не от Ctor (который, конечно, является предпочтительным и более распространенным подходом).отмечу заранее, что этот пример (сценарий) предполагает, что вы не используете "умные указатели" (т. е.-
std::unique_ptr
) для вашего класса с указателем(ы) данных.Итак, к делу: In case, вы хотите, чтобы Dtor вашего класса "принимал меры" при вызове его после того, как (для этого случая) вы поймаете исключение, что ваш
Init()
метод throw - вы не должны бросать исключение из Ctor, вызвать вызов Dtor для Ctor не вызываются на "полусырых" объектах.ниже пример, чтобы продемонстрировать мою точку зрения:
#include <iostream> using namespace std; class A { public: A(int a) : m_a(a) { cout << "A::A - setting m_a to:" << m_a << endl; } ~A() { cout << "A::~A" << endl; } int m_a; }; class B { public: B(int b) : m_b(b) { cout << "B::B - setting m_b to:" << m_b << endl; } ~B() { cout << "B::~B" << endl; } int m_b; }; class C { public: C(int a, int b, const string& str) : m_a(nullptr) , m_b(nullptr) , m_str(str) { m_a = new A(a); cout << "C::C - setting m_a to a newly A object created on the heap (address):" << m_a << endl; if (b == 0) { throw exception("sample exception to simulate situation where m_b was not fully initialized in class C ctor"); } m_b = new B(b); cout << "C::C - setting m_b to a newly B object created on the heap (address):" << m_b << endl; } ~C() { delete m_a; delete m_b; cout << "C::~C" << endl; } A* m_a; B* m_b; string m_str; }; class D { public: D() : m_a(nullptr) , m_b(nullptr) { cout << "D::D" << endl; } void InitD(int a, int b) { cout << "D::InitD" << endl; m_a = new A(a); throw exception("sample exception to simulate situation where m_b was not fully initialized in class D Init() method"); m_b = new B(b); } ~D() { delete m_a; delete m_b; cout << "D::~D" << endl; } A* m_a; B* m_b; }; void item10Usage() { cout << "item10Usage - start" << endl; // 1) invoke a normal creation of a C object - on the stack // Due to the fact that C's ctor throws an exception - its dtor // won't be invoked when we leave this scope { try { C c(1, 0, "str1"); } catch (const exception& e) { cout << "item10Usage - caught an exception when trying to create a C object on the stack:" << e.what() << endl; } } // 2) same as in 1) for a heap based C object - the explicit call to // C's dtor (delete pc) won't have any effect C* pc = 0; try { pc = new C(1, 0, "str2"); } catch (const exception& e) { cout << "item10Usage - caught an exception while trying to create a new C object on the heap:" << e.what() << endl; delete pc; // 2a) } // 3) Here, on the other hand, the call to delete pd will indeed // invoke D's dtor D* pd = new D(); try { pd->InitD(1,0); } catch (const exception& e) { cout << "item10Usage - caught an exception while trying to init a D object:" << e.what() << endl; delete pd; } cout << "\n \n item10Usage - end" << endl; } int main(int argc, char** argv) { cout << "main - start" << endl; item10Usage(); cout << "\n \n main - end" << endl; return 0; }
Я еще раз упомяну, что это не рекомендуемый подход, просто хотел поделиться дополнительным пунктом вид.
кроме того, как вы могли видеть из некоторых печатей в коде - он основан на пункте 10 в фантастическом "более эффективном C++" Скотта Мейерса (1-е издание).
надеюсь, что это помогает.
спасибо,
парень.