例外 | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、C++ の例外と呼ばれる機能の基本的な仕組みや使い方を解説します。例外を送出する方法、捕捉、例外クラスの種類、関数の例外指定(noexcept)など、例外に関する主要なトピックを扱います。また、例外安全、copy-and-swapイディオム、RAII といった設計手法についても取り上げます。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



例外 🔗

現在のリングバッファの実装(「オブジェクトのコピー」のページにあります)に細かい修正を加えていきます。まずは「例外」と呼ばれる機能について取り上げます。

例外📘 (exception) とはエラー処理の仕組みの一種で、通常の処理を実行している中で、問題のある状況(つまりエラー)が発生してしまったときに、例外ハンドラ (exception handler) と呼ばれるエラー時専用のコードに実行を移す機能です。例外によるエラー処理のことを例外処理📘 (exception handling) と呼びます。

例外は、これまでのページでも少しだけ登場しています。「文字列処理」のページでは、std::stoi関数などの関数が、文字列から算術型への変換に失敗したときに例外を使うことに触れました。

#include <iostream>
#include <string>

void stoi_test(const std::string& s)
{
    try {  // 例外を使っているコードを取り囲む
        int result = std::stoi(s);  // エラーを例外で伝達する関数を呼び出している
        std::cout << "変換結果: " << result << "\n";
    }
    catch (const std::invalid_argument&) {  // エラー発生時にジャンプしてくる先
        std::cout << "変換できません。\n";
    }
    catch (const std::out_of_range&) {      // エラー発生時にジャンプしてくる先
        std::cout << "変換結果が表現できません。\n";
    }
}

int main()
{
    stoi_test("123");
    stoi_test("xyz");
    stoi_test("111111111111111111");
}

実行結果:

変換結果: 123
変換できません。
変換結果が表現できません。

std::stoi関数はエラーの発生を伝えるために例外を使っています。当該コードが try {} で囲まれていると、例外発生時に専用のエラー処理コードへジャンプします。エラー処理コードは catchキーワードを用いたブロックを使って記述します。エラーの原因となることが複数ある場合、catchブロックを複数作ることで、それぞれ異なるエラー処理を行わせることができます。詳しいことは、あとで改めて解説します

このような例外によるエラー処理には、戻り値を使ってエラーを知らせる方法と比べて以下のような利点があります。

一方で以下のような欠点があります。

さきほどのプログラムをみれば文法は何となくでも分かると思います。一見難しくはなさそうですが、エラー処理として結局何をすればいいのかが問題になります。std::stoi関数を呼び出した側としては、とにかく文字列を数値に変換したいのであって、できなかったときに何をすればいいのだろうかということです。エラーの種類や発生状況、プログラムの意味などをトータルに考えなければ正しい結論はでません。

また、例外を使ってエラー処理をおこなうことにしているコードに対して、例外処理を書かなかったとしても、コンパイラは何もいいません。さきほどのプログラムから例外に関するコードを消してもコンパイルできますが、実行するとプログラムは異常終了📘してしまいます。

#include <iostream>
#include <string>

void stoi_test(const std::string& s)
{
    int result = std::stoi(s);
    std::cout << "変換結果: " << result << "\n";
}

int main()
{
    stoi_test("123");
    stoi_test("xyz");
    stoi_test("111111111111111111");
}

実行結果:

変換結果: 123
(エラー)

こうした使い方の難しさもあり、例外処理を使わないことを勧めるガイドラインもあるほどです[1]。例外を使えば、基本的にエラーの発生を無視できなくなるので、致命的ではないエラーには戻り値による方法を用い、致命的なエラーに例外を用いるという使い分けをすることもあります。

例外を投げる (throw) 🔗

例外処理を使う場合、エラーが起きたときにまず行うことは、例外を投げる (throwing an exception) ことです。「スローする」とか「送出する」「発生させる」と表現することもあります。

例外を投げるには、throwキーワード を用いて、次のような throw式 (throw expression) を記述します。

throw;

「式」には、後述する例外ハンドラに渡す、例外オブジェクト (exception object) と呼ばれる一時オブジェクト(「オブジェクトのコピー」のページを参照)を生成する式を記述します[2]。これは「何が起こったのか」を伝えるための情報を作るためのものです。

throw式を評価した型は void です。[3]

