noexceptを実際に使用する必要があるのはいつですか?


509

noexceptキーワードが適切に多くの関数のシグネチャに適用することができますが、私は実際にそれを使用することを検討すべきときになどわからないと思います。これまでに読んだ内容に基づくと、のぎりぎりの追加はnoexcept、ムーブコンストラクターがスローするときに発生するいくつかの重要な問題に対処しているようです。ただし、noexceptそもそも詳細について読むためのいくつかの実際的な質問に対しては、まだ満足のいく答えを提供することはできません。

  1. 私がスローしないとわかっている関数の例はたくさんありますが、コンパイラーがそれ自体で決定することはできません。このような場合noexceptすべて関数宣言に追加する必要がありますか?

    すべての関数宣言のnoexcept後に追加する必要があるかどうかを考える必要があると、プログラマーの生産性が大幅に低下します(率直に言って、お尻の痛みになります)。どのような状況での使用についてより注意する必要がありますか。また、どのような状況で暗黙の状況を回避できますか?noexceptnoexcept(false)

  2. 使用後にパフォーマンスの改善が見られるのはいつ現実的に期待できnoexceptますか?特に、C ++コンパイラーがを追加した後でより優れたマシンコードを生成できるコードの例を示しますnoexcept

    個人的には、noexcept特定の種類の最適化を安全に適用するためにコンパイラーに提供される自由の増加のために気にしています。最近のコンパイラnoexceptはこのように利用していますか?そうでない場合、近いうちにそうすることを期待できますか?


40
を使用するコードmove_if_nothrow(またはwhatchamacallit)では、例外なくmove ctorがある場合、パフォーマンスが向上します。
R.マルティーニョフェルナンデス


5
ですmove_if_noexcept
ニコス

回答:


180

これを実際に使用するのに十分な時間がなかったため、「ベストプラクティス」の答えを出すのは時期尚早だと思います。スロー指定子が出た直後にこれについて尋ねられた場合、答えは今とは大きく異なります。

noexceptすべての関数宣言の後に追加する必要があるかどうかを考える必要があると、プログラマーの生産性が大幅に低下します(率直に言って、苦痛になります)。

それでは、関数がスローしないことが明らかな場合に使用します。

使用後にパフォーマンスの改善が見られるのはいつ現実的に期待できnoexceptますか?[...]個人的には、noexcept特定の種類の最適化を安全に適用するためにコンパイラーに提供された自由の増加のために気にしています。

最適化による最大の効果は、ユーザーによる最適化によるものであり、コンパイラーによるものではなく、チェックnoexceptとオーバーロードの可能性があるためです。ほとんどのコンパイラーは、例外なしの例外処理方法に従っているため、コードのマシンコードレベルで多くの(または何でも)変更されるとは思えませんが、おそらく、コードの処理。

使用noexcept四大に(彼らはすでにしているようなコンストラクタ、代入、デストラクタではないnoexceptとして可能性の高い最善の改善の原因になります)noexceptチェックは、このようなのようなテンプレートコードの「一般的」でありstd、容器。たとえば、std::vectorマークされていない限り、クラスの移動は使用しませんnoexcept(または、コンパイラはそれ以外の場合はそれを推測できます)。


6
std::terminateトリックは、まだゼロコストモデルに従っていると思います。つまり、スタックアンワインダーの代わりにif を使用noexceptすると、関数内の命令の範囲がマップされてcall になることがあります。したがって、通常の例外追跡よりもオーバーヘッドが多いのではないかと思います。std::terminatethrow
Matthieu M.

4
@Klaimこれを参照してください:stackoverflow.com/a/10128180/964135実際には、スローされない必要がありますが、noexcept保証されています。
Pubby

3
noexcept関数がスローstd::terminateすると、少しオーバーヘッドがかかるように見える」が呼び出されます...いいえ、これは、そのような関数の例外テーブルを生成しないことによって実装する必要があります。
Potatoswatter、2012年

