321

コンストラクターから例外をスローすることについて同僚と議論していて、フィードバックが欲しいと思いました。

設計の観点から、コンストラクターから例外をスローしても大丈夫ですか?

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クラスのメンバー関数initを作成し、そのpthread mutex_init中で呼び出して、の戻り値に基づいてブール値を返す必要がありpthread mutex_initますか?このようにして、このような低レベルのオブジェクトに例外を使用する必要はありません。

4

10 に答える 10

284

はい、失敗したコンストラクターから例外をスローすることが、これを行うための標準的な方法です。詳細については、失敗したコンストラクターの処理に関するこのFAQをお読みください。init()メソッドも機能しますが、mutexのオブジェクトを作成するすべての人は、init()を呼び出す必要があることを覚えておく必要があります。RAIIの原則に反しているように感じます。

于 2009-05-01T10:02:09.900 に答える
116

コンストラクターから例外をスローする場合、コンストラクター初期化子リストでその例外をキャッチする必要がある場合は、関数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
    { ... }
于 2009-05-01T10:31:14.057 に答える
35

例外をスローすることは、コンストラクターの失敗に対処するための最良の方法です。特に、オブジェクトを半分構築してから、クラスのユーザーに依存して、ある種のフラグ変数をテストすることにより、構築の失敗を検出することは避けてください。

関連する点として、ミューテックスエラーを処理するためのいくつかの異なる例外タイプがあるという事実は私を少し心配します。継承は優れたツールですが、使いすぎる可能性があります。この場合、おそらく有益なエラーメッセージを含む単一のMutexError例外を好むでしょう。

于 2009-05-01T10:12:52.527 に答える
17
#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

デストラクタは呼び出されないため、コンストラクタで例外をスローする必要がある場合は、多くの作業(クリーンアップなど)を行う必要があります。

于 2016-06-29T15:01:37.007 に答える
15

コンストラクターからスローすることは問題ありませんが、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)
  {
  }
}
于 2009-05-01T12:01:00.083 に答える
6

コンストラクターから例外をスローしないのは、プロジェクトに例外の使用を禁止するルールがある場合のみです(たとえば、Googleは例外を好みません)。その場合、コンストラクターで例外を他のどこよりも使用したくないので、代わりに何らかのinitメソッドを使用する必要があります。

于 2009-05-01T15:16:14.293 に答える
5

ここでのすべての答えに加えてInit、Ctorからではなく、クラスのメソッドから例外をスローすることを好む可能性がある非常に具体的な理由/シナリオ(もちろん、これは好ましい、より一般的なアプローチです)。

std::unique_ptrこの例(シナリオ)では、クラスのポインターデータメンバーに「スマートポインター」(つまり-)を使用しないことを前提としています。

つまり、要点:場合によっては、メソッドがスローした例外をキャッチした後(この場合)にクラスのDtorを呼び出すときに、クラスのDtorが「アクションを実行」することを望みますInit()。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に基づいています。

于 2018-04-10T14:54:08.343 に答える
4

プロジェクトが一般に、不良データと良好なデータを区別するために例外に依存している場合、コンストラクターから例外をスローする方が、スローしないよりも優れたソリューションです。例外がスローされない場合、オブジェクトはゾンビ状態で初期化されます。このようなオブジェクトは、オブジェクトが正しいかどうかを示すフラグを公開する必要があります。このようなもの:

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を返したときに正しく動作するかどうかという1つの追加テストでカバーする必要があります。これは、ゾンビ状態でオブジェクトを初期化することは悪い考えであることを非常によく説明しています。

于 2013-12-19T14:32:46.847 に答える
4

ミューテックスが初期化されていない場合は実際にEINVALを返し、の呼び出し後にスローできるため、特定のケースでコンストラクターからスローする必要がないという事実は別として、次のように実行されます。pthread_mutex_locklockstd::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)
}

これらのステートメントに焦点を当てます。

  1. static std::mutex mutex
  2. std::lock_guard<std::mutex> lock(mutex);
  3. std::ofstream file("example.txt");

最初のステートメントはRAIIとnoexceptです。lock_guard(2)では、RAIIが適用され、実際に適用できることは明らかですがthrow、(3)では、フラグをofstreamチェックする呼び出しによってオブジェクトの状態をチェックする必要があるため、RAIIではないようです。is_open()failbit

一見すると、それが標準的な方法であるかどうかは未定であり、最初のケースでstd::mutexは、* OP実装とは対照的に*、初期化をスローしません。2番目のケースでは、からスローされたものはstd::mutex::lockすべてスローされ、3番目のケースではスローはまったくありません。

違いに注意してください。

(1)静的に宣言でき、実際にはメンバー変数として宣言されます(2)実際にはメンバー変数として宣言されることは期待されません(3)メンバー変数として宣言されることが期待され、基になるリソースは常に利用できるとは限りません。

これらの形式はすべてRAIIです。これを解決するには、 RAIIを分析する必要があります。

  • リソース:オブジェクト
  • 取得(割り当て):作成中のオブジェクト
  • 初期化:オブジェクトは不変状態にあります

これには、構築時にすべてを初期化して接続する必要はありません。たとえば、ネットワーククライアントオブジェクトを作成する場合、失敗を伴う低速操作であるため、作成時に実際にサーバーに接続することはありません。代わりに、connectまさにそれを行う関数を記述します。一方、バッファを作成することも、単にその状態を設定することもできます。

したがって、問題は、初期状態の定義に要約されます。あなたの場合、初期状態がmutexである場合は、初期化する必要があり、コンストラクターからスローする必要があります。対照的に、(で行われているように)初期化せず、 mutexが作成されstd::mutexたときに不変状態を定義するのは問題ありません。いずれにせよ、オブジェクトはパブリックメソッドとの間で変化するため、不変条件は必ずしもそのメンバーオブジェクトの状態によって損なわれるわけではありません。mutex_lockedunlockedMutexMutex::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
};
于 2015-12-26T23:04:15.033 に答える
-2

私はプロレベルでC++を使用したことはありませんが、私の意見では、コンストラクターから例外をスローしても問題ありません。私は(必要に応じて).Netでそれを行います。これこのリンクをチェックしてください。それはあなたの興味があるかもしれません。

于 2009-05-01T10:07:25.597 に答える