C++のnoexceptについて

AI実装検定のご案内

noexceptは、関数が例外を呼び出し元へ送出しないことを示すための例外仕様です。

基本的には、関数宣言の末尾に記述します。

void process() noexcept
{
    // 例外を外部へ送出しない処理
}

noexceptを指定した関数から例外が外へ出ようとすると、例外は呼び出し元へ伝播せず、std::terminate()が呼び出されます。

したがって、noexceptは「関数内で例外が絶対に発生しない」という意味ではありません。

より正確には、次のような契約を表します。

この関数から例外を外へ送出しない。例外が外へ出ようとした場合は、通常の例外処理を行わずプログラムを終了させる。

目次

noexceptの基本的な書き方

例外を送出しない関数

例外を外部へ送出しない関数には、次のようにnoexceptを指定します。

void f() noexcept
{
}

これは、次の書き方と同じ意味です。

void f() noexcept(true)
{
}

引数を省略したnoexceptは、noexcept(true)の省略形です。

例外を送出する可能性がある関数

noexcept(false)を指定すると、その関数は例外を送出する可能性があるものとして扱われます。

void g() noexcept(false)
{
}

一般的な関数では、次の2つは基本的に同じ分類になります。

void g1();
void g2() noexcept(false);

どちらも、例外を送出する可能性がある関数です。

ただし、テンプレートや条件付きnoexceptでは、noexcept(false)が明示的に使われることがあります。

noexcept関数から例外が出た場合

std::terminate()が呼び出される

次のコードでは、noexceptが指定された関数から例外を送出しています。

#include <stdexcept>

void f() noexcept
{
    throw std::runtime_error("error");
}

int main()
{
    f();
}

f()から例外が外へ出ようとすると、std::terminate()が呼び出されます。

呼び出し側でtrycatchを使用しても、通常の例外として捕捉することはできません。

#include <iostream>
#include <stdexcept>

void f() noexcept
{
    throw std::runtime_error("error");
}

int main()
{
    try
    {
        f();
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << '\n';
    }
}

この場合、例外がf()の外へ伝播する前にstd::terminate()が呼ばれるため、catchブロックでは処理されません。

スタックの巻き戻しを期待してはいけない

noexcept違反によってstd::terminate()が呼ばれる場合、関数内のローカルオブジェクトが必ず破棄されるとは限りません。

struct Logger
{
    ~Logger()
    {
        // ログ出力
    }
};

void f() noexcept
{
    Logger logger;
    throw 1;
}

このコードでは、std::terminate()が呼ばれる前にLoggerのデストラクタが必ず実行されるとは限りません。

そのため、noexcept違反時の後処理やログ記録を、ローカルオブジェクトのデストラクタに依存させるべきではありません。

noexcept関数の内部で例外を処理する方法

関数内部で捕捉すれば問題ない

noexcept関数の内部で例外が発生すること自体は禁止されていません。

関数内部で例外を捕捉し、外へ出さなければnoexcept違反にはなりません。

#include <iostream>
#include <stdexcept>

void f() noexcept
{
    try
    {
        throw std::runtime_error("error");
    }
    catch (const std::exception& e)
    {
        std::cerr << "エラーを処理しました: "
                  << e.what() << '\n';
    }
}

この例では、例外がf()の内部で処理されているため、std::terminate()は呼び出されません。

再送出するとnoexcept違反になる

捕捉した例外を再送出した場合、その例外が関数の外へ出ようとするため、std::terminate()が呼ばれます。

void f() noexcept
{
    try
    {
        throw 1;
    }
    catch (...)
    {
        throw;
    }
}

catchしていても、最終的に例外を外へ出しているため、noexceptの契約には違反します。

noexceptを指定する主な目的

関数の例外保証を明確にする

noexceptを付けることで、その関数が例外を呼び出し元へ伝播させないことを明示できます。

たとえば、単純なgetterは例外を送出しないことが多いため、noexceptを指定しやすい関数です。