8
@Pubby C ++例外処理は、通常、スローする可能性のある呼び出しサイトのアドレスをハンドラーのエントリポイントにマップするジャンプテーブルを除いて、オーバーヘッドなしで行われます。これらのテーブルの削除は、例外処理を完全に削除するのと同じくらい簡単です。唯一の違いは、実行可能ファイルのサイズです。おそらく何も言及する価値はありません。
Potatoswatter、2012年

26
「関数がスローしないことが明らかな場合に使用します。」同意しません。noexcept関数のインターフェースの一部です。現在の実装がスローしないという理由だけでそれを追加するべきではありません。この質問に対する正しい答えはわかりませんが、関数が今日どのように動作するかは、それとは関係がないと確信しています...
Nemo

133

私が最近繰り返しているように:セマンティクスを最初に

追加noexceptnoexcept(true)およびnoexcept(false)セマンティクスについてまず第一です。偶発的に、いくつかの可能な最適化が調整されるだけです。

プログラマーがコードを読んでいるとき、の存在はの存在noexceptと似てconstいます。これにより、発生する可能性のあるものと発生しない可能性のあるものを理解することができます。したがって、関数がスローするかどうかを知るかどうかを考えるのに時間をかける価値があります。念のため、あらゆる種類の動的メモリ割り当てがスローされる場合があります。


では、可能な最適化について説明しましょう。

最も明白な最適化は、実際にはライブラリで実行されます。C ++ 11は、関数が存在するかどうかを知ることができるいくつかの特性を提供します。noexcept標準ライブラリの実装自体は、それらの特性を使用してnoexcept、可能であれば、操作するユーザー定義オブジェクトの操作を優先します。このような動きの意味

それがあるため、コンパイラは、データを例外処理からの脂肪(多分)のビットを剃ることがあり、アカウントにあなたが嘘をついているかもしれないという事実を取ること。マークされた関数noexceptがスローする場合std::terminateは、呼び出されます。

これらのセマンティクスが選択された理由は2つあります。

  • noexcept依存関係がまだ使用していない場合でもすぐにメリットがあります(下位互換性)
  • noexcept理論的にはスローされる可能性があるが、指定された引数では予期されない関数を呼び出すときの指定を許可する

2
たぶん私は世間知らずかもしれませんが、関数のみを呼び出すnoexcept関数は、発生する可能性のある例外がterminateこのレベルに到達する前にトリガーされるため、特別なことをする必要がないと想像します。これは、bad_alloc例外を処理して伝播する必要があることとは大きく異なります。

8
はい、あなたが提案する方法でnoexceptを定義することは可能ですが、それは本当に使用できない機能です。多くの関数は、特定の条件が満たされていない場合にスローする可能性があり、条件が満たされていることがわかっていても、それらを呼び出すことができませんでした。たとえば、std :: invalid_argumentをスローする可能性がある関数。
tr3w 2013

3
@MatthieuM。返答には少し遅れますが、それでもなおです。noexceptとマークされた関数は、スローできる他の関数を呼び出すことができます。この関数は例外を発生させないことが約束されています。つまり、例外を自分で処理する必要があります。
Honf 2013

4
私はずっと前にこの回答に賛成しましたが、それについてもう少し読んで考えた後、コメント/質問があります。「セマンティクスを移動する」は、誰もが明らかに役立つ/良いアイデアをどこにでも与えるのを見た唯一のnoexceptです。ムーブの構築、ムーブの割り当て、スワップだけが唯一のケースだと思い始めています...他に何か知っていますか?
Nemo

5
@Nemo:標準ライブラリではおそらくそれだけですが、他の場所で再利用できる原則を示しています。移動操作とは、ある状態を一時的に "limbo"にする操作であり、その状態になって初めて、noexcept後でアクセスできるデータに対して自信を持って使用できるようになります。このアイデアが他の場所で使用されているのを見ることができましたが、標準ライブラリはC ++ではかなり薄く、私が思う要素のコピーを最適化するためにのみ使用されています。
Matthieu M.14年