【上級】例外オブジェクトが生成され、それが捕捉される過程の中で、あらたな例外を投げなければならない状況が発生した場合は、std::terminate関数が呼び出されてプログラムは強制終了します[4]

throw式は通常、tryブロック (try block) の内側に記述します。tryブロックは tryキーワードに、ブロックをあらわす {} を加えたものです。

try {
    throw;
}
catch (...) {

}

tryブロックとセットになるかたちで catch (...) {} が付いていますが、これはこのあと説明します

プログラムの実行時に tryブロックのところに来たら、普通に内側に侵入し、そこに書かれているコードが実行されます。throw式以外の式が特別な動作になることはありません。


tryブロックがネストすることは許されています。tryブロックの内側から呼び出した関数の中にも tryブロックが現れるような、関数をまたいだネストも含みます。

throw式を tryブロックの外側に記述できますが、投げられた例外は処理されることがありません。これは後述する、例外ハンドラを発見できなかった場合と同じ結果になります。

例外を捕捉する (catch) 🔗

投げられた例外は catchキーワードを使って記述される例外ハンドラによって捕捉します。つまり、throw式が生成した例外オブジェクトを受け取って、例外時の処理を実行します。

例外ハンドラは tryブロックとセットになっており、次のように記述します。

try {
    throw;
}
catch (例外宣言) {
    // 捕捉時の処理
}

「例外宣言」には次のようなコードを記述します。


型 名前
...

【上級】手前に属性を記述することもできます。

「型」は、捕捉可能な例外オブジェクトの型です。詳細は省きますが、完全に同じ型でなくても、適合可能な型であれば捕捉できます[5]。「名前」は、例外ハンドラ内で例外オブジェクトにアクセスするときに使う名前になります。つまり、これは関数の引数の仕組みと同じで、渡されてくる例外オブジェクトが実引数です。そのため「型」のところを参照型にしておくことで、オブジェクトがコピーされるコストを抑えられます。

「…」はどんな例外オブジェクトであっても捕捉できることを表します。この場合は「名前」を指定できないので、捕捉後にできることは限定されます。基本的にはこの方法は最後の砦のようなものであって、優先的に用いるものではありません。

例外ハンドラは複数記述できます。

try {
    throw 1;
}
catch (const std::string& s) {
}
catch (int n) {
}
catch (...) {
}

例外が投げられたときには、上から順番に適合できる例外ハンドラを探し、最初に発見した例外ハンドラで捕捉されます。この場合、throw式には 1 を与えているので int型の例外オブジェクトが投げられるので、catch (int n) の例外ハンドラで捕捉されます。

throw式が記述された tryブロックとセットになっている例外ハンドラのいずれでも捕捉できない場合、この tryブロック全体を取り囲むほかの tryブロックがあれば、そちらに移動して、さらなる検索が行われます。これはたとえ関数の外に抜け出すことになったとしても続きます。

main関数のレベルにまで戻ってもなお、捕捉可能な例外ハンドラが発見できない場合は、std::terminate関数が呼び出されます。std::terminate関数が呼び出されると、デフォルトの挙動としては、std::abort関数(「アサート」のページを参照)によるプログラムの異常終了となります。

正確には、まず std::terminate_handler というハンドラ関数が呼び出されます。このハンドラ関数は、std::set_terminate関数で任意に設定できますが、設定を行わなければデフォルトのハンドラ関数が使われることとなり、その実装が std::abort関数を呼びます。[6]

throw式から例外ハンドラに移動するまでのあいだ、tryブロック内で生成された自動ストレージ期間を持つオブジェクトは、その生成の逆の順番で破棄され、デストラクタも呼び出されます。この動きは、ネストしている外側の tryブロックに巻き戻っていくような動きになることから、スタックのアンワインディング(巻き戻し) (stack unwinding) と呼ばれています[7]

オブジェクトを初期化している最中(つまりコンストラクタの途中)や、破棄している最中(つまりデストラクタの途中)に発生した例外によって、これらの作業が中断されてしまった場合、その時点までに完全に初期化できているサブオブジェクトに対してのみデストラクタが呼び出されます。言い換えると、コンストラクタが最後まで完了していないときにはデストラクタは呼び出されません。なぜなら、すべてのデータメンバの初期化を完了できていない可能性があり、オブジェクトの状態が不完全だからです。一部のデータメンバが不完全なまま、デストラクタで終了処理が走ったら、むしろおかしな結果になりかねません。