class User
{
public:
    int id() const noexcept
    {
        return id_;
    }

private:
    int id_{};
};

この宣言を見るだけで、利用者はid()が例外を送出しない関数であることを確認できます。

標準ライブラリが適切な処理を選択できる

noexceptは、std::vectorなどの標準コンテナが要素を移動するときにも重要です。

特に、ムーブコンストラクタがnoexceptかどうかによって、要素の再配置時にムーブとコピーのどちらが選択されるかが変わる場合があります。

最適化に役立つ場合がある

noexceptによって、コンパイラが例外伝播に関する制御フローを単純化できる場合があります。

ただし、noexceptを付ければ必ず高速になるわけではありません。

実務では、直接的な速度向上よりも、次の効果のほうが重要です。

  • 関数の例外保証を明確にできる
  • 標準コンテナが安全な処理を選択できる
  • 型特性によるコンパイル時判定ができる
  • テンプレートで処理を切り替えられる

ムーブコンストラクタとnoexcept

std::vectorの再配置に影響する

std::vectorは容量が不足すると、新しいメモリ領域を確保し、既存の要素を新しい領域へ移動します。

このとき、ムーブコンストラクタがnoexceptであれば、安全にムーブできると判断されやすくなります。

#include <string>
#include <utility>

class User
{
public:
    User(User&& other) noexcept
        : name_(std::move(other.name_))
    {
    }

private:
    std::string name_;
};

一方、ムーブコンストラクタにnoexceptが付いていない場合、型システム上はムーブ中に例外が発生する可能性があるものとして扱われます。

class User
{
public:
    User(User&& other)
        : name_(std::move(other.name_))
    {
    }

private:
    std::string name_;
};

ムーブ途中で例外が発生すると、一部の要素だけが移動済みになる可能性があります。

そのため、コピー可能な型では、標準コンテナが強い例外保証を維持するためにコピーを選択する場合があります。

std::move_if_noexceptの役割

標準ライブラリには、ムーブとコピーを状況に応じて使い分けるstd::move_if_noexceptがあります。

概念的には、次のような条件で処理が選択されます。

if constexpr (
    std::is_nothrow_move_constructible_v<T> ||
    !std::is_copy_constructible_v<T>
)
{
    // ムーブする
}
else
{
    // コピーする
}

使用例は次のとおりです。

T new_object(std::move_if_noexcept(old_object));

次のいずれかに該当する場合はムーブが選択されます。

  • ムーブコンストラクタがnoexcept
  • コピーコンストラクタが使用できない

それ以外の場合は、コピーが選択されます。

コピーできない型では例外保証に注意する

コピーできず、ムーブコンストラクタが例外を送出する可能性を持つ型では、ほかに選択肢がないためムーブが使われます。

struct MoveOnly
{
    MoveOnly(const MoveOnly&) = delete;
    MoveOnly(MoveOnly&&);
};

このような型では、ムーブ中に例外が発生した場合、処理によっては強い例外保証を維持できないことがあります。

std::move_if_noexceptがムーブを選択したからといって、必ず例外安全になるわけではありません。

ムーブ操作にnoexceptを付ける基準

ムーブコンストラクタやムーブ代入演算子が実際に例外を送出しないのであれば、noexceptを指定するのが一般的です。

class Resource
{
public:
    Resource(Resource&& other) noexcept;
    Resource& operator=(Resource&& other) noexcept;
};

ポインタやハンドルの所有権を移すだけの処理であれば、noexceptにできるケースが多くあります。

#include <cstddef>
#include <utility>

class Buffer
{
public:
    Buffer(Buffer&& other) noexcept
        : data_(std::exchange(other.data_, nullptr)),
          size_(std::exchange(other.size_, 0))
    {
    }

private:
    char* data_ = nullptr;
    std::size_t size_ = 0;
};

条件付きnoexceptとは

条件に応じて例外仕様を変えられる

テンプレートでは、内部で呼び出す処理が例外を送出しない場合だけ、関数をnoexceptにできます。

