このページは、練習問題の解答例や解説のページです。
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 で割った結果を返し、奇数ならエラーとする関数を作成してください。エラーの発生を戻り値で知らせる方法と、例外で知らせる方法のそれぞれで作ってみてください。
たとえば次のようになります。
#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 {};
(n, &result); // エラーチェックしていない
divide_by_two_if_evenstd::cout << n << " / 2 = " << result << "\n"; // 5 / 2 = 0
}
例外による方法では、返したい値をすなおに戻り値で返せるうえ、エラーを無視することもできません。もし catchブロックで捕捉しなかったとしたら、プログラムの実行が終了します(本編解説)。
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:
// コンストラクタ
(RingBuffer& body, size_type pos, bool is_past_the_end);
Iterator
// ==演算子
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);
}
// ++演算子(前置)
& operator++();
Iterator
// ++演算子(後置)
operator++(int);
Iterator
private:
template <typename T>
inline static auto* common_get_elem_ptr(T* self) noexcept
{
return &self->m_body.m_data[self->m_pos];
}
private:
& m_body; // 本体のリングバッファへの参照
RingBuffersize_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:
// コンストラクタ
(const RingBuffer& body, size_type pos, bool is_past_the_end);
ConstIterator
// ==演算子
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);
}
// ++演算子(前置)
& operator++();
ConstIterator
// ++演算子(後置)
operator++(int);
ConstIterator
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:
// コンストラクタ
() = default;
RingBuffer
// コピーコンストラクタ
(const RingBuffer& other) = default;
RingBuffer
// テンプレート変換コンストラクタ
template <typename U, std::size_t Size2>
(const RingBuffer<U, Size2>& other);
RingBuffer
// コピー代入演算子
& operator=(const RingBuffer& rhs) = default;
RingBuffer
// ==演算子
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>
<T, Size>::RingBuffer(const RingBuffer<U, Size2>& other) :
RingBufferm_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>
<T, Size>::Iterator::Iterator(RingBuffer& body, size_type pos, bool is_past_the_end) :
RingBufferm_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)
{
{*this};
Iterator tmp ++(*this);
return tmp;
}
// ------------ ConstIterator ------------
// コンストラクタ
template <typename T, std::size_t Size>
<T, Size>::ConstIterator::ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end) :
RingBufferm_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)
{
{*this};
Iterator tmp ++(*this);
return tmp;
}
}
#endif
ここでは std::logic_error を選びましたが、使用する例外クラスは厳密にどれが正しいということはありません。
次のプログラムで動作を確認できます。
#include <iostream>
#include "ring_buffer.h"
int main()
{
::RingBuffer<int, 8> rb{};
mylib
try {
std::cout << rb.front() << "\n";
}
catch (const std::logic_error& ex) {
std::cerr << ex.what() << "\n";
}
}
実行結果:
RingBuffer is empty.
std::queue の popメンバ関数など、コンテナから要素を取り除く関数が、呼び出し元に取り除いた要素を返さない(戻り値が void になっている)理由を、例外処理の観点から考えて説明してください。
コンテナから取り除かれることを望んでいる関数なので、呼び出し元に返すのだとすれば、先にコピーを作って、コピーのほうを返すしかありません。順序はこうなります。
しかし、3のところで例外が発生する恐れがあります。もしそうなってしまった場合、呼び出し元は結果を受け取ることができませんが、すでに2のところで要素は取り除かれています。そして、すでに関数の呼び出しは終えているため、取り除かれてしまった要素を取り戻すことは不可能です。例外安全(本編解説)の考え方では、これは「保証なし」に当たります。
もし、取り除かれる要素を呼び出し元が受け取る用途を想定しているのなら、std::queue がそうしているように、次に取り除かれる予定の要素を返す frontメンバ関数と、要素を取り除く popメンバ関数を分けておき、2段階の操作をさせるようにすれば、例外安全のレベルを高められます。
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
![]() |
管理者情報 | プライバシーポリシー |