このため、コンストラクタやデストラクタの中で例外を捕捉する場合、例外ハンドラ内で静的でないデータメンバをアクセスすると、未定義の動作となります[8]

また、デストラクタを例外によって中断させてしまうようなコードを書いてはいけません。デストラクタを途中で中断してしまうと、そのオブジェクトを正しく破棄する手立てがなくなります。

デストラクタは暗黙的に、例外を投げない指定が与えられます。

なお、tryブロックは関数本体のコードに記述するため、どの関数とも関わりがない名前空間スコープの静的ストレージ期間を持つオブジェクトから投げられる例外を捕捉する手段はありません。

例外の再送出 🔗

例外ハンドラ内で、throw; のようにオペランドがない throw式を記述すると、現在捕捉している例外をそのままもう1度送出することを意味します。これは、例外の再送出 (rethrow) と呼ばれます。

try {
    throw 1;
}
catch (int n) {
    throw;
}

再送出される例外オブジェクトは、捕捉時に受け取ったオブジェクトそのものであって、新たなコピーが作られることはありません。

関数tryブロック 🔗

関数tryブロック (function try block) は、関数の本体自体を tryブロックにする機能です。

戻り値の型 関数名(仮引数の並び)
try {
    // 関数本体のコード
}
catch () {
}

関数の本体全体が tryブロックであるとみなされるため、どこで例外が投げられても、捕捉しようとします。仮引数が囲まれていないように見えますが、例外ハンドラ内から仮引数にアクセスしても構いません(寿命が関数tryブロックの終わりに拡張される[9])。

コンストラクタの場合で、コンストラクタ初期化子を使うときには、tryキーワードの直後のところに差し込むような構文になります。

コンストラクタ名(仮引数の並び)
try : メンバ初期化子 {
    // 関数本体のコード
}
catch () {
}

メンバ初期化子から例外が投げられたときにも、その例外を捕捉しようとします。これは、通常の tryブロックにはできないことです。


関数tryブロックの例外ハンドラの末尾まで処理が実行されると、コンストラクタやデストラクタの場合には、捕捉した例外オブジェクトが再び送出されます。これら以外の関数の場合には、return; によって関数を抜けることと同じ結果になり、戻り値が存在すべき関数だったときには未定義の動作となります[10]。また、コンストラクタやデストラクタの場合、例外ハンドラ内から return文で抜け出そうとすることは許されません[11]

例外クラス 🔗

例外オブジェクトの型は自由に選べますが、標準ライブラリには、例外オブジェクトに使える例外クラス (exception class) と呼ばれるクラスがいくつか定義されています。C++ の仕様として例外を送出すると決められている場面では、これらの標準の例外クラスのいずれかのオブジェクトが送出されます。

例外クラスは親子関係のように定義されていて、親に当たるクラスを用いると、その子であるクラス型の例外オブジェクトのすべてを捕捉できます。

【上級】クラスの継承によって実現されています。

例外クラスの親子関係は次のようになっています[12]

std::exception はすべての基底として存在しているため、上記のいずれが送出されたとしても、catch (const std::exception& ex) のような記述ですべて捕捉できます。ただし、単なる int や std::string のようなものも例外オブジェクトとして使えるので、std::exception であらゆる例外を捕捉できるわけではありません(あらゆる例外を捕捉するには、catch (...) を使う必要があります)。

std::exceptionクラスには whatメンバ関数があり、例外が発生した理由を説明する文字列を返します。文字列の具体的な内容は実装依存であることに注意してください。

try {
    ...
}
catch (const std::exception& ex)
{
    std::cerr << ex.what() << "\n";
}

std::exception をはじめとして、多くの例外クラスは <exception> に定義されています。また、std::logic_error、std::runtime_error とそれらの子のクラスの多くは <stdexcept> にあります。

特定の機能から発生する例外に特化している例外クラスは、それぞれに合った標準ヘッダのほうで定義されます。

自分で例外を送出するときにも、これらの例外クラスの中から、意味に合ったものを選んで使うことができます。また、未解説の機能ですが、これらの例外クラスを継承して独自のクラスを定義して使うことができます。またこれらのクラスのコンストラクタには、例外の理由に関する文字列を渡すことができ、前述の whatメンバ関数で返される文字列として使用されます(できます)。

例外指定 (noexcept) 🔗