template <typename T>
void process(T& value)
    noexcept(noexcept(value.execute()))
{
    value.execute();
}

value.execute()が例外を送出しない場合、process()noexceptになります。

一方、value.execute()が例外を送出する可能性を持つ場合、process()も例外を送出する可能性がある関数になります。

外側と内側のnoexceptは役割が異なる

次の記述には、2つのnoexceptがあります。

noexcept(noexcept(value.execute()))

外側のnoexceptは関数の例外仕様です。

void function() noexcept(条件);

内側のnoexceptは、式が例外を送出する可能性を調べる演算子です。

noexcept(式)

この2つは同じ単語ですが、役割は異なります。

noexcept演算子の使い方

式が例外を送出する可能性を調べる

noexceptは演算子としても使用できます。

noexcept(式)

指定した式が例外を送出しないと判定されればtrue、例外を送出する可能性があればfalseになります。

void f() noexcept;
void g();

static_assert(noexcept(f()));
static_assert(!noexcept(g()));

noexcept演算子の式は実行されない

noexcept(式)の中に書かれた式は、実際には実行されません。

#include <iostream>

int f()
{
    std::cout << "実行されました\n";
    return 1;
}

int main()
{
    constexpr bool result = noexcept(f());
}

このコードでは、f()は呼び出されません。

noexcept演算子のオペランドは、実行時評価が行われない文脈です。

関数本体を解析して判定するわけではない

次の関数には、例外を送出する処理がありません。

void f()
{
}

しかし、宣言にnoexceptが付いていないため、通常は次の結果になります。

static_assert(!noexcept(f()));

noexcept演算子は、関数本体を解析して「実際には例外を投げなさそう」と判断しているわけではありません。

関数宣言の例外仕様を基に判定します。

void f() noexcept
{
}

static_assert(noexcept(f()));

引数評価も判定対象になる

次の関数自体はnoexceptです。

void f(int) noexcept;

しかし、引数を生成する処理が例外を送出する可能性を持つ場合、呼び出し式全体はnoexceptとは限りません。

int create_value();

static_assert(!noexcept(f(create_value())));

noexcept(f(create_value()))では、f()だけでなく、create_value()の評価も含めて判定されます。

単純な整数を渡す場合は、呼び出し式全体もnoexceptです。

static_assert(noexcept(f(10)));

実行時の挙動については、別に考える必要があります。

try
{
    f(create_value());
}
catch (...)
{
    // create_value()が投げた例外は捕捉できる
}

create_value()の例外はf()の実行開始前に発生するため、f()noexcept違反にはなりません。

ただし、noexcept演算子では、引数評価を含む式全体が判定対象です。

型特性を使ったnoexceptの確認

std::is_nothrow_move_constructible

型を例外なしでムーブ構築できるかを調べます。

#include <type_traits>

static_assert(
    std::is_nothrow_move_constructible_v<MyClass>
);

std::is_nothrow_move_assignable

型を例外なしでムーブ代入できるかを調べます。

static_assert(
    std::is_nothrow_move_assignable_v<MyClass>
);

std::is_nothrow_constructible

指定した引数で、例外なしにオブジェクトを構築できるかを調べます。

static_assert(
    std::is_nothrow_constructible_v<MyClass, int, double>
);

std::is_nothrow_invocable

関数や関数オブジェクトを、指定した引数で例外なしに呼び出せるかを調べます。

static_assert(
    std::is_nothrow_invocable_v<decltype(my_function), int>
);

std::is_nothrow_swappable

型を例外なしで交換できるかを調べます。

static_assert(
    std::is_nothrow_swappable_v<MyClass>
);

デフォルト化されたムーブ操作とnoexcept

コンパイラが例外仕様を推論する

デフォルト化されたムーブコンストラクタやムーブ代入演算子の例外仕様は、基底クラスやデータメンバーの操作を基に決まります。

#include <string>

class User
{
public:
    User(User&&) = default;
    User& operator=(User&&) = default;

private:
    std::string name_;
};