77

これは実際には、コンパイラーのオプティマイザーに(潜在的に)大きな違いをもたらします。コンパイラーは、実際には、何年もの間、関数定義の後の空のthrow()ステートメントと適切な拡張機能によってこの機能を備えてきました。最近のコンパイラーがこの知識を利用してより良いコードを生成することを保証します。

コンパイラのほぼすべての最適化では、関数の「フローグラフ」と呼ばれるものを使用して、正当なものについて推論します。フローグラフは、一般に関数の「ブロック」と呼ばれるもの(1つの入口と1つの出口を持つコードの領域)とブロック間のエッジで構成され、フローのジャンプ先を示します。Noexceptはフローグラフを変更します。

あなたは特定の例を求めました。このコードを考えてみましょう:

void foo(int x) {
    try {
        bar();
        x = 5;
        // Other stuff which doesn't modify x, but might throw
    } catch(...) {
        // Don't modify x
    }

    baz(x); // Or other statement using x
}

barラベルが付けられている場合、この関数のフローグラフは異なりますnoexcept(実行の終わりbarとcatchステートメントの間をジャンプする方法はありません)。とラベル付けされているnoexcept場合、コンパイラは、baz関数の実行中にxの値が5であることを確認します。x= 5ブロックは、batch(x)ブロックを「支配」すると言われていbar()ます。

次に、「一定の伝播」と呼ばれる処理を実行して、より効率的なコードを生成できます。ここで、bazがインライン化されている場合、xを使用するステートメントには定数も含まれる可能性があり、実行時評価であったものをコンパイル時評価などに変えることができます。

とにかく、短い答え:noexceptコンパイラーに、よりタイトなフローグラフを生成させ、フローグラフを使用して、あらゆる種類の一般的なコンパイラー最適化を推論します。コンパイラーにとって、この性質のユーザー注釈は素晴らしいです。コンパイラはこのことを理解しようとしますが、通常は不可能です(問題の関数は、コンパイラから見えない別のオブジェクトファイルにあるか、見えない関数を推移的に使用している可能性があります)。気付かれていないためにスローされる可能性があるいくつかの些細な例外のため、暗黙的にラベルを付けることはできませんnoexcept(たとえば、メモリを割り当てるとbad_allocがスローされる場合があります)。


3
これは実際には実際に違いがありますか?これまでにx = 5も投げることができないため、この例は不自然です。tryブロックのその部分が何らかの目的を果たした場合、推論は成立しません。
Potatoswatter、2012年

7
私はそれがtry / catchブロックを含む関数を最適化することで本当の違いを生むと思います。私が示した例は、人為的ではありますが、完全ではありません。大きな点は、noexcept(その前のthrow()ステートメントのように)は、コンパイルが実行する多くの最適化の基本的な部分である、より小さいフローグラフ(エッジが少なく、ブロックが少ない)を生成するのに役立ちます。
Terry Mahaffey、2012年

コードが例外をスローできることをコンパイラはどのように認識できますか?配列へのアクセスは、例外として考えられますか?
Tomas Kubes 2015

3
@ qub1nコンパイラーが関数の本体を認識できる場合、コンパイラーは明示的なthrowステートメント、またはそれnewがスローする可能性のある他のものを探すことができます。コンパイラが本体を認識できない場合は、が存在するかどうかに依存する必要がありnoexceptます。通常の配列アクセスでは例外は生成されません(C ++には境界チェックがありません)。そのため、配列アクセスだけでは、関数が例外をスローしたとコンパイラが判断することにはなりません。(範囲外のアクセスはUBであり、保証された例外ではありません。)
cdhowie '16 / 07/15

@cdhowie "noexcept の存在または不在に依存している必要があります "またはthrow()prenoexcept C ++の存在
curiousguy 2018年

57