例外指定(例外仕様) (exception specifications) という機能を使うと、関数が呼び出し元のほうへ例外を投げるかどうかを指定できます。

例外指定には種類がありますが、記述する位置としては、対象の関数の宣言や定義の後ろ側です(constメンバ関数の const を書くところ)。もっとも単純なものは noexceptキーワードを置くだけです。

戻り値の型 関数名(仮引数の並び) noexcept;

noexcept を付加された関数は、例外を関数の外側に投げないつもりであることを意味します。関数の中に閉じ込められたかたちで例外を使うことは許されます。「投げないつもり」が問題で、実際には例外を投げるように実装することは許されたままです。しかし、実際に例外を投げてしまったときには、前述した std::terminate関数が呼び出されます。

noexcept を以下のように記述することもできます。

戻り値の型 関数名(仮引数の並び) noexcept(定数式);

「定数式」には、評価した結果が true か false になる定数式を記述できます。結果として noexcept(true) である場合は単に noexcept とした場合と同じ結果になります。noexcept(false) である場合は、例外を投げることを許します。constexpr やテンプレート仮引数を駆使して、コンパイル時に結果を切り替えることが可能です(「constexpr」のページを参照)。

noexcept には演算子としての機能もありnoexcept(式) のように使用すると、式が例外を投げる可能性があるかどうかを判定できます。これを例外指定の定数式を記述するときに利用できます。

【C++17】例外仕様が、関数の型の一部とみなされるようになりました。たとえば、noexcept である関数を指し示す関数ポインタは、関数ポインタ側の型にも noexcept が必要になります。[13]

デストラクタは暗黙的に noexcept です(あえて明示的に書くこともできます)。明示的に noexcept(false) を与えることで、例外を投げることを許可できますが、前述したとおりデストラクタを例外で中断させるのはよくないので、noexcept のままにしておくべきです

【上級】このほか、delete演算子も暗黙的に noexcept です。

関数がオーバーロードされている場合、それぞれの例外仕様が異なっても構いません。


RingBuffer にも noexcept を適用してみます。注意しなければならないのは、例外指定はその関数が約束する仕様であるということです。つまり、いったん例外を投げないのだと約束したら、あとからやっぱり投げることもある、というように変えることは難しいということです。関数の作者と使用者が同一人物なら問題ないかもしれませんが、そうではないつもりでいるべきです。

そのため、不用意に noexcept を付けない考え方もありますが、ゲッターのようなシンプルな関数に付けることは安全であるはずです。また、constメンバ関数も、データメンバが変更されないので、例外を発生させるような処理でない可能性が高く、そうであれば noexcept にしても問題ないといえます。

そうしたことを踏まえて、以下のように修正を加えました。

ring_buffer.h の完全なコード
// ring_buffer.h
#ifndef RING_BUFFER_H_INCLUDED
#define RING_BUFFER_H_INCLUDED

#include <algorithm>
#include <array>
#include <cassert>

namespace mylib {

    // リングバッファ
    template <typename T, std::size_t Size>
    class RingBuffer {

        template <typename T, std::size_t Size>
        friend class RingBuffer;

    public:
        // イテレータ
        class Iterator {
        public:
            using value_type = T;                       // 要素型
            using reference = value_type&;              // 要素の参照型
            using const_reference = const value_type&;  // 要素の参照型
            using pointer = value_type*;                // 要素のポインタ型
            using const_pointer = const value_type*;    // 要素の constポインタ型
            using size_type = std::size_t;              // サイズ型
            using difference_type = std::ptrdiff_t;     // 距離型

        public:
            // コンストラクタ
            Iterator(RingBuffer& body, size_type pos, bool is_past_the_end);


            // ==演算子
            inline bool operator==(const Iterator& rhs) const noexcept
            {
                return m_body == rhs.m_body
                    && m_pos == rhs.m_pos
                    && m_is_past_the_end == rhs.m_is_past_the_end;
            }

            // !=演算子
            inline bool operator!=(const Iterator& rhs) const noexcept
            {
                return !(*this == rhs);
            }

            // *演算子(間接参照)
            inline reference operator*() noexcept
            {
                return *common_get_elem_ptr(this);
            }

            // *演算子(間接参照)
            inline const_reference operator*() const noexcept
            {
                return *common_get_elem_ptr(this);
            }