この場合、Userのムーブ操作がnoexceptになるかどうかは、std::stringなどのメンバー型のムーブ操作を基にコンパイラが判定します。

特別な理由がなければ、デフォルト化された特殊メンバー関数には明示的なnoexceptを付けず、コンパイラによる推論を利用する方法が安全です。

User(User&&) = default;
User& operator=(User&&) = default;

必要に応じて型特性で確認できます。

static_assert(
    std::is_nothrow_move_constructible_v<User>
);

明示的にnoexceptを付ける場合の注意

次のように明示することもできます。

User(User&&) noexcept = default;

ただし、将来クラスに例外を送出する可能性があるメンバーを追加すると、ムーブ中の例外がstd::terminate()につながる可能性があります。

明示的なnoexceptは、将来もその保証を維持できる場合に指定するのが適切です。

デストラクタとnoexcept

デストラクタから例外を外へ出さない

デストラクタから例外を外へ送出する設計は、原則として避けるべきです。

class File
{
public:
    ~File()
    {
        throw 1;
    }
};

多くのクラスでは、基底クラスやデータメンバーのデストラクタが例外を送出しないため、暗黙のデストラクタも例外を送出しないものになります。

ただし、すべてのデストラクタが無条件にnoexcept(true)になるわけではありません。

struct Member
{
    ~Member() noexcept(false);
};

struct Owner
{
    Member member;
};

この場合、Ownerのデストラクタの例外仕様は、Memberのデストラクタの影響を受けます。

スタック巻き戻し中の例外は危険

次の処理では、例外が送出された後、スタックの巻き戻しによってローカルオブジェクトが破棄されます。

void f()
{
    Object obj;
    throw std::runtime_error("first error");
}

このとき、objのデストラクタが別の例外を外へ送出すると、std::terminate()が呼ばれます。

struct X
{
    ~X() noexcept(false)
    {
        throw 2;
    }
};

void f()
{
    X x;
    throw 1;
}

そのため、デストラクタ内部で失敗する可能性がある処理を行う場合は、例外を内部で処理する必要があります。

class File
{
public:
    ~File() noexcept
    {
        try
        {
            close();
        }
        catch (...)
        {
            // ログ出力などを行う
        }
    }

private:
    void close();
};

重要なエラーを呼び出し側へ通知したい場合は、明示的なclose()関数を用意する方法があります。

class File
{
public:
    void close();

    ~File() noexcept
    {
        // 最終的な後始末を行う
    }
};

エラー処理が必要な場合は、利用者が明示的にclose()を呼び出します。

仮想関数とnoexcept

派生クラスで例外保証を弱めることはできない

基底クラスの仮想関数がnoexceptである場合、派生クラスのオーバーライド関数もnoexceptでなければなりません。

class Base
{
public:
    virtual void update() noexcept;
};

次のオーバーライドは不適切です。

class Derived : public Base
{
public:
    void update() override;
};

基底クラスでは例外を送出しない契約になっているため、派生クラス側でその保証を弱めることはできません。

正しくは次のように記述します。

class Derived : public Base
{
public:
    void update() noexcept override;
};

派生クラスで保証を強めることはできる

基底クラスが例外を送出する可能性を持ち、派生クラスでnoexceptを指定することは可能です。

class Base
{
public:
    virtual void update();
};

class Derived : public Base
{
public:
    void update() noexcept override;
};

この場合、派生クラスは基底クラスよりも強い例外保証を提供しています。

関数ポインタとnoexcept

C++17以降は関数型の一部になる

C++17以降、noexceptは関数型の一部として扱われます。

using SafeFunction = void (*)() noexcept;
using NormalFunction = void (*)();

noexcept関数は、通常の関数ポインタへ代入できます。

void safe() noexcept
{
}

void (*p1)() noexcept = safe;
void (*p2)() = safe;

一方、例外を送出する可能性がある関数を、noexcept関数ポインタへ代入することはできません。

void normal()
{
}

