コンストラクターから例外をスローする
-
03-07-2019 - |
質問
私は、コンストラクターから例外をスローすることについて同僚と議論していますが、フィードバックが必要だと思いました。
設計の観点から、コンストラクターから例外をスローしても大丈夫ですか?
クラスで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
呼び出しが失敗した場合、mutexオブジェクトは使用できないため、例外をスローすると、mutexが作成されなくなります。
Mutexクラスのメンバー関数initを作成し、<=>の呼び出しに基づいてboolを返す<=>を呼び出す必要がありますか?このように、このような低レベルのオブジェクトに対して例外を使用する必要はありません。
解決
はい、失敗したコンストラクターから例外をスローするのがこれを行う標準的な方法です。詳細については、失敗したコンストラクターの処理に関するよくある質問をご覧ください。 init()メソッドも機能しますが、mutexのオブジェクトを作成するすべての人は、init()を呼び出す必要があることを覚えておく必要があります。 RAII の原則に反すると思います。
他のヒント
コンストラクタから例外をスローする場合、コンストラクタ初期化リストでその例外をキャッチする必要がある場合は、関数try / catch構文を使用する必要があることに注意してください。
e.g。
func::func() : foo()
{
try {...}
catch (...) // will NOT catch exceptions thrown from foo constructor
{ ... }
}
vs。
func::func()
try : foo() {...}
catch (...) // will catch exceptions thrown from foo constructor
{ ... }
例外をスローすることは、コンストラクターの障害に対処する最良の方法です。特に、オブジェクトの半分の構築を避け、クラスのユーザーが何らかのフラグ変数をテストして構築の失敗を検出することを避けてください。
関連する点で、ミューテックスエラーを処理するためにいくつかの異なる例外タイプがあるという事実は、少し心配しています。継承は優れたツールですが、使いすぎる可能性があります。この場合、おそらく有益なエラーメッセージを含む単一のMutexError例外を好むでしょう。
#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
デストラクタは呼び出されないため、コンストラクタで例外をスローする必要がある場合は、多くのこと(クリーンアップなど)を行う必要があります。
コンストラクタからスローしても構いませんが、 オブジェクトは、 main の開始後、その前に構築されます 終了:
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)
{
}
}
プロジェクトが一般的に、悪いデータと良いデータを区別するために例外に依存している場合、コンストラクターから例外をスローすることは、スローしないよりも優れたソリューションです。例外がスローされない場合、オブジェクトはゾンビ状態で初期化されます。そのようなオブジェクトは、オブジェクトが正しいかどうかを示すフラグを公開する必要があります。このようなもの:
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;
}
このアプローチの問題は、呼び出し側にあります。クラスのすべてのユーザーは、実際にオブジェクトを使用する前にifを実行する必要があります。これはバグの呼び出しです-続行する前に条件をテストすることを忘れることほど簡単ではありません。
コンストラクターから例外をスローする場合、オブジェクトを構築するエンティティーが問題をすぐに処理することになっています。ストリームの下流のオブジェクトコンシューマーは、オブジェクトを取得したという単なる事実から、オブジェクトが100%操作可能であると自由に想定できます。
この議論は多くの方向に続くことができます。
たとえば、検証の問題として例外を使用することは悪い習慣です。それを行う1つの方法は、ファクトリクラスと組み合わせたTryパターンです。すでに工場を使用している場合は、次の2つのメソッドを記述します。
class ScalerFactory
{
public:
Scaler CreateScaler(double factor) { ... }
int TryCreateScaler(double factor, Scaler **scaler) { ... };
}
このソリューションを使用すると、ファクトリーメソッドの戻り値として、不正なデータでコンストラクターを入力することなく、ステータスフラグをインプレースで取得できます。
2番目のことは、コードを自動テストでカバーするかどうかです。その場合、例外をスローしないオブジェクトを使用するすべてのコードは、IsValid()メソッドがfalseを返すときに正しく動作するかどうかという追加テストでカバーする必要があります。これは、ゾンビ状態でオブジェクトを初期化することは悪い考えであることを非常によく説明しています。
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-初期化)プログラミングパラダイム。
を確認してください。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::lock
は初期化をスローしません* OPの実装とは対照的*。 2番目のケースでは、connect
からスローされたものはすべてスローされ、3番目のケースではスローはまったくありません。
違いに注意してください:
(1)静的に宣言でき、実際にはメンバー変数として宣言されます (2)実際にメンバー変数として宣言されることは期待されない (3)メンバー変数として宣言されることが期待されており、基礎となるリソースが常に利用できるとは限りません。
これらのフォームはすべて RAII です。これを解決するには、 RAII を分析する必要があります。
- リソース:オブジェクト
- 取得(割り当て):作成中のオブジェクト
- 初期化:オブジェクトは不変状態 にあります
これにより、構築時にすべてを初期化して接続する必要はありません。たとえば、ネットワーククライアントオブジェクトを作成する場合、作成時にサーバーに実際に接続することはありません。これは、失敗を伴う遅い操作であるためです。代わりにmutex_
関数を記述して、まさにそれを行います。一方、バッファを作成するか、単にその状態を設定できます。
したがって、問題は最終的に初期状態を定義することになります。あなたの場合、初期状態が mutexを初期化する必要があるの場合、コンストラクタからスローする必要があります。対照的に、(locked
で行われるように)初期化せずに mutexが作成されたとして不変状態を定義することは問題ありません。 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メソッドを用意する必要があります。
ここですべての答えに加えて、CtorからではなくクラスのInit
メソッドから例外をスローすることを好むかもしれない非常に具体的な理由/シナリオに言及するつもりでした(コースは優先されます)より一般的なアプローチ)。
この例(シナリオ)では、<!> quot; smart pointers <!> quot;を使用しないことを前提としています。 (つまり、-std::unique_ptr
)クラスの
sポインターデータメンバー。
要点:場合によっては、クラスのDtorが<!> quot;アクションを起こす<!> quot; (この場合)後に呼び出した場合、Init()
メソッドがスローした例外をキャッチします。Ctorから例外をスローしてはいけません。CtorのDtor呼び出しは<!> quot; half-bakedでは呼び出されません。 <!> quot;オブジェクト。
私のポイントを示すために、以下の例を参照してください:
#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;
}
再度言及しますが、これは推奨されるアプローチではなく、追加の視点を共有したいだけです。
また、コードの一部の印刷から見たように、素晴らしい<!> quot;より効果的なC ++ <!> quot;の項目10に基づいています。スコット・マイヤーズ(初版)。
お役に立てば幸いです。
乾杯、
男。