私は、コンストラクターから例外をスローすることについて同僚と議論していますが、フィードバックが必要だと思いました。
設計の観点から、コンストラクターから例外をスローしても大丈夫ですか?
クラスで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を作成し、pthread mutex_init
の戻り値に基づいてboolを返すpthread mutex_init
を呼び出す必要がありますか?このように、このような低レベルのオブジェクトに例外を使用する必要はありません。
はい、失敗したコンストラクターから例外をスローするのがこれを行う標準的な方法です。詳細については、このFAQ 失敗したコンストラクターの処理 をお読みください。 init()メソッドも機能しますが、mutexのオブジェクトを作成するすべての人は、init()を呼び出す必要があることを覚えておく必要があります。 RAII 原則に反すると感じます。
コンストラクターから例外をスローする場合、コンストラクター初期化リストでその例外をキャッチする必要がある場合は、関数try/catch構文を使用する必要があることに注意してください。
例えば.
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)
{
}
}
pthread_mutex_lock
は実際にEINVALを返すので、特定のケースではコンストラクターからスローする必要がないという事実とは別に、ミューテックスが初期化されていない場合 およびstd::mutex
で行われるように、lock
の呼び出し後にスローできます。
void
lock()
{
int __e = __gthread_mutex_lock(&_M_mutex);
// EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may)
if (__e)
__throw_system_error(__e);
}
次に、一般的にコンストラクターからのスローは、構築中の取得エラーに対してokであり、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
は、オブジェクトの状態がfailbit
フラグをチェックするis_open()
を呼び出してチェックします。
一見、標準的な方法で未定で、最初のケースではstd::mutex
はOP実装とは対照的に、初期化をスローしません*。 2番目の場合、std::mutex::lock
からスローされるものはすべてスローし、3番目のケースでは、スローはまったくありません。
違いに注意してください:
(1)静的に宣言でき、実際にメンバー変数として宣言されます(2)実際にメンバー変数として宣言されることは決してありません(3)メンバー変数として宣言されることが期待され、基礎となるリソースは常に利用できるとは限りません。
これらの形式はすべてRAIIです。これを解決するには、RAIIを分析する必要があります。
これは、構築時にすべてを初期化して接続する必要はありません。たとえば、ネットワーククライアントオブジェクトを作成する場合、作成時にサーバーに実際に接続することはありません。これは、失敗を伴う遅い操作であるためです。代わりにconnect
関数を作成して、まさにそれを行います。一方、バッファを作成するか、その状態を設定することもできます。
したがって、問題は最終的に初期状態を定義することになります。あなたの場合、初期状態がmutexを初期化する必要がありますの場合、コンストラクターからスローする必要があります。対照的に、(std::mutex
で行われるように)初期化せず、不変の状態を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
};
通常、プロジェクトが例外に依存して不良データと正常データを区別する場合、コンストラクターから例外をスローする方が、スローしないよりも優れたソリューションです。例外がスローされない場合、オブジェクトはゾンビ状態で初期化されます。そのようなオブジェクトは、オブジェクトが正しいかどうかを示すフラグを公開する必要があります。このようなもの:
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を返すときに正しく動作するかどうかという追加テストでカバーする必要があります。これは、ゾンビ状態でオブジェクトを初期化することは悪い考えであることを非常によく説明しています。
コンストラクタから例外をスローしないのは、プロジェクトに例外の使用に対するルールがある場合だけです(たとえば、 Google は例外を好まない)。その場合、コンストラクタで例外を使用するのは他のどこよりも望ましくなく、代わりに何らかのinitメソッドを用意する必要があります。
ここのすべての答えに加えて、CtorからではなくクラスのInit
メソッドから例外をスローすることを好む非常に具体的な理由/シナリオに言及するつもりでした(もちろん、より一般的なアプローチです) 。
この例(シナリオ)では、クラスのポインターデータメンバーに「スマートポインター」(つまり、std::unique_ptr
)を使用しないことを前提としています。
要点:場合によっては、Init()
メソッドがスローした例外をキャッチした後(この場合)、クラスのDtorを呼び出したときに「アクションを起こす」ことを望みます。 Ctorから例外をスローしないため、CtorのDtor呼び出しは「半ベイク」オブジェクトでは呼び出されません。
私のポイントを示すために、以下の例を参照してください。
#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;
}
繰り返しますが、これは推奨されるアプローチではなく、追加の観点を共有したかっただけです。
また、コードの一部の印刷からもわかるように、Scott Meyers(第1版)による幻想的な「より効果的なC++」の項目10に基づいています。
それが役に立てば幸い。
乾杯、
男。