            // ->演算子
            inline pointer operator->() noexcept
            {
                return common_get_elem_ptr(this);
            }

            // ->演算子
            inline const_pointer operator->() const noexcept
            {
                return common_get_elem_ptr(this);
            }

            // ++演算子(前置)
            Iterator& operator++();

            // ++演算子(後置)
            Iterator operator++(int);


        private:
            template <typename T>
            inline static auto* common_get_elem_ptr(T* self) noexcept
            {
                return &self->m_body.m_data[self->m_pos];
            }

        private:
            RingBuffer&             m_body;             // 本体のリングバッファへの参照
            size_type               m_pos;              // 指し示している要素の位置
            bool                    m_is_past_the_end;  // 終端の次を指すイテレータか
        };

        // constイテレータ
        class ConstIterator {
        public:
            using value_type = T;                       // 要素型
            using const_reference = const value_type&;  // 要素の参照型
            using const_pointer = const value_type*;    // 要素の constポインタ型
            using size_type = std::size_t;              // サイズ型
            using difference_type = std::ptrdiff_t;     // 距離型

        public:
            // コンストラクタ
            ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end);


            // ==演算子
            inline bool operator==(const ConstIterator& rhs) const noexcept
            {
                return m_body == rhs.m_body
                    && m_pos == rhs.m_pos
                    && m_is_past_the_end == rhs.m_is_past_the_end;
            }

            // !=演算子
            inline bool operator!=(const ConstIterator& rhs) const noexcept
            {
                return !(*this == rhs);
            }

            // *演算子(間接参照)
            inline const_reference operator*() const noexcept
            {
                return *common_get_elem_ptr(this);
            }

            // ->演算子
            inline const_pointer operator->() const noexcept
            {
                return common_get_elem_ptr(this);
            }

            // ++演算子(前置)
            ConstIterator& operator++();

            // ++演算子(後置)
            ConstIterator operator++(int);


        private:
            template <typename T>
            inline static auto* common_get_elem_ptr(T* self) noexcept
            {
                return &self->m_body.m_data[self->m_pos];
            }

        private:
            const RingBuffer&       m_body;             // 本体のリングバッファへの参照
            size_type               m_pos;              // 指し示している要素の位置
            bool                    m_is_past_the_end;  // 終端の次を指すイテレータか
        };

    public:
        using container_type = typename std::array<T, Size>;                // 内部コンテナの型
        using value_type = typename container_type::value_type;             // 要素型
        using reference = typename container_type::reference;               // 要素の参照型
        using const_reference = typename container_type::const_reference;   // 要素の const参照型
        using pointer = typename container_type::pointer;                   // 要素のポインタ型
        using const_pointer = typename container_type::const_pointer;       // 要素の constポインタ型
        using size_type = typename container_type::size_type;               // サイズ型
        using iterator = Iterator;                                          // イテレータ型
        using const_iterator = ConstIterator;                               // constイテレータ型

    public:
        // コンストラクタ
        RingBuffer() = default;

        // コピーコンストラクタ
        RingBuffer(const RingBuffer& other) = default;

        // テンプレート変換コンストラクタ
        template <typename U, std::size_t Size2>
        RingBuffer(const RingBuffer<U, Size2>& other);


        // コピー代入演算子
        RingBuffer& operator=(const RingBuffer& rhs) = default;
        
        // ==演算子
        bool operator==(const RingBuffer& rhs) const noexcept;

        // !=演算子
        inline bool operator!=(const RingBuffer& rhs) const noexcept
        {
            return !(*this == rhs);
        }


        // 要素を追加
        void push_back(const value_type& value);

        // 要素を取り除く
        void pop_front();

        // 空にする
        void clear();


        // 先頭の要素の参照を返す
        inline reference front()
        {
            return common_front(this);
        }

        // 先頭の要素の参照を返す
        inline const_reference front() const
        {
            return common_front(this);
        }

        // 末尾の要素の参照を返す
        inline reference back()
        {
            return common_back(this);
        }

        // 末尾の要素の参照を返す
        inline const_reference back() const
        {
            return common_back(this);
        }



        // 先頭の要素を指す イテレータを返す
        inline iterator begin() noexcept
        {
            return iterator(*this, m_front, empty());
        }

        // 末尾の要素の次を指す イテレータを返す
        inline iterator end() noexcept
        {
            return iterator(*this, m_back, true);
        }