noexcept一部の操作のパフォーマンスを劇的に向上させることができます。これは、コンパイラーがマシンコードを生成するレベルでは発生しませんが、最も効果的なアルゴリズムを選択することで発生しますstd::move_if_noexcept。他の人が述べたように、この選択はfunctionを使用して行います。たとえば、の増加std::vector(たとえば、を呼び出すときreserve)は、強力な例外安全性保証を提供する必要があります。T移動コンストラクタがスローしないことがわかっている場合は、すべての要素を移動できます。それ以外の場合は、すべてTのをコピーする必要があります。これについては、この投稿で詳しく説明しています。


4
補遺:つまり、移動コンストラクターを定義するか、移動割り当て演算子noexceptをそれらに追加する場合(該当する場合)!暗黙的に定義された移動メンバー関数がnoexcept自動的に追加されました(該当する場合)。
mucaho 2015年

33

使用後にパフォーマンスの向上を観察する以外は、いつ現実的に使用できnoexceptますか?特に、noexceptを追加した後、C ++コンパイラーがより良いマシンコードを生成できるコードの例を示します。

えっと?時間はないですか?決して。

noexcept以下のためにあるのコンパイラと同じように、パフォーマンスの最適化constコンパイラのパフォーマンス最適化のためです。つまり、ほとんどありません。

noexcept主に、「あなた」がコンパイル時に関数が例外をスローできるかどうかを検出できるようにするために使用されます。注意:ほとんどのコンパイラーは、実際に何かをスローしない限り、例外に対して特別なコードを発行しません。したがって、関数の使用方法についてのヒントnoexceptを与えるの同じくらい、関数を最適化する方法についてのコンパイラのヒントを与えることではありません。

のようなテンプレートmove_if_noexceptは、移動コンストラクタが定義されているかどうかを検出し、定義されていない場合はタイプのの代わりにnoexceptを返します。安全に移動できるかどうかは、移動するという言い方です。const&&&

一般に、noexceptそうすることが実際に役立つと思われるときに使用する必要があります。一部のコードはis_nothrow_constructible、そのタイプに該当する場合、異なるパスを使用します。それを行うコードを使用している場合は、noexcept適切なコンストラクタを自由に使用してください。

つまり、移動コンストラクタや同様の構成要素に使用しますが、それに慣れる必要はありません。


13
厳密にmove_if_noexceptは、コピーを返さず、右辺値参照ではなくconst左辺値参照を返します。一般に、これにより呼び出し元は移動でmove_if_noexceptはなくコピーを作成しますが、コピーは実行していません。そうでなければ、素晴らしい説明。
ジョナサンウェイクリー、

12
+1ジョナサン。たとえば、ベクトルのサイズを変更すると、移動コンストラクタがの場合、オブジェクトをコピーする代わりに移動しますnoexcept。そのため、「決して」は真実ではありません。
mfontanini、2012年

4
つまり、コンパイラーその状況でより良いコード生成します。OPは、コンパイラーがより最適化されたアプリケーションを生成できる例を求めています。これはそうです(コンパイラの最適化ではありませんが)。
mfontanini、2012年

7
@mfontanini:コンパイラーは異なるコードパスをコンパイルするよう強制されるため、コンパイラーはより良いコードを生成するだけです。それは唯一のために動作しstd::vectorコンパイルするコンパイラを強制的に書かれている別のコードを。コンパイラが何かを検出することではありません。ユーザーコードが何かを検出することについてです。
Nicol Bolas、2012年

3
問題は、私があなたの答えの初めの引用に「コンパイラの最適化」を見つけることができないようです。@ChristianRauが言ったように、コンパイラーがより効率的なコードを生成するため、その最適化の起源が何であるかは問題ではありません。結局のところ、コンパイラーより効率的なコードを生成していますよね?PS:コンパイラの最適化だと言ったことはありません。「コンパイラの最適化ではない」とさえ言っています。
mfontanini、2012年

22

Bjarneの言葉(C ++言語、第4版プログラミング、ページ366):

