例外 解答ページ | Programming Place Plus 新C++編

トップページ新C++編例外

このページの概要 🔗

このページは、練習問題の解答例や解説のページです。



解答・解説 🔗

問題1 (確認★) 🔗

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


たとえば次のようなプログラムで確認できます。

#include <exception>
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> vec {0, 1, 2, 3, 4};

    try {
        for (int i = 0; i < 10; ++i) {
            std::cout << vec.at(i) << "\n";
        }
    }
    catch (const std::out_of_range& ex){
        std::cerr << ex.what() << "\n";
    }
}

実行結果:

0
1
2
3
4
invalid vector<T> subscript

例外によるエラー処理を行うためには、例外を発生させる可能性があるコードを tryブロックで囲み(本編解説)、例外発生時の処理を記述する catchブロック(例外ハンドラ)を記述します(本編解説)。

catchブロックの () には、catchブロックでエラー処理を行う例外の種類を記述します。今回は std::out_of_range なので、catch (const std::out_of_range& ex) のようになります。ex は何でも構いません。また、参照型であることは必須ではありませんが、std::out_of_range型のオブジェクトが渡されてくるときに、コピーが発生することは無駄なので、処理効率を損なわないために参照型にします。

なお、std::out_of_range は標準ライブラリに含まれているクラスで、<exception> で定義されています(本編解説)。

要素が5個入っている std::vector に対して、0~9 の値を atメンバ関数に渡しているので、0~4 までは正常に動作しますが、5 のときにエラーとなり、std::out_of_range例外が送出されます。

問題2 (基本★) 🔗

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


たとえば次のようになります。

#include <iostream>
#include <stdexcept>

// 戻り値でエラーを知らせる (false のときエラー)
bool divide_by_two_if_even(int n, int* result)
{
    if (n % 2 == 0) {
        *result = n / 2;
        return true;
    }
    else {
        return false;
    }
}

// 例外でエラーを知らせる
int divide_by_two_if_even_ex(int n)
{
    if (n % 2 == 0) {
        return n / 2;
    }
    else {
        throw std::invalid_argument("n is odd number.");
    }
}

int main()
{
    int n {5};

    int result {};
    if (divide_by_two_if_even(n, &result)) {
        std::cout << n << " / 2 = " << result << "\n";
    }
    else {
        std::cout << "error: n is odd number." << "\n";
    }

    try {
        int result {divide_by_two_if_even_ex(n)};
        std::cout << n << " / 2 = " << result << "\n";
    }
    catch (const std::invalid_argument& ex) {
        std::cout << "error: " << ex.what() << "\n";
    }
}

実行結果:

error: n is odd number.
error: n is odd number.

戻り値による方法では、戻り値として返せる値は1個だけなので、本来その関数が返したい値(ここでは 2 で割った結果)と、エラーの有無を別々にすることになり、作りも使い方も複雑化します。std::pair などでまとめることはできますが、それでも面倒はあります。また、エラーを無視してしまえることも問題です。

int main()
{
    int n {5};

    int result {};
    divide_by_two_if_even(n, &result);  // エラーチェックしていない
    std::cout << n << " / 2 = " << result << "\n";  // 5 / 2 = 0
}

例外による方法では、返したい値をすなおに戻り値で返せるうえ、エラーを無視することもできません。もし catchブロックで捕捉しなかったとしたら、プログラムの実行が終了します(本編解説)。

問題3 (応用★★) 🔗

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


現在の実装では、common_frontメンバ関数、common_backメンバ関数のところで、要素が空の場合には assert で停止させています。これを取りやめて、例外を送出するようにします。

// ring_buffer.h
#ifndef RING_BUFFER_H_INCLUDED
#define RING_BUFFER_H_INCLUDED

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

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)
        {
            if (self->empty()) {
                throw std::logic_error("RingBuffer is empty.");
            }
            return self->m_data[self->m_front];
        }

        template <typename T>
        inline static auto& common_back(T* self)
        {
            if (self->empty()) {
                throw std::logic_error("RingBuffer is empty.");
            }
            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

ここでは std::logic_error を選びましたが、使用する例外クラスは厳密にどれが正しいということはありません。

次のプログラムで動作を確認できます。

#include <iostream>
#include "ring_buffer.h"

int main()
{
    mylib::RingBuffer<int, 8> rb{};

    try {
        std::cout << rb.front() << "\n";
    }
    catch (const std::logic_error& ex) {
        std::cerr << ex.what() << "\n";
    }
}

実行結果:

RingBuffer is empty.

問題4 (調査★★★) 🔗

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


コンテナから取り除かれることを望んでいる関数なので、呼び出し元に返すのだとすれば、先にコピーを作って、コピーのほうを返すしかありません。順序はこうなります。

  1. 取り除く要素のコピーを作る
  2. データメンバから取り除く
  3. コピーを返す

しかし、3のところで例外が発生する恐れがあります。もしそうなってしまった場合、呼び出し元は結果を受け取ることができませんが、すでに2のところで要素は取り除かれています。そして、すでに関数の呼び出しは終えているため、取り除かれてしまった要素を取り戻すことは不可能です。例外安全(本編解説)の考え方では、これは「保証なし」に当たります。

もし、取り除かれる要素を呼び出し元が受け取る用途を想定しているのなら、std::queue がそうしているように、次に取り除かれる予定の要素を返す frontメンバ関数と、要素を取り除く popメンバ関数を分けておき、2段階の操作をさせるようにすれば、例外安全のレベルを高められます。


参考リンク 🔗



更新履歴 🔗




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