void (*p)() noexcept = normal;

pを通じて呼び出した関数が例外を送出しないことを保証できないためです。

noexceptだけではオーバーロードできない

noexceptの有無だけで、通常の関数を別々にオーバーロードすることはできません。

void f();
void f() noexcept;

これらは別のオーバーロードではなく、同じ関数に対する宣言として扱われます。

そのため、例外仕様には整合性が必要です。

ラムダ式とnoexcept

ラムダ式にも指定できる

ラムダ式にもnoexceptを指定できます。

auto add = [](int a, int b) noexcept
{
    return a + b;
};

noexcept演算子を使えば、例外仕様を確認できます。

static_assert(noexcept(add(1, 2)));

条件付きnoexceptも使用できる

呼び出し対象の例外仕様を、ラムダ式へ反映させることも可能です。

#include <utility>

auto call = [](auto&& function)
    noexcept(noexcept(
        std::forward<decltype(function)>(function)()
    ))
{
    return std::forward<decltype(function)>(function)();
};

渡された関数オブジェクトがnoexceptであれば、このラムダの呼び出しもnoexceptになります。

constexprnoexceptの違い

役割はそれぞれ異なる

constexprnoexceptは、異なる性質を表します。

constexpr int add(int a, int b) noexcept
{
    return a + b;
}

constexprは、条件を満たせばコンパイル時に評価できることを示します。

noexceptは、例外を関数の外へ送出しないことを示します。

主な違いは次のとおりです。

指定主な意味
constexpr条件を満たせばコンパイル時に評価できる
consteval必ずコンパイル時に評価する
noexcept例外を関数の外へ送出しない
constメンバー関数内でオブジェクトの状態を変更しない

これらは独立した指定なので、組み合わせて使用できます。

constexpr int size() const noexcept
{
    return size_;
}

noexceptを付けやすい関数

単純なgetter

int size() const noexcept
{
    return size_;
}

単純な値を返すだけで、例外を送出する処理がない場合は、noexceptを指定しやすい関数です。

単純なsetter

void set_enabled(bool value) noexcept
{
    enabled_ = value;
}

ただし、代入対象の型が例外を送出しないことが前提です。

std::stringやユーザー定義型への代入では、例外が発生する可能性があります。

ムーブコンストラクタとムーブ代入演算子

MyClass(MyClass&& other) noexcept;
MyClass& operator=(MyClass&& other) noexcept;

実際の処理が例外を送出しない場合は、積極的にnoexceptを検討できます。

リソース解放処理

void release() noexcept
{
    // ポインタやハンドルを解放する
}

ただし、解放処理中に失敗した場合の扱いは、あらかじめ設計しておく必要があります。

swap

メンバーを交換するswapも、例外を送出しない場合はnoexceptにできます。

#include <type_traits>
#include <utility>

template <typename T>
class Wrapper
{
public:
    void swap(Wrapper& other)
        noexcept(std::is_nothrow_swappable_v<T>)
    {
        using std::swap;
        swap(value_, other.value_);
    }

private:
    T value_;
};

メンバーのswapが例外を送出しない場合だけ、Wrapper::swap()noexceptになります。

noexceptを付ける際に注意が必要な関数

メモリ確保を行う関数

次の処理は、メモリ確保に失敗すると例外を送出する可能性があります。

void add_name(const std::string& name) noexcept
{
    names_.push_back(name);
}

std::vector::push_backや文字列のコピーでは、std::bad_allocなどが発生する可能性があります。

この関数にnoexceptを付けると、例外発生時にstd::terminate()が呼び出されます。

通常は次のように記述します。

void add_name(const std::string& name)
{
    names_.push_back(name);
}

例外を使ってエラーを通知する関数

例外を使って失敗を通知する関数には、noexceptを指定できません。

User find_user(int id);

ユーザーが見つからない場合に例外を送出する設計なら、次の宣言は不適切です。

User find_user(int id) noexcept;

例外が呼び出し側へ伝わらず、プログラムが終了するためです。

