C++ コア ガイドライン:noexcept 指定子と演算子

残りの規則からエラー処理までざっと目を通すと、noexcept という言葉をよく目にします。エラー処理のルールについて書く前に、この投稿ではまず noexcept 指定子と noexcept 演算子について書きます。

例外なし

noexcept は C++11 以降、指定子と演算子の 2 つの形式で存在します。 C++ コア ガイドラインでは指定子を使用しています。

指定子としての noexcept

関数、メソッド、またはラムダ関数を noexcept として宣言することにより、これらが例外をスローしないことを指定し、例外がスローされても気にせず、プログラムをクラッシュさせます。簡単にするために、関数について書きますが、メソッドと関数テンプレートも意味します。意思表示にはさまざまな方法があります:

void func1() noexcept; // does not throw
void func2() noexcept(true); // does not throw
void func3() throw(); // does not throw

void func4() noexcept(false); // may throw

noexcept 指定は、noexcept(true) 指定と同等です。 throw() は noexcept(true) と同等ですが、C++11 で非推奨となり、C++20 で削除されます。対照的に、 noexcept(false) は、関数が例外をスローする可能性があることを意味します。 noexcept 指定は関数型の一部ですが、関数のオーバーロードには使用できません。

noexcept を使用する正当な理由が 2 つあります。まず、例外指定子が関数の動作を文書化します。関数が noexcept として指定されている場合、スローしない関数で安全に使用できます。 2 つ目は、コンパイラにとって最適化の機会です。 noexcept は を呼び出すことはできません std::unexpectedand はスタックをアンワインドしない可能性があります。移動コンストラクターが noexcept として宣言されている場合、コンテナーの初期化により、要素がコンテナーに安価に移動される可能性があります。 noexcept として宣言されていない場合、要素がコンテナーにコピーされるとコストがかかる可能性があります。

C++ の各関数は、スローされないか、スローされる可能性があります。投げる可能性のある手段:

<オール>
  • 関数は、スローする可能性のある関数を使用する場合があります。
  • 関数が noexcept 指定なしで宣言されています。
  • この関数は、参照型への dynamic_cast を使用しています。
  • ルール 2 には例外があり、noexcept が指定されていない場合、関数はスローされる可能性があります。これらの例外には、次の 6 つの特別なメンバー関数が含まれます。

    • デフォルトのコンストラクタとデストラクタ
    • コンストラクターの移動とコピー
    • 代入演算子の移動とコピー

    デストラクタなどのこの特別な 6 メンバーは、属性および基本クラスのすべてのデストラクタが非スローである場合にのみ非スローになります。もちろん、対応するステートメントは他の 5 つの特別なメンバー関数にも当てはまります。

    非スローとして宣言された関数で例外をスローするとどうなりますか?この場合、std::terminate が呼び出されます。 std::terminate は、デフォルトで std::abort を呼び出す現在インストールされている std::terminate_handler を呼び出します。その結果、プログラムが異常終了します。

    完全を期すために、演算子として noexcept を提示する必要があります。

    noexcept as operator

    noexcept 演算子は、式が例外をスローしないかどうかをコンパイル時にチェックします。 noexcept 演算子は式を評価しません。関数テンプレートの noexcept 指定子で使用して、関数が現在の型に応じて例外をスローする可能性があることを宣言できます。

    ここでの説明を明確にするために、戻り値をコピーする関数テンプレートの簡単な例を示します。

    // noexceptOperator.cpp
    
    #include <iostream>
    #include <array>
    #include <vector>
    
    class NoexceptCopy{
    public:
     std::array<int, 5> arr{1, 2, 3, 4, 5}; // (2)
    };
    
    class NonNoexceptCopy{
    public:
     std::vector<int> v{1, 2, 3, 4 , 5}; // (3)
    };
    
    template <typename T> 
    T copy(T const& src) noexcept(noexcept(T(src))){ // (1)
     return src; 
    }
    
    int main(){
     
     NoexceptCopy noexceptCopy;
     NonNoexceptCopy nonNoexceptCopy;
     
     std::cout << std::boolalpha << std::endl;
     
     std::cout << "noexcept(copy(noexceptCopy)): " << // (4)
     noexcept(copy(noexceptCopy)) << std::endl;
     
     std::cout << "noexcept(copy(nonNoexceptCopy)): " << // (5)
     noexcept(copy(nonNoexceptCopy)) << std::endl;
    
     std::cout << std::endl;
    
    }
    

    もちろん、この例で最も興味深いのは行 (1) です。特に、式 noexcept(noexcept(T(src)). 内側の noexcept は noexcept 演算子であり、外側の noexcept は noexcept 指定子です. 式 noexcept(T(src)) は、この場合、コピー コンストラクターがスローしないかどうかをチェックします。 .This is the case for the class Noexcept (2) but not for the class NonNoexcept (3) because the copy constructor of std::vector that may throw. その結果、式 (4) は true を返し、式 (5) false を返します。

    多分あなたはそれについて知っています。型 T にスローしないコピー コンストラクター std::is_nothrow_copy_constructible::value があるかどうかは、型特性ライブラリを使用してコンパイル時に確認できます。この述語に基づいて、noexcept 演算子の代わりに型特性ライブラリの述語を使用できます:

    template <typename T> 
    T copy(T const& src) noexcept(std::is_nothrow_copy_constructible<T>::value){
     return src; 
    }
    

    どのバージョンのコピーがお好みですか?より表現力があるため、型特性バージョンを好みます。

    次のルールは、noexcept 指定子に関するものです。

    E.12:noexcept throw のために関数を終了するとき 不可能または容認できない

    このルールのタイトルは、少しわかりにくいかもしれません。もし

    • スローしない
    • 例外が発生しても構いません。メモリ不足のために std::bad_alloc などの例外を処理できないため、プログラムをクラッシュさせても構わないと思っています。

    オブジェクトの直接の所有者である場合、例外をスローすることはお勧めできません。

    E.13:オブジェクトの直接所有者である間はスローしない

    以下は、ガイドラインから直接所有権を取得する例です:

    void leak(int x) // don't: may leak
    {
     auto p = new int{7};
     if (x < 0) throw Get_me_out_of_here{}; // may leak *p
     // ...
     delete p; // we may never get here
    }
    

    スローが発生すると、メモリが失われ、リークが発生します。簡単な解決策は、所有権を取り除き、C++ ランタイムをオブジェクトの直接の所有者にすることです。ローカル オブジェクトまたは少なくともガードをローカル オブジェクトとして作成するだけです。そして、C++ ランタイムがローカル オブジェクトを処理することを知っています。このアイデアの 3 つのバリエーションを次に示します。

    void leak(int x) // don't: may leak
    {
     auto p1 = int{7};
     auto p2 = std::make_unique<int>(7);
     auto p3 = std::vector<int>(7);
     if (x < 0) throw Get_me_out_of_here{}; 
     // ...
    }
    

    p1 はローカルに作成されますが、p2 と p3 はオブジェクトの一種のガードです。 std::vector はヒープを使用してデータを管理します。さらに、3 つのバリエーションすべてで、delete 呼び出しを取り除きます。

    次は?

    もちろん、例外とエラー処理に関する私の話は次の投稿に続きます。