終了が許容可能な応答である場合、キャッチされない例外は、それがterminate()(§13.5.2.5)の呼び出しに変わるため、それを達成します。また、noexcept指定子(§13.5.1.1)はその欲求を明示的にすることができます。

成功するフォールトトレラントシステムはマルチレベルです。各レベルは、歪みすぎずにできるだけ多くのエラーに対処し、残りをより高いレベルに残します。例外はそのビューをサポートします。さらに、terminate()例外処理メカニズム自体が破損している場合、またはメカニズムが不完全に使用されている場合にエスケープを提供することにより、 このビューをサポートし、例外をキャッチしません。同様に、 noexcept回復しようとしても実行不可能と思われるエラーを簡単に回避できます。

double compute(double x) noexcept;     {
    string s = "Courtney and Anya";
    vector<double> tmp(10);
    // ...
}

ベクトルコンストラクターは、10のdoubleのメモリを取得できず、をスローする場合がありstd::bad_allocます。その場合、プログラムは終了します。std::terminate()(§30.4.1.3)を呼び出すことで無条件に終了します。関数の呼び出しからデストラクタを呼び出すことはありません。これは、実装定義との間のスコープからデストラクタかどうかである thrownoexcept(例えば、計算に秒間())が呼び出されます。プログラムはまさに終了しようとしているので、とにかくオブジェクトに依存するべきではありません。指定子を追加するnoexceptことで、コードがスローに対処するように作成されていないことを示します。


2
この引用の出典はありますか?
アントンゴロフ2017

5
@AntonGolov "C ++プログラミング言語、第4版" pg。366
Rusty Shackleford 2017

1
これnoexceptは、例外を処理することを明示的に望んでいる場合を除いて、毎回実際に追加する必要があるかのように聞こえます。実際に考えてみましょう。ほとんどの例外はありそうにないか、致命的であるため、救助は合理的または不可能です。たとえば、引用された例では、割り当てが失敗すると、アプリケーションは正常に動作し続けることができなくなります。
Neonit

21
  1. 私がスローしないとわかっている関数の例はたくさんありますが、コンパイラーがそれ自体で決定することはできません。このような場合はすべて、関数宣言にnoexceptを追加する必要がありますか?

noexcept関数インターフェイスの一部であるため、注意が必要です。特に、ライブラリを作成している場合、クライアントコードはnoexceptプロパティに依存する可能性があります。既存のコードを壊す可能性があるため、後で変更するのは難しい場合があります。アプリケーションでのみ使用されるコードを実装する場合は、それほど心配する必要はありません。

スローできない関数がある場合は、それがとどまるのが好きか、それともnoexcept将来の実装を制限するだろうかと自問してください。たとえば、例外をスローすることにより(たとえば、単体テストの場合)不正な引数のエラーチェックを導入したり、例外の仕様を変更する可能性のある他のライブラリコードに依存したりする場合があります。その場合は、保守的にして省略した方が安全noexceptです。

一方、関数がスローしてはならないという確信があり、それが仕様の一部であることが正しい場合は、宣言する必要がありnoexceptます。ただし、noexcept実装が変更された場合、コンパイラは違反を検出できないことに注意してください。

  1. noexceptの使用についてもっと注意する必要があるのはどの状況ですか?暗黙のnoexcept(false)を回避できるのはどの状況ですか?

