3

Modern C++ では、例外の安全性は非常に重要です。

例外の安全性については、すでに素晴らしい質問があります。したがって、一般的な例外の安全性について話しているわけではありません。私は本当に、C++ での Qt の例外安全性について話しているのです。Stack Overflow での Qt 例外の安全性に関する質問もあり、Qt のドキュメントがあります。

Qt での例外の安全性について見つけたすべてを読んだ後、Qt で例外の安全性を達成するのは非常に難しいと感じています。その結果、私は自分で例外をスローするつもりはありません。

本当の問題は std::bad_alloc にあります:

  • Qt のドキュメントには、Qt のシグナルスロット接続メカニズムによって呼び出されたスロットからの例外のスローは、スロット内で処理されない限り、未定義の動作と見なされると記載されています
  • 私の知る限り、Qt のどのスロットも std::bad_alloc をスローする可能性があります。

std::bad_alloc がスローされる前にアプリケーションを終了することが唯一の合理的なオプションのように思えます(私は実際には未定義の動作領域に入りたくありません)。

これを実現する方法は、演算子 new and をオーバーロードすることです。

  • GUI スレッドで割り当てエラーが発生した場合: アプリケーションを終了 (強制終了) します。
  • 別のスレッドで割り当てエラーが発生した場合は、std::bad_alloc をスローするだけです。

その演算子 new を作成する前に、フィードバックをいただければ幸いです。

  1. それは良い考えですか?
  2. 私のコードはこの方法で例外セーフになりますか?
  3. Qt で例外安全なコードを書くことさえ可能ですか?
4

3 に答える 3

4

この問題は長い間解決されており、Qt には慣用的な解決策があります。

すべてのスロット呼び出しは、最終的に次のいずれかから発生します。

  • イベントハンドラ、例えば:

    • タイマーのtimeoutシグナルは、QTimera の処理から生じQTimerEventます。

    • QObejctaを処理すると、キューに入れられたスロット呼び出しが発生しQMetaCallEventます。

  • あなたが完全に制御できるコード、例えば:

    • main、または from QThread::run、またはfromの実装でシグナルを発信する場合QRunnable::run

オブジェクト内のイベント ハンドラは、常に を通じて到達しQCoreApplication::notifyます。したがって、アプリケーション クラスをサブクラス化し、notify メソッドを再実装するだけです。

これは、イベント ハンドラーから発生するすべてのシグナル スロット呼び出しに影響します。具体的には:

  1. イベントハンドラから発生したすべてのシグナルとそれらに直接接続されたスロット

    これにより、信号ごとのコストではなく、スロットごとのコストではなく、イベントごとのコストが追加されます。なぜ違いが重要なのですか?多くのコントロールは、1 つのイベントごとに複数のシグナルを発行します。はQPushButton、 に反応して、 、または、およびをすべて同じイベントからQMouseEvent放出できます。複数のシグナルが発行されているにもかかわらず、が呼び出されたのは 1 回だけです。clicked(bool)pressed()released()toggled(bool)notify

  2. キューに入れられたすべてのスロット呼び出しとメソッド呼び出し

    これらはQMetaCallEvent、レシーバー オブジェクトに a をディスパッチすることによって実装されます。呼び出しは によって実行されQObject::eventます。イベント配信が絡むのでnotify使用します。コストはコール呼び出しごとです (したがって、スロットごとです)。このコストは、必要に応じて簡単に軽減できます (実装を参照)。

イベントハンドラーからではなく、たとえばmain関数の内部からシグナルを発信し、スロットが直接接続されている場合、この処理方法は明らかに機能しません。シグナルの発信を試してラップする必要があります。 /キャッチブロック。

QCoreApplication::notifyは配信されたすべてのイベントに対して呼び出されるため、このメソッドの唯一のオーバーヘッドは、try/catch ブロックと基本実装のメソッド呼び出しのコストです。後者は小さい。

前者は、マークされたオブジェクトに通知をラップするだけで軽減できます。これは、オブジェクトのサイズを犠牲にすることなく、補助データ構造のルックアップを伴わずに行う必要があります。これらの余分なコストは、例外がスローされない try/catch ブロックのコストを超えます。

「マーク」はオブジェクト自体から取得する必要があります。そこに可能性があります: QObject::d_ptr->unused. 残念ながら、これはそうではありません。なぜなら、そのメンバーはオブジェクトのコンストラクターで初期化されていないため、ゼロに設定されていることに依存することはできません。このようなマークを使用するソリューションでは、Qt 自体を少し変更する必要があります (unused = 0;行を に追加QObjectPrivate::QObjectPrivate)。

コード:

template <typename BaseApp> class SafeNotifyApp : public BaseApp {
  bool m_wrapMetaCalls;
public:
  SafeNotifyApp(int & argc, char ** argv) : 
    BaseApp(argc, argv), m_wrapMetaCalls(false) {}
  void setWrapMetaCalls(bool w) { m_wrapMetaCalls = w; }
  bool doesWrapMetaCalls() const { return m_wrapMetaCalls; }
  bool notify(QObject * receiver, QEvent * e) Q_DECL_OVERRIDE {
    if (! m_wrapMetaCalls && e->type() == QEvent::MetaCall) {
      // This test is presumed to have a lower cost than the try-catch
      return BaseApp::notify(receiver, e);
    }
    try {
      return BaseApp::notify(receiver, e);
    }
    catch (const std::bad_alloc&) {
      // do something clever
    }
  }
};

int main(int argc, char ** argv) {
  SafeNotifyApp<QApplication> a(argc, argv);
  ...
}

特定の状況で を処理することが意味があるかどうかを完全に無視していることに注意してくださいstd::bad_alloc。単にそれを処理するだけでは、例外の安全性にはなりません

于 2014-03-18T15:49:20.060 に答える
3

オーバーロードほど複雑なものは必要ありませんoperator newExceptionGuardデストラクタがチェックするクラスを作成しますstd::uncaught_exception。try-catch ブロックの外側で、自動持続時間で各スロットにこのオブジェクトを作成します。それでもエスケープする例外がある場合は、std::terminateそうでなければ Qt に戻る直前に呼び出すことができます。

大きな利点は、すべてのランダムな呼び出しではなく、スロットだけに配置できることですnew。大きな欠点は、それを使用することを忘れる可能性があることです.

ところで、厳密に呼び出す必要はありませんstd::terminateExceptionGuardそれは最後の手段として意図されているので、私はまだそうすることをお勧めします. アプリケーション固有のクリーンアップを実行できます。スロットに固有のクリーンアップ動作がある場合ExceptionGuardは、通常のcatchブロックの外側で行うことをお勧めします。

于 2014-03-18T09:43:09.077 に答える