        // 先頭の要素を指す constイテレータを返す
        inline const_iterator begin() const noexcept
        {
            return const_iterator(*this, m_front, empty());
        }

        // 末尾の要素の次を指す constイテレータを返す
        inline const_iterator end() const noexcept
        {
            return const_iterator(*this, m_back, true);
        }


        // 容量を返す
        inline size_type capacity() const noexcept
        {
            return m_data.size();
        }

        // 要素数を返す
        inline size_type size() const noexcept
        {
            return m_size;
        }

        // 空かどうかを返す
        inline bool empty() const noexcept
        {
            return m_size == 0;
        }

        // 満杯かどうかを返す
        inline bool full() const noexcept
        {
            return m_size == capacity();
        }

    private:
        // 次の位置を返す
        inline size_type get_next_pos(size_type pos) const noexcept
        {
            return (pos + 1) % capacity();
        }

        // 手前の位置を返す
        inline size_type get_prev_pos(size_type pos) const noexcept
        {
            if (pos >= 1) {
                return pos - 1;
            }
            else {
                return m_size - 1;
            }
        }

        template <typename T>
        inline static auto& common_front(T* self)
        {
            assert(self->empty() == false);
            return self->m_data[self->m_front];
        }

        template <typename T>
        inline static auto& common_back(T* self)
        {
            assert(self->empty() == false);
            return self->m_data[self->get_prev_pos(self->m_back)];
        }

    private:
        container_type              m_data{};       // 要素
        size_type                   m_size{0};      // 有効な要素の個数
        size_type                   m_back{0};      // 次に push される位置
        size_type                   m_front{0};     // 次に pop される位置
    };


    // コンストラクタ(異なる要素型の RingBuffer から作成)
    template <typename T, std::size_t Size>
    template <typename U, std::size_t Size2>
    RingBuffer<T, Size>::RingBuffer(const RingBuffer<U, Size2>& other) :
        m_data(other.m_data.capacity()),
        m_size {other.m_size},
        m_back {other.m_back},
        m_front {other.m_front}
    {
        std::transform(
            std::cbegin(other.m_data),
            std::cend(other.m_data),
            std::begin(m_data),
            [](const auto& e) {
                return static_cast<T>(e);
            }
        );
    }

    // ==演算子
    template <typename T, std::size_t Size>
    bool RingBuffer<T, Size>::operator==(const RingBuffer& rhs) const noexcept
    {
        return m_data == rhs.m_data
            && m_size == rhs.m_size
            && m_back == rhs.m_back
            && m_front == rhs.m_front;
    }

    // 要素を追加
    template <typename T, std::size_t Size>
    void RingBuffer<T, Size>::push_back(const value_type& value)
    {
        if (full()) {
            m_front = get_next_pos(m_front);
        }
        else {
            m_size++;
        }

        m_data[m_back] = value;
        m_back = get_next_pos(m_back);
    }

    // 要素を取り除く
    template <typename T, std::size_t Size>
    void RingBuffer<T, Size>::pop_front()
    {
        assert(empty() == false);

        m_front = get_next_pos(m_front);
        --m_size;
    }

    // 空にする
    template <typename T, std::size_t Size>
    void RingBuffer<T, Size>::clear()
    {
        m_size = 0;
        m_back = 0;
        m_front = 0;
    }




    // ------------ Iterator ------------
    // コンストラクタ
    template <typename T, std::size_t Size>
    RingBuffer<T, Size>::Iterator::Iterator(RingBuffer& body, size_type pos, bool is_past_the_end) :
        m_body {body},
        m_pos {pos},
        m_is_past_the_end {is_past_the_end}
    {

    }

    // ++演算子(前置)
    template <typename T, std::size_t Size>
    typename RingBuffer<T, Size>::Iterator& RingBuffer<T, Size>::Iterator::operator++()
    {
        assert(!m_is_past_the_end);

        m_pos = m_body.get_next_pos(m_pos);

        // 終端要素の位置を越えた?
        if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
            m_is_past_the_end = true;
        }

        return *this;
    }

    // ++演算子(後置)
    template <typename T, std::size_t Size>
    typename RingBuffer<T, Size>::Iterator RingBuffer<T, Size>::Iterator::operator++(int)
    {
        Iterator tmp {*this};
        ++(*this);
        return tmp;
    }


    // ------------ ConstIterator ------------
    // コンストラクタ
    template <typename T, std::size_t Size>
    RingBuffer<T, Size>::ConstIterator::ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end) :
        m_body {body},
        m_pos {pos},
        m_is_past_the_end {is_past_the_end}
    {

    }

    // ++演算子(前置)
    template <typename T, std::size_t Size>
    typename RingBuffer<T, Size>::ConstIterator& RingBuffer<T, Size>::ConstIterator::operator++()
    {
        assert(!m_is_past_the_end);

        m_pos = m_body.get_next_pos(m_pos);

        // 終端要素の位置を越えた?
        if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
            m_is_past_the_end = true;
        }

        return *this;
    }

    // ++演算子(後置)
    template <typename T, std::size_t Size>
    typename RingBuffer<T, Size>::ConstIterator RingBuffer<T, Size>::ConstIterator::operator++(int)
    {
        Iterator tmp {*this};
        ++(*this);
        return tmp;
    }
}