最大の影響を与える可能性が高いため、集中すべき関数のクラスは4つあります。

  1. 移動操作(割り当て演算子の移動とコンストラクターの移動)
  2. スワップ操作
  3. メモリ割り当て解除子(演算子の削除、演算子の削除[])
  4. デストラクタ(ただし、ユーザーnoexcept(true)が作成しない限り、これらは暗黙的に行われますnoexcept(false)

これらの関数は一般的にnoexceptである必要があり、ライブラリ実装がnoexceptプロパティを利用できる可能性が最も高いです。たとえば、std::vector強力な例外保証を犠牲にすることなく、スローしない移動操作を使用できます。それ以外の場合は、要素のコピーにフォールバックする必要があります(C ++ 98の場合と同様)。

この種の最適化はアルゴリズムレベルで行われ、コンパイラーの最適化に依存しません。特に要素のコピーにコストがかかる場合は、大きな影響を与える可能性があります。

  1. noexceptを使用した後、パフォーマンスの改善が見られるのはいつ現実的に期待できるでしょうか?特に、noexceptを追加した後、C ++コンパイラーがより良いマシンコードを生成できるコードの例を示します。

noexcept例外なしの仕様に対する利点、またはthrow()標準により、コンパイラーがスタックの巻き戻しに関してより自由にコンパイラーを使用できるようになります。このthrow()場合でも、コンパイラーはスタックを完全に巻き戻す必要があります(オブジェクト構造とまったく逆の順序で実行する必要があります)。

一方、このnoexcept場合、その必要はありません。スタックを巻き戻す必要はありません(ただし、コンパイラーは引き続き巻き戻すことができます)。この自由により、常にスタックを巻き戻すことができるというオーバーヘッドが低くなるため、コードをさらに最適化できます。

noexcept、スタックの巻き戻しとパフォーマンスに関する関連する質問では、スタックの巻き戻しが必要な場合のオーバーヘッドについて詳しく説明しています。

また、Scott Meyersの本「Effective Modern C ++」、「Item 14:Declare functions noexcept not exclude発信しないときは例外になる」も読んでください。


それでも、例外がJavaのようにC ++で実装されていて、スローする可能性のあるメソッドをthrowsnoexceptネガティブではなくキーワードでマークする場合は、はるかに意味があります。C ++のデザインの選択肢の一部を取得できない...
doc

彼らはすでに取られたnoexceptのでそれを名付けたthrow。簡単にthrow言えば、あなたが言うほとんどの方法で使用できますが、それらの設計がおかしくなり、ほとんど役に立たなくなりました-有害です。しかし、それを削除すると、ほとんどメリットのない重大な変更になるので、今はそれで行き詰まっています。noexcept基本的にそうですthrow_v2
AnorZaken

どうしてthrow役に立たないの?
curiousguy

@curiousguyそれ自体(例外をスローするため)の「スロー」は便利ですが、例外指定子としての「スロー」は非推奨になり、C ++ 17では削除さえされています。例外指定子が役に立たない理由については、次の質問を参照してください。stackoverflow.com
PhilippClaßenMay

1
@PhilippClaßen throw()例外指定子は、nothrow
curiousguy

17

私がスローしないとわかっている関数の例はたくさんありますが、コンパイラーがそれ自体で決定することはできません。このような場合はすべて、関数宣言にnoexceptを追加する必要がありますか?

「私は[彼らは]決して投げないことを知っている」と言うとき、あなたは関数の実装を調べることによって、その関数が投げないことを知っています。そのアプローチは裏返しだと思います。

関数が例外をスローして関数の設計の一部となるかどうかを検討することをお勧めしますconst。引数リストと同じくらい重要で、メソッドがミューテーター(... )であるかどうかです。「この関数は例外をスローしない」と宣言することは、実装上の制約です。省略しても、関数が例外をスローする可能性があります。つまり、関数の現在のバージョン将来のすべてのバージョンで例外がスローされる可能性があります。これは、実装を困難にする制約です。しかし、一部のメソッドには、実際に役立つ制約が必要です。最も重要なのは、デストラクタから呼び出すことができるためですが、強力な例外保証を提供するメソッドでの「ロールバック」コードの実装にも使用できます。


これは断然最良の答えです。あなたはあなたのメソッドのユーザーに保証をしている、それはあなたがあなたの実装を永遠に制約しているという別の言い方です(破壊的な変更を意味します)。啓発的な視点をありがとう。
AnorZaken

関連するJavaの質問も参照してください。
Raedwald、2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.