外部ライブラリを呼び出す関数

外部ライブラリの関数が例外を送出する可能性を持つ場合、安易にnoexceptを付けるべきではありません。

void execute() noexcept
{
    third_party_library_call();
}

外部ライブラリから例外が送出されると、std::terminate()が呼ばれます。

外部との境界で例外を止める必要がある場合は、関数内部で捕捉します。

void execute() noexcept
{
    try
    {
        third_party_library_call();
    }
    catch (...)
    {
        // ログ出力やエラーコードへの変換を行う
    }
}

コピー・アンド・スワップとnoexcept

関数本体と引数構築を分けて考える

コピー・アンド・スワップを使用した代入演算子では、値渡し引数の構築にも注意が必要です。

class Data
{
public:
    Data& operator=(Data other)
        noexcept(noexcept(swap(other)))
    {
        swap(other);
        return *this;
    }

    void swap(Data& other) noexcept
    {
        using std::swap;
        swap(value_, other.value_);
    }

private:
    int value_{};
};

値渡し引数のotherは、関数本体へ入る前にコピーまたはムーブによって構築されます。

Data a;
Data b;

a = b;

概念的には、次の処理が含まれています。

Data temporary(b);
a.operator=(std::move(temporary));

コピーコンストラクタが例外を送出する可能性を持つ場合、代入式全体はnoexceptになりません。

static_assert(!noexcept(a = b));

一方、operator=に指定したnoexceptは、関数本体から例外が外へ出ないことを表します。

引数の構築中に発生した例外は、関数本体の実行前に発生するため、operator=noexcept違反ではありません。

このように、関数自体の例外仕様と、呼び出し式全体の例外安全性は分けて考える必要があります。

古いthrow()との違い

現在はnoexceptを使用する

以前のC++では、例外を送出しない関数を次のように記述することがありました。

void f() throw();

現在は、次のようにnoexceptを使用します。

void f() noexcept;

また、以前は送出可能な例外型を列挙する動的例外仕様もありました。

void f() throw(std::runtime_error);

throw(Type1, Type2)のような動的例外仕様はC++17で削除されています。

空のthrow()はC++17ではnoexcept(true)と同様に扱われますが、新しいコードではnoexceptを使用するのが適切です。

noexceptを付ける前に確認したいこと

関数自身が例外を送出しないか

関数内に明示的なthrowがある場合、通常はnoexceptを付けられません。

throw std::runtime_error("error");

呼び出している関数が例外を送出しないか

次の関数では、内部で呼び出しているg()の例外仕様を確認する必要があります。

void f() noexcept
{
    g();
}

g()が例外を送出する可能性を持つ場合は、次のいずれかを選択します。

  • f()からnoexceptを外す
  • f()内部で例外を捕捉する
  • 例外発生時にプログラムを終了させる設計を受け入れる

メモリ確保が発生しないか

次の処理は、メモリ確保に伴う例外が発生する可能性があります。

std::string text = source;
vector.push_back(value);
map.insert(value);
std::make_unique<T>();
std::make_shared<T>();

メモリ不足を例外として呼び出し側へ伝えたい場合は、noexceptを指定してはいけません。

エラーから回復する必要があるか

エラーが発生した場合に、呼び出し側で回復処理を行う必要があるなら、例外を伝播できる設計が適しています。

一方、エラー発生時に処理継続ができない低レベル関数や終了処理では、noexceptが適切な場合があります。

将来も保証を維持できるか

現在は単純な処理でも、将来の実装変更によって例外が発生する処理が追加される可能性があります。

int id() const noexcept
{
    return id_;
}

単純なgetterであれば、将来もnoexceptを維持しやすいでしょう。

一方、次のような抽象度の高い関数では注意が必要です。

void update() noexcept;

将来、文字列処理、ログ出力、データベースアクセスなどを追加すると、例外が発生する可能性があります。

公開APIにnoexceptを付ける場合は、将来的にも保証を維持できるかを検討する必要があります。