#endif

noexcept演算子 🔗

noexceptキーワードには、noexcept演算子 (noexcept operator) としての使い方もあります。

noexcept()

「式」が例外を送出する可能性がある場合に false、可能性がない場合に true になります。「式」自体は評価されません。

#include <iostream>

void f1()
{
}

void f2() noexcept
{
}

void f3()
{
    throw "xxxx";
}

int main()
{
    std::cout << std::boolalpha
              << noexcept(f1()) << "\n"
              << noexcept(f2()) << "\n"
              << noexcept(f3()) << "\n";
}

実行結果:

false
true
false

本当に例外が送出されるかどうかを問題にしていないことに注意してください。このサンプルプログラムでいえば、f1関数は実際には例外を送出することはありませんが、noexcept演算子の判定は false(例外を送出する可能性がある)となっています。これは、f1関数に例外仕様の noexcept を付加していないためです。

動的例外指定 🔗

throwキーワードを用いた方法で記述する、動的例外指定 (dynamic exception specifications) というものがあります。これは古い C++ で使われていた方法であり、現在では使用するべきではありません(あるいはもう使用できません)。

動的例外指定では、例外オブジェクトの型を指示し、その型の例外が投げられることを許可するかたちになっています。

戻り値の型 関数名(仮引数の並び) throw();
戻り値の型 関数名(仮引数の並び) throw(型名の並び);

【C++17】() が空の throw() を除いて、動的例外指定は削除されました。throw() は非推奨です。[14]

【C++20】() が空の throw() についても削除されました[15]

【C++98/03 経験者】 動的例外指定という名称は C++11 で与えられたものですが、意味は C++03 までのものと同じで、投げることを許可する例外オブジェクトの型を指示できるものでした。noexcept を使った方法で同じことはできませんが、これは実質的に無意味であるか、有効に使うことが難しいためです。唯一、throw() とした場合の動作(例外を投げない)だけに価値があるとされ、その動作が noexcept と形を変えて残ることになりました。
C++17 で throw() 以外の使い方が削除された結果、この機能のためだけに存在していた std::unexpected関数、std::set_unexpected関数、std::get_unexpected関数、std::unexpected_handler型もそれぞれ削除されています。[14]

例外安全 🔗

noexcept を用いて例外を投げないことを保証すると、例外について考慮する必要がなくなります。これはとても重要なことです。なぜなら、例外が投げられる可能性が少しでもある場合、例外の送出によって処理が途中で打ち切られてしまい、特にリソースの管理に対する影響があるからです。ここでいうリソースとは、たとえば「オブジェクトのコピー」のページで解説した動的オブジェクトです。

次のように代入演算子を実装することを考えてみます。

class MyClass {
public:
    MyClass& MyClass::operator=(const MyClass& rhs)
    {
        if (this != &rhs) {
            m_data = rhs.m_data;
        }
        return *this;
    }

private:
    std::vector<int>  m_data;
}

std::vector<int> のコピーの途中で例外が送出されるかもしれません。この代入演算子を使用する箇所を catch のブロックで囲んでやれば、例外を捕捉することができるので、たしかにエラーの発生を検知して、何かしらかのエラー処理を記述できる余地はあります。しかし、m_data を書き換えている最中に例外が送出されたとすれば、すでにいくつかの要素は上書きされてしまっています。以前の状態を復帰させることはもうできませんし、うまく続きの要素のコピーを再開することもできません。

例外処理を用いると決めたのならば、このように、例外の発生によってオブジェクトが不適切な状態に陥ってしまう可能性を考えておかなければなりません。そこで、例外安全 (exception safety) という考え方があります。例外安全なコードになっていると、例外が発生しても、プログラムは予測可能な動作を継続できることが保証されます。例外安全でないコードでは、例外発生してしまうと、プログラムの動作は予測不能になります。

例外安全には、その程度に応じて以下のような段階があるとされています。

  1. 例外を投げない保証
  2. 強い保証
  3. 基本保証
  4. 保証なし

「例外を投げない保証」が、例外安全の中でもっとも強力です。noexcept を付加した関数がこれに当たります。そもそも例外が発生しないので、「例外の発生によってオブジェクトが不適切な状態に陥る」ことはありえません。

「強い保証」が次に強力です。これは、例外が発生した場合には、オブジェクトが例外発生前の状態に戻ることをいいます。つまり、処理のすべてが正常に完了するか、まったくなにも行われていない状態になるかの2択しかありえないように設計されているものが、強い保証があるといえます。

「基本保証」は最低限の保証で、例外が発生しても、少なくともオブジェクトが不適切な状態にはならないことをいいます。さきほどの std::vector<int> の例はこれに当たります。強い保証とちがって、オブジェクトの状態は変わってしまっているかもしれないので、実際のところ意図どおりに処理を継続することはできないと思われますが、少なくとも未定義動作には陥らないので、安全な流れでプログラムを終了させる対応が取れます。

「保証なし」は文字通り、例外が発生したときにオブジェクトの状態がどうなるか分からないことをいいます。

copy-and-swapイディオム 🔗

さきほどの代入演算子の実装に、より高い例外安全を実現する方法として、copy-and-swapイディオム がよく知られています。

copy-and-swapイディオムは、オブジェクトのコピーを用意したあと、例外を投げない保証がある方法で新旧のオブジェクトを入れ替える(swap)という方法です。具体的には次のようになります。

class MyClass {
public:
    MyClass& MyClass::operator=(const MyClass& rhs)
    {
        if (this != &rhs) {
            MyClass temp {rhs};  // コピー
            temp.swap(*this);    // 例外を投げない交換
        }
        return *this;
    }

private:
    std::vector<int>  m_data;
}

まず、コピー元になるオブジェクトの一時的なコピー(temp)を作ります。コピーを取る流れの中で例外が発生する可能性がありますが、もしそうなったとしても、コピー元に影響はありませんし、temp は新規で作られる一時的な変数なので、データが失われることはありません。

コピーが成功したら、this が指すオブジェクトと temp の内容を交換します。この交換には、対象のオブジェクト専用に実装された swap関数や、std::swap関数を使用します。いずれにせよ、その交換関数が「例外を投げない保証」を持っていることが重要です。交換の最中に例外が発生しないなら、ここで失敗することはないということですから、this と temp の内容は確実に交換されます。temp の内容はコピー元の rhs とまったく同じですから、結果的に、rhs の内容が this にコピーできたことになります。

また、作業のために作った temp は、デストラクタの働きによって正しく解放されます。このようにリソースの管理をクラスに任せることで、デストラクタでの確実な解放を実現する手法には、RAII (Resource Acquisition Is Initialization)📘 という名前があります。RAII の手法を用いれば、どのようなかたちでスコープを終えようとも、リソースを正しく解放できるので、C++ において、RAII の考え方は非常に重要です。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

std::vector などのコンテナが持つ atメンバ関数は、範囲外アクセスを検知すると std::out_of_range例外を送出します。実際のプログラムを書いて動作を確認してみてください。

解答・解説

問題2 (基本★)

引数で渡された整数が、偶数なら 2 で割った結果を返し、奇数ならエラーとする関数を作成してください。エラーの発生を戻り値で知らせる方法と、例外で知らせる方法のそれぞれで作ってみてください。

解答・解説

問題3 (応用★★)

RingBufferクラステンプレートで、要素の参照を返すメンバ関数は、要素が空の場合に行える処理がありません。このような場合に例外を送出するように実装を修正してください。

解答・解説

問題4 (調査★★★)

std::queue の popメンバ関数など、コンテナから要素を取り除く関数が、呼び出し元に取り除いた要素を返さない(戻り値が void になっている)理由を、例外処理の観点から考えて説明してください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る