noexceptに関するよくある誤解

noexceptなら例外は発生しない

noexceptは、関数内部で例外が発生しないことを保証する機能ではありません。

void f() noexcept
{
    std::string text(1'000'000'000, 'a');
}

メモリ確保に失敗すれば、例外が発生する可能性があります。

その例外が関数の外へ出ようとすると、std::terminate()が呼び出されます。

コンパイラが例外を禁止してくれる

次のコードは、必ずしもコンパイルエラーになるとは限りません。

void f() noexcept
{
    throw 1;
}

コンパイラが警告を出す場合はありますが、noexceptは関数内部の例外発生を静的に禁止する仕組みではありません。

例外が外へ出ようとした段階で、std::terminate()が呼ばれます。

すべての関数に付けたほうが高速になる

noexceptを付ければ、必ず速度が向上するわけではありません。

例外が発生する可能性のある関数に無理に付けると、回復可能なエラーでもプログラムが終了します。

void load_file() noexcept;

ファイル読み込みでは、ファイル不存在、権限不足、解析エラー、メモリ不足などが発生する可能性があります。

これらを例外で通知する設計なら、noexceptは不適切です。

後から自由に外せる

公開APIに指定したnoexceptは、関数の契約の一部です。

void process() noexcept;

利用側のコードは、この関数が例外を送出しないことを前提にする可能性があります。

後から次のように変更すると、APIの意味が変わります。

void process();

特に、仮想関数、関数ポインタ、テンプレート、共有ライブラリでは慎重な対応が必要です。

noexceptの実務的な使い方

単純な関数には明示的に付ける

例外を送出しないことが明らかで、将来も保証しやすい関数にはnoexceptを指定します。

int size() const noexcept
{
    return size_;
}

デフォルト化されたムーブは型特性で確認する

デフォルト化したムーブ操作では、コンパイラによる例外仕様の推論を利用できます。

class MyClass
{
public:
    MyClass(MyClass&&) = default;
    MyClass& operator=(MyClass&&) = default;
};

必要に応じて型特性で確認します。

static_assert(
    std::is_nothrow_move_constructible_v<MyClass>
);

テンプレートでは条件付きnoexceptを使う

ラップする処理の例外仕様を、外側の関数へ反映させます。

#include <utility>

template <typename F, typename... Args>
decltype(auto) invoke_wrapper(F&& function, Args&&... args)
    noexcept(noexcept(
        std::forward<F>(function)(
            std::forward<Args>(args)...
        )
    ))
{
    return std::forward<F>(function)(
        std::forward<Args>(args)...
    );
}

呼び出し対象がnoexceptなら、ラッパーもnoexceptになります。

まとめ

noexceptは、関数が例外を呼び出し元へ送出しないことを示す例外仕様です。

void f() noexcept;

noexcept関数から例外が外へ出ようとすると、std::terminate()が呼び出されます。

そのため、noexceptは単なる最適化指定ではなく、関数が提供する例外保証を表す重要な契約です。

実務では、次の点を意識する必要があります。

  • 実際に例外を外へ送出しない関数だけに指定する
  • ムーブコンストラクタやムーブ代入演算子で積極的に検討する
  • テンプレートでは条件付きnoexceptを使用する
  • メモリ確保や外部関数の呼び出しがある場合は慎重に判断する
  • デストラクタから例外を外へ出さない
  • 関数自体の例外仕様と、呼び出し式全体の例外安全性を区別する
  • 公開APIでは将来も保証を維持できるか確認する

特に重要なのは、安易にnoexceptを付けないことです。

void risky_operation() noexcept;

内部でメモリ確保、文字列操作、コンテナ操作、外部ライブラリの呼び出しなどを行っている場合、例外が発生するとプログラムが終了します。

noexceptは、処理を高速化するためだけに指定するものではありません。

その関数が例外を外へ送出しないという保証を、本当に提供できる場合に使用することが重要です。

以上、C++のnoexceptについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次