From 572e9ce3778af0ed2fad34091c62c499dc1a03b2 Mon Sep 17 00:00:00 2001 From: Ryou Ezoe Date: Mon, 10 Dec 2018 18:42:34 +0900 Subject: [PATCH] =?UTF-8?q?vector=E3=81=AE=E3=83=A1=E3=83=A2=E3=83=AA?= =?UTF-8?q?=E7=A2=BA=E4=BF=9D=E3=81=BE=E3=81=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 034-vector-memory-allocation.md | 784 ++ README.md | 4 + docs/index.html | 12611 ++++++++++++++++-------------- 3 files changed, 7439 insertions(+), 5960 deletions(-) create mode 100644 034-vector-memory-allocation.md diff --git a/034-vector-memory-allocation.md b/034-vector-memory-allocation.md new file mode 100644 index 0000000..885a2ef --- /dev/null +++ b/034-vector-memory-allocation.md @@ -0,0 +1,784 @@ +# vectorの実装 : メモリ確保 + +## メモリ確保の起こるタイミング + +`std::vector`はどこでメモリを確保しているのだろうか。 + +デフォルト構築すると空になる。 + +~~~cpp +int main() +{ + std::vector v ; + v.empty() ; // bool +} +~~~ + +コンストラクターに要素数を渡すことができる。 + +~~~cpp +int main() +{ + std::vector v(100) ; + v.size() ; // 100 +} +~~~ + +すると`std::vector`は指定した要素数の有効な要素をもつ。 + +コンストラクターに要素数と初期値を渡すことができる。 + +~~~cpp +int main() +{ + std::vector v(100, 123) ; + v[0] ; // 123 + v[12] ; // 123 + v[68] ; // 123 +} +~~~ + +すると、指定した要素数で、要素の値はすべて初期値になる。 + +vectorのオブジェクトを構築した後でも、メンバー関数`resize(size)`で要素数を`size`個にできる。 + +~~~cpp +int main() +{ + std::vector v ; + v.resize(10) ; + v.size() ; // 10 + // 減らす + v.resize(5) ; + v.size(5) ; +} +~~~ + +`resize`で要素数が増える場合、増えた要素の初期値も指定できる。 + +~~~cpp +int main() +{ + std::vector v ; + v.resize(3, 123) ; + // vは{123,123,123} +} +~~~ + +`resize`で要素数が減る場合、末尾が削られる。 + +~~~cpp +int main() +{ + std::vector v = {1,2,3,4,5} ; + v.resize(3) ; + // vは{1,2,3} +} +~~~ + +メンバー関数`push_back(value)`を呼び出すと要素数が1増え、要素の末尾の要素が値`value`になる。 + +~~~cpp +int main() +{ + std::vector v ; + // vは{} + v.push_back(1) ; + // vは{1} + v.push_back(2) ; + // vは[1,2} + v.push_back(3) ; + // vは{1,2,3} +} +~~~ + +`reserve(size)`は少なくとも`size`個の要素が追加の動的メモリ確保なしで追加できるようにメモリを予約する。 + +~~~cpp +int main() +{ + std::vector v ; + // 少なくとも3個の要素を追加できるように動的メモリ確保 + v.reserve(3) ; + v.size() ; // 0 + v.capacity() ; // 3以上 + + // 動的メモリ確保は発生しない + v.push_back(1) ; + v.push_back(2) ; + v.push_back(3) ; + // 動的メモリ確保が発生する可能性がある。 + v.push_back(3) ; +} +~~~ + +この章ではここまでの実装をする。 + +## デフォルトコンストラクター + +簡易vectorのデフォルトコンストラクターは何もしない。 + +~~~c++ +vector( ) { } +~~~ + +何もしなくてもポインターはすべてnullptrで初期化され、アロケーターもデフォルト構築されるからだ。 + +これで簡易vectorの変数を作れるようになった。ただしまだ何もできない。 + +~~~c++ +int main() +{ + vector v ; + // まだ何もできない。 +} +~~~ + +## アロケーターを取るコンストラクター + +`std::vector`のコンストラクターは最後の引数にアロケーターを取れる。 + +~~~cpp +int main() +{ + std::allocator alloc ; + // 空 + std::vector v1(alloc) ; + // 要素数5 + std::vector v2(5, alloc) ; + // 要素数5で初期値123 + std::vector v3(5, 123, alloc) ; +} +~~~ + +これを実装するには、アロケーターを取ってデータメンバーにコピーするコンストラクターを書く。 + +~~~c++ +vector( const allocator_type & alloc ) noexcept + alloc( alloc ) +{ } +~~~ + +他のコンストラクターはこのコンストラクターにまずデリゲートすればよい。 + +~~~c++ +vector() + : vector( allocator_type() ) +{ } + +vector( size_type size, const allocator_type & alloc = allocator_type() ) + : vector( alloc ) +{ /*実装*/ } +vector( size_type size, const_reference value, const allocator_type & alloc = allocator_type() ) + : vector( alloc ) +{ /*実装*/ } +~~~ + +## 要素数と初期値を取るコンストラクターの実装 + +要素数と初期値を取るコンストラクターは`resize`を使えば簡単に実装できる。 + +~~~c++ +vector( size_type size, const allocator_type & alloc ) + : vector( alloc ) +{ + resize( size ) ; +} +vector( size_type size, const_reference value, const allocator_type & alloc ) + : vector( alloc ) +{ + resize( size, value ) ; +} +~~~ + +しかしこれは実装を`resize`に丸投げしただけだ。`resize`の実装をする前に、実装を楽にするヘルパー関数を実装する。 + +## ヘルパー関数 + +ここではvectorの実装を楽にするためのヘルパー関数をいくつか実装する。このヘルパー関数はユーザーから使うことは想定しないので、privateメンバーにする。 + +~~~c++ +// 例 +struct vector +{ +private : + // ユーザーからは使えないヘルパー関数 + void helper_function() ; +public : + // ユーザーが使える関数 + void func() + { + // ヘルパー関数を使って実装 + helper_function() ; + } +} ; +~~~ + +### ネストされた型名traits + +アロケーターは`allocator_traits`を経由して使う。実際のコードはとても冗長になる。 + +~~~c++ +template < typename Allocator > +void f( Allocator & alloc ) +{ + std::allocator_traits::allocate( alloc, 1 ) ; +} +~~~ + +この問題はエイリアス名を使えば解決できる。 + +~~~c++ +private : + using traits = std::allocator_traits ; + + template < typename Allocator > + void f( Allocator & alloc ) + { + traits::allocate( alloc, 1 ) ; + } +~~~ + +### allocate/deallocate + +`allocate(n)`はアロケーターから`n`個の要素を格納できる生のメモリの動的確保をして先頭要素へのポインターを返す。 + +`deallocate(ptr)`はポインター`ptr`を解放する。 + +~~~c++ +private: + pointer allocate( size_type n ) + { return traits::allocate( alloc, n ) ; } + void deallocate( ) + { traits::deallocate( alloc, first, capacity() ) ; } +~~~ + +### construct/destroy + +`construct(ptr)`は生のメモリへのポインター`ptr`に`vector`の`value_type`型の値をデフォルト構築する。 + +`construct(ptr, value)`は生のメモリへのポインター`ptr`に値`value`のオブジェクトを構築する。 + +~~~c++ + + void construct( pointer ptr ) + { traits::construct( alloc, ptr ) ; } + void construct( pointer ptr, const_reference value ) + { traits::construct( alloc, ptr, value ) ; } +~~~ + +`destroy(ptr)`は`ptr`の指すオブジェクトを破棄する。 + +~~~c++ +private : + void destroy( pointer ptr ) + { traits::destroy( alloc, ptr ) ; } +~~~ + +### `destroy_all/destroy_until` + +`destroy_all()`は`vector`の要素を末尾から先頭に向けて順番に破棄する。 + +`std::vector`の初期化では、要素は先頭から末尾に向けて順番に構築される。C++では破棄は構築の逆順に行われるので、`std::vector`の破棄にあたっては、要素は末尾から先頭に向けて順番に破棄される。 + +~~~cpp +struct X +{ + X() { } + ~X)() { } +} ; + +int main() +{ + std::vector v(3) ; +} +~~~ + +このコードでは、`v[0], v[1], v[2]`の順番に要素が構築され、`v[2], v[1], v[0]`の順番で破棄される。 + +`destroy_all`の実装は、この次に説明する`destroy_until`を使う。 + +~~~c++ +private : + void destroy_all() + { + destroy_until( rend() ) ; + } +~~~ + +`destroy_until(rend)`は、`vector`が保持する`rbegin()`からリバースイテレーター`rend`までの要素を破棄する。リーバスイテレーターを使うので、要素の末尾から先頭に向けて順番に破棄される。 + + +~~~c++ +private : + void destroy_until( reverse_iterator rend ) + { + for ( auto riter = rbegin() ; riter != rend ; ++riter ) + { + destroy( &*riter ) ; + } + } +~~~ + +`&*riter`はやや泥臭い方法だ。簡易`vector`の`iterator`は単なる`T *`だが、`riter`はリバースイテレーターなのでポインターではない。ポインターを取るために`*riter`でまず`T &`を得て、そこに`&`を適用することで`T *`を得ている。 + +## デストラクター + +ヘルパー関数を組み合わせることでデストラクターが実装できるようになった。 + +`std::vector`のデストラクターは、 + +1. 要素を末尾から先頭に向かう順番で破棄 +2. 生のメモリを解放する + +この2つの処理はすでに実装した。デストラクターの実装は単にヘルパー関数を並べて呼び出すだけでよい。 + +~~~c++ +~vector() +{ + // 1. 要素を末尾から先頭に向かう順番で破棄 + destroy_all( ) ; + // 2. 生のメモリを解放する + deallocate() ; +} +~~~ + + +## reserveの実装 + +reserveの実装は生の動的メモリを確保してデータメンバーを適切に設定する。 + +ただし、いろいろと考慮すべきことが多い。 + + +現在の`capacity`より小さい要素数が`reserve`された場合、無視してよい。 + +~~~cpp +int main() +{ + // 要素数5 + std::vector v = {1,2,3,4,5} ; + // 3個の要素を保持できるよう予約 + v.reserve( 3 ) ; + // 無視する +} +~~~ + +すでに指定された要素数以上に予約されているからだ。 + +動的メモリ確保が行われていない場合、単に動的メモリ確保をすればよい。 + +~~~cpp +int main() +{ + std::vector v ; + // おそらく動的メモリ確保 + v.reserve( 10000 ) ; +} +~~~ + +「おそらく」というのは、C++の規格はvectorのデフォルトコンストラクターが予約するストレージについて何も言及していないからだ。すでに要素数10000を超えるストレージが予約されている実装も規格準拠だ。本書で実装している`vector`は、デフォルトコンストラクターでは動的メモリ確保をしない実装になっている。 + + +有効な要素が存在する場合、その要素の値は引き継がなければならない。 + +~~~cpp +int main() +{ + // 要素数3 + std::vector v = {1,2,3} ; + // 1万個の要素を保持できるだけのメモリを予約 + v.reserve( 10000 ) ; + // vは{1,2,3} +} +~~~ + +つまり動的メモリ確保をした後に、既存の要素を新しいストレージにコピーしなければならないということだ。 + +まとめよう。 + +1. すでに指定された要素数以上に予約されているなら何もしない +2. まだ動的メモリ確保が行われていなければ動的メモリ確保をする +3. 有効な要素がある場合は新しいストレージにコピーする。 + +~~~c++ +void reserve( size_type sz ) +{ + // すでに指定された要素数以上に予約されているなら何もしない + if ( sz <= capacity() ) + return ; + + // 動的メモリ確保をする + auto ptr = allocate( sz ) ; + // 現在の要素数を保存しておく。 + auto current_size = size() ; + // 有効な要素があれば + if ( begin() != end() ) + { + // 新しいストレージにコピーする + std::copy( begin(), end(), ptr ) ; + // 古いストレージ上の要素を破棄する + destroy_all() ; + // 古いストレージを解放する + deallocate() ; + } + // 新しいストレージを使う + first = ptr ; + // 有効な要素の次のポインター + last = ptr + current_size ; + // 予約したストレージの末尾の次のポインター + reserved_last = ptr + sz ; +} +~~~ + + +## resize + +`resize(sz)`は要素数を`sz`個にする。 + +~~~cpp +int main() +{ + // 要素数0 + std::vector v ; + // 要素数10 + v.resize(10) ; + // 要素数5 + v.resize(5) + // 要素数変わらず + v.resize(5) +} +~~~ + +`resize`は呼び出し前より要素数を増やすことも減らすこともある。また変わらないこともある。 + +要素数が増える場合、増えた要素数の値はデフォルト構築された値になる。 + +~~~cpp +struct X +{ + X() { std::cout << "default constructed.\n" ; } +} ; + +int main() +{ + std::vector v ; + v.resize(5) ; +} +~~~ + +このプログラムを実行すると、"default constructed.\n"は5回標準出力される。 + +`resize(sz, value)`は`resize`を呼び出した結果要素が増える場合、その要素を`value`で初期化する。 + +~~~cpp +int main() +{ + std::vector v = {1,2,3} ; + v.resize(5, 4) ; + // vは{1,2,3,4,4} +} +~~~ + +要素数が減る場合、要素は末尾から順番に破棄されていく。 + +~~~cpp +struct X +{ + ~X() + { std::cout << "destructed.\n"s ; } +} ; + +int main() +{ + std::vector v(5) ; + v.resize(2) ; + std::cout << "resized.\n"s ; +} +~~~ + +このプログラムを実行すると、以下のように出力される。 + +~~~c++ +destructed. +destructed. +destructed. +resized. +destructed. +destructed. +~~~ + +最初の`v.resize(2)`で、`v[4], v[3], v[2]`が書いた順番で破棄されていく。`main`関数を抜けるときに残りの`v[1], v[0]`が破棄される。 + +`resize(sz)`を呼び出したときに`sz`が現在の要素数と等しい場合は何もしない。 + +~~~cpp +int main() +{ + // 要素数5 + std::vector v(5) ; + v.resize(5) ; // 何もしない +} +~~~ + +まとめると`resize`は以下のように動作する。 + +1. 現在の要素数より少なくリサイズする場合、末尾から要素を破棄する +2. 現在の要素数より大きくリサイズする場合、末尾に要素を追加する +3. 現在の要素数と等しくリサイズする場合、何もしない。 + +実装しよう。 + +~~~c++ +void resize( size_type sz ) +{ + // 現在の要素数より少ない + if ( sz < size() ) + { + // 破棄する要素数を求める + auto diff = size() - sz ; + // 末尾から順番に破棄する + destroy_until( rbegin() + diff ) ; + // 新しいサイズを設定 + last = first + sz ; + } + // 現在の要素数より大きい + else if ( sz > size() ) + { + // 少なくとも指定された要素数を保持できるだけのメモリを予約する + reserve( sz ) ; + // 追加の要素を構築する。 + for ( auto iter = last ; iter != reserved_last ; ++iter ) + { + construct( iter ) ; + } + // 新しいサイズを設定 + last = first + sz ; + } +} +~~~ + +要素を破棄する場合、破棄する要素数だけ末尾から順番に破棄する。 + +要素を増やす場合、`reserve`を呼び出してメモリを予約してから、追加の要素を構築する。 + +`sz == size()`の場合は、どちらのif文の条件にも引っかからないので、何もしない。 + +`size(sz, value)`は、追加の引数を取るほか、`construct( iter )`の部分が`constrcut( iter, value )`に変わるだけだ。 + +~~~c++ +void resize( size_type sz, const_reference value ) +{ + // ... + construct( iter, value ) ; + // ... +} +~~~ + +これで自作のvectorはある程度使えるようになった。コンストラクターで要素数を指定できるし、リサイズもできる。 + +~~~c++ +int main() +{ + vector v(10, 1) ; + v[2] = 99 ; + v.resize(5) ; + // vは{1,1,99,1,1} +} +~~~ + +## push_back + +`push_back`は`vector`の末尾に要素を追加する。 + +~~~cpp +int main() +{ + std::vector v ; + // vは{} + v.push_back(1) ; + // vは{1} + v.push_back(2) ; + // vは{1,2} +} +~~~ + +push_backの実装は、末尾の予約された未使用のストレージに値を構築する。もし予約された未使用のストレージがない場合は、新しく動的メモリ確保する。 + +追加の動的メモリ確保なしで保持できる要素の個数はすでに実装した`capacity()`で取得できる。`push_back`は要素をひとつ追加するので、`size() + 1 <= capacity()`ならば追加の動的メモリ確保はいらない。逆に、`size() + 1 > capacity()`ならば追加の動的メモリ確保をしなければならない。追加の動的メモリ確保はすでに実装した`reserve`を使えばよい。 + + +~~~c++ +void push_back( const_reference value ) +{ + // 予約メモリが足りなければ拡張 + if ( size() + 1 > capacity() ) + { + // ひとつだけ増やす + reserve( size() + 1 ) ; + } + + // 要素を末尾に追加 + construct( last, value ) ; + // 有効な要素数を更新 + ++last ; +} +~~~ + +これは動く。ただし、効率的ではない。自作のvectorを使った以下のような例を見てみよう。 + +~~~c++ +int main() +{ + // 要素数10000 + vector v(1000) ; + // 10001個分のメモリを確保する + // 10000個の既存の要素をコピーする + v.push_back(0) ; + // 10002個分のメモリを確保する + // 10001個の既存の要素をコピーする + v.push_back(0) ; +} +~~~ + +たった1つの要素を追加するのに、毎回動的メモリ確保と既存の全要素のコピーをしている。これは無駄だ。 + +`std::vector`は`push_back`で動的メモリ確保が必要な場合、`size()+1`よりも多くメモリを確保する。こうすると、`push_back`を呼び出すたびに毎回動的メモリ確保と全要素のコピーを行う必要がなくなるので、効率的になる。 + +ではどのくらい増やせばいいのか。10個づつ増やす戦略は以下のようになる。 + +~~~cpp +void push_back( const_reference value ) +{ + // 予約メモリが足りなければ拡張 + if ( size() + 1 > capacity() ) + { + // 10個増やす + reserve( capacity() + 10 ) ; + } + construct( last, value ) ; + ++last ; +} +~~~ + +しかしこの場合、以下のようなコードで効率が悪い。 + +~~~cpp +int main() +{ + std::vector v ; + for ( auto i = 0 ; i != 10000 ; ++i ) + { + v.push_back(i) ; + } +} +~~~ + +10個づつ増やす戦略では、この場合に1000回の動的メモリ確保と全要素のコピーが発生する。 + +上のような場合、`vector`の利用者が事前に`v.reserve(10000)`とすれば効率的になる。しかし、コンパイル時に要素数がわからない場合、その手も使えない。 + +~~~cpp +int main() +{ + std::vector inputs ; + // 要素数は実行時にしかわからない + // 10万個の入力が行われるかも知れない + std::copy( + std::ostream_iterator(std::cin>), + std::ostream_iterator(), + std::back_inserter(inputs) ) ; +} +~~~ + +よくある実装は、現在のストレージサイズの2倍のストレージを確保する戦略だ。 + +~~~cpp +void push_back( const_reference value ) +{ + // 予約メモリが足りなければ拡張 + if ( size() + 1 > capacity() ) + { + // 現在のストレージサイズ + auto c = size() ; + // 0の場合は1に + if ( c == 0 ) + c = 1 ; + else + // それ以外の場合は2倍する + c *= 2 ; + + reserve( c ) ; + } + construct( last, value ) ; + ++last ; +} +~~~ + +`size()`は`0`を返す場合もあるということに注意。単に`reserve(size()*2)`としたのでは`size() == 0`のときに動かない。 + +### `shrink_to_fit` + +`shrink_to_fit()`は`vector`が予約しているメモリのサイズを実サイズに近づけるメンバー関数だ。 + +本書で実装してきた自作の`vector`は、`push_back`時に予約しているメモリがなければ、現在の要素数の2倍のメモリを予約する実装だった。すると以下のようなコードで、 + +~~~c++ +int main() +{ + vector v ; + std::copy( std::istream_iterator(std::cin). std::istream_iterator(), + std::back_inserter(v) ) ; +} +~~~ + +ユーザーが4万個のint型の値を入力した場合、65536個のint型の値を保持できるだけのメモリが確保されてしまい、差し引き`sizeof(int) * 25536`バイトのメモリが未使用のまま確保され続けてしまう。 + +メモリ要件の厳しい環境ではこのようなメモリの浪費を避けたい。しかし、実行時にユーザーから任意の個数の入力を受けるプログラムを書く場合には、`push_back`を使いたい。 + +こういうとき、`shrink_to_fit`はvectorが予約するメモリを切り詰めて実サイズに近くする、かもしれない。「かもしれない」というのは、C++の標準規格は`shrink_to_fit`が必ずメモリの予約サイズを切り詰めるよう規定してはいないからだ。 + +自作の`vector`では必ず切り詰める実装にしてみよう。 + + +まず予約するメモリを切り詰めるとはどういうことか。現在予約しているメモリで保持できる最大の要素数は`capacity()`で得られる。実際に保持している要素数を返すのは`size()`だ。すると`size() == capacity()`になるようにすればいい。 + +~~~c++ +vector v ; +// ... +v.shrink_to_fit() ; +v.size() == v.capacity() ; // trueにする +~~~ + +`shrink_to_fit()`を呼んだとき、すでに`size() == capacity()`が`true`である場合は、何もしなくてもよい。 + +それ以外の場合は、現在の有効な要素数文の新しいストレージを確保し、現在の値を新しいストレージにコピーし、古いメモリは破棄する。 + +~~~c++ +void shrink_to_fit() +{ + // 何もする必要がない + if ( size() == capacity() ) + return ; + + // 新しいストレージを確保 + auto ptr = allocate( size() ) ; + // コピー + auto current_size = size() ; + for ( auto raw_ptr = ptr, iter = begin(), iter_end = end() ; + iter != iter_end ; ++iter, ++raw_ptr ) + { + construct( raw_ptr, *iter ) ; + } + // 破棄 + destroy_all() ; + deallocate() ; + // 新しいストレージを使う + first = ptr ; + last = ptr + current_size ; + reserved_last = last ; +} +~~~ + +この実装は`reserve`と似ている。 + diff --git a/README.md b/README.md index 1bea96b..1973bf2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ モチベーション維持のために公開 +GitHub Pagesでの閲覧 + + + # ライセンス GPLv3 diff --git a/docs/index.html b/docs/index.html index d831e49..c60af08 100644 --- a/docs/index.html +++ b/docs/index.html @@ -420,7 +420,7 @@

江添亮のC++入門

  • 配列版new/delete
  • スマートポインター
  • -
  • vectorの実装
  • @@ -1027,7 +1052,7 @@

    標準出力

    C++は他の多くの言語と同じように、逐次実行される。つまり、コードは書いた順番に実行される。そして標準出力のような外部への副作用は、実行された順番で出力される。このコードを実行した結果は以下の通り。

    one two three 

    “three two one”や“two one three”のような出力結果にはならない。

    -

    C++を含む多くの言語でa + b + cと書けるように、opeartor <<a << b << cと書ける。operator <<で標準出力をするには、左端はstd::coutでなければならない。

    +

    C++を含む多くの言語でa + b + cと書けるように、operator <<a << b << cと書ける。operator <<で標準出力をするには、左端はstd::coutでなければならない。

    この場合、まず2*3が計算され6となり、1+6が計算され7となる。

    -

    1+2の法を先に計算したい場合、括弧()で囲むことにより、計算の優先度を帰ることができる。

    +

    1+2の法を先に計算したい場合、括弧()で囲むことにより、計算の優先度を変えることができる。

    int main()
     {
         // 9
    @@ -1505,7 +1530,7 @@ 

    文法エラー

    ^~~

    1行目はエラー内容をテキストで表現したものだ。これによると、‘auto’の前に’,‘か’;’があるべきとあるが、やはりまだわからない。

    2行目は問題のある箇所のソースコードを部分的に抜粋したもので、3行目はそのソースコードの問題のある文字を視覚的にわかりやすく示しているものだ。

    -

    ともかく、コンパイラーの支持に従って’auto’の前に’,’を付けてみよう。

    +

    ともかく、コンパイラーの指示に従って’auto’の前に’,’を付けてみよう。

        ,auto y = x + 1 ;

    これをコンパイルすると、また違ったエラーメッセージが表示される。

    main.cpp: In function ‘int main()’:
    @@ -1820,19 +1845,19 @@ 

    条件分岐

    真(true)というのは、意味が真であるときだ。正しい、成り立つ、正解などと言い換えてもよい。それ以外の場合はすべて偽(false)だ。正しくない、成り立たない、不正解などと言い換えてもいい。

    整数や浮動小数点数の場合、話は簡単だ。

    -
    int main()
    -{
    -    // 1は2より小さいか?
    -    if ( 1 < 2 )
    -    {   // 真、お使いのコンピューターは正常です
    -        std::cout << "Your computer works just fine.\n"s ;
    -    }
    -    else
    -    {
    -        // 偽、お使いのコンピューターには深刻な問題があります
    -        std::cout << "Your computer has serious issues."
    -    }
    -}
    +

    文字列の場合、内容が同じであれば等しい。違うのであれば等しくない。

    -

    比較演算子の結果はbool値になるということを覚えてるだろうか。“1 < 2”はtrueになり、“1 > 2”はfalseになる。

    +

    比較演算子の結果はbool値になるということを覚えてるだろうか。“1 < 2”はtrueになり、“1 > 2”はfalseになる。

    bool値同士も同値比較ができるということは、“(1 < 2) == true”のように書くことも可能だということだ。

    + else + { std::cout << "Bad.\n"s ; } +}

    このコードは、operator &&を使えば簡潔に書ける。

    論理和は、「AもしくはB」を表現するのに使える。

    @@ -2481,7 +2508,7 @@

    これまでのおさらい

    int main()
     {
         // 身長1.63m
    -    double height = 1.63 ; 
    +    double height = 1.63 ;
         // 体重73kg
         double mass = 73.0 ;
     
    @@ -2522,7 +2549,7 @@ 

    これまでのおさらい

    int main()
     {
         // 身長1.6m
    -    double height = 1.63 ; 
    +    double height = 1.63 ;
         // 体重73kg
         double mass = 73.0 ;
     
    @@ -2555,7 +2582,7 @@ 

    標準入力

    すると今度は身長が1.48mで体重が48kgの人がやってきて私のBMIも計測しろとうるさい。しかも昨日と今日で体重が変わったからどちらも計測したいと言い出す始末。

    こういうとき、プログラムのコンパイル時ではなく、実行時に値を入力できたならば、いちいちプログラムをコンパイルし直す必要がなくなる。

    -

    入力にはstd::cinを使う。std::coutは標準出力を扱うのに対し、std::cinは標準入力を扱う。std::coutoperator <<を使って値を出力したのに対し、std::cinopeartor >>を使って値を変数に入れる。

    +

    入力にはstd::cinを使う。std::coutは標準出力を扱うのに対し、std::cinは標準入力を扱う。std::coutoperator <<を使って値を出力したのに対し、std::cinoperator >>を使って値を変数に入れる。

    int main()
     {
         // 入力を受け取るための変数
    @@ -2637,7 +2664,7 @@ 

    標準入力

    “true”, “false”という文字列でtrue, falseの入力をしたい場合、std::cinstd::boolalphaを「入力」させる。

    int main()
     {
    -    // 整数
    +    // bool型
         bool b{} ;
         std::cin >> std::boolalpha >> b ;
     
    @@ -2799,7 +2826,7 @@ 

    これまでのおさらい

    goto文

    ここでは繰り返し(ループ)の基礎的な仕組みを理解するために、最も原始的で最も使いづらい繰り返しの機能であるgoto文を学ぶ。goto文で実用的な繰り返し処理をするのは面倒だが、恐れることはない。より簡単な方法もすぐに説明するからだ。なぜ本書でgoto文を先に教えるかと言うと、あらゆる繰り返しは、結局のところif文goto文へのシンタックスシュガーにすぎないからだ。goto文を学ぶことにより、繰り返しを恐れることなく使う本物のプログラマーになれる。

    無限ループ

    -

    “hello”と3回出力するプログラムはどうやって書くのだろうか。“hello”を1回出力するプログラムの書き方はすでにわかっているので、同じ文を3回書けばよい。。

    +

    “hello”と3回出力するプログラムはどうやって書くのだろうか。“hello”を1回出力するプログラムの書き方はすでにわかっているので、同じ文を3回書けばよい。

    // 1回"hello\n"を出力する関数
     void hello()
     {
    @@ -2833,7 +2860,7 @@ 

    無限ループ

    std::cout << 1 ; // ラベルskipまで飛ぶ - goto skip ; + goto skip ; std::cout << 2 ; @@ -3095,7 +3122,7 @@

    インデックスループ

    具体的に考えてみよう。n == 3のとき、つまり3回繰り返すときを考えよう。

    1. 1回目のif文実行の時、i == 0
    2. -
    3. 2回めのif文実行の時、i == 1
    4. +
    5. 2回目のif文実行の時、i == 1
    6. 3回目のif文実行の時、i == 2
    7. 4回目のif文実行の時、i == 3
    @@ -4043,7 +4070,7 @@

    vector

    { std::vector<int> v ; - for ( int i = 0 ; i != 0 ; ++i ) + for ( int i = 0 ; i != 1000 ; ++i ) { v.push_back( i ) ; } @@ -4093,7 +4120,7 @@

    vector

    // 0, 0番目の最初の要素 std::cout << v.at(0) ; // 4, 5番目の要素 - std::cout << v.at(5) ; + std::cout << v.at(4) ; // 9, 10番目の最後の要素 std::cout << v.at(9) ; }
    @@ -4744,7 +4771,7 @@

    情報の単位

    情報の最小単位はビット(bit)だ。ビットは2種類の状態を表現できる。たとえばbool型はtrue/falseという2種類の状態を表現できる。

    しかし、2種類の状態しか表現できない整数は使いづらい。0もしくは1しか表現できない整数とか、100もしく1000しか表現できない整数は使い物にならない。

    また、ビットという単位も扱いづらい。コンピューターは膨大な情報を扱うので、ビットをいくつかまとめたバイト(byte)を単位として情報を扱っている。1バイトが何ビットであるかは環境により異なる。本書では最も普及している1バイトは8ビットを前提にする。

    -

    1ビットは2種類の状態を表現できるので、1バイトの中の8ビットは\(2^8 = 256\)種類の状態を表現できる。2バイトならば16ビットとなり、\(2^8 = 65536\)種類の状態を表現できる。

    +

    1ビットは2種類の状態を表現できるので、1バイトの中の8ビットは\(2^8 = 256\)種類の状態を表現できる。2バイトならば16ビットとなり、\(2^16 = 65536\)種類の状態を表現できる。

    1バイトで表現された整数

    整数の表現方法について理解するために、1バイトで表現された整数を考えよう。

    1バイトは8ビットであり256種類の状態を表現できる。整数を0から正の方向の数だけ表現したいとすると、0から255までの値を表現できることになる。

    @@ -4892,7 +4919,7 @@

    long long int型

    auto c = 123ull ;

    short int型

    short int型int型より小さい範囲の値を扱う整数型だ。long, long longと同様に、unsigned short int型もある。単にshortと書くと、short intと同じ意味になる。

    -

    整数リテラルでshort int型を表現する方法はない。z2

    +

    整数リテラルでshort int型を表現する方法はない。

    char型

    char型はやや特殊で、char, signed char, unsigned charの三種類の型がある。signed charcharは別物だ。char型は整数型であり、後で説明するように文字型でもある。char型の符号の有無は実装ごとに異なる。

    整数型のサイズ

    @@ -5200,7 +5227,7 @@

    浮動小数点数同士の変換

    // 変換 long double c = a ; }
    -

    異なる浮動小数点数同士を演算すると、float<double<long doubeの順で大きい浮動小数点数型に合わせて変換される。

    +

    異なる浮動小数点数同士を演算すると、float<double<long doubleの順で大きい浮動小数点数型に合わせて変換される。

    int main()
     {
         // float
    @@ -5659,7 +5686,7 @@ 

    型名の別名を

    これは変数の宣言と同じ文法だ。変数の宣言が以下のような文法で、

    型名 変数名 ;

    これにtypedefキーワードを使えばtypedef名の宣言になる。

    -

    しかしtypedefキーワードによるtypdef名の宣言は罠が多い。例えば熟練のC++プログラマーでも、以下のコードが合法だということに驚くだろう。

    +

    しかしtypedefキーワードによるtypedef名の宣言は罠が多い。例えば熟練のC++プログラマーでも、以下のコードが合法だということに驚くだろう。

    int main()
     {
         int typedef Number ;
    @@ -5766,7 +5793,7 @@ 

    スコープ

    f() ; // 2 }

    宣言されている場所に注意が必要だ。名前fは3つある。最初の関数呼び出しの時点ではグローバル名前空間のfが呼ばれる。まだ名前fは関数mainの中で宣言されていないからだ。そして関数mainのスコープの中で名前fが宣言される。このときグローバル名前空間のfは隠される。そのため、次の関数fの呼び出しでは関数mainのfが呼ばれる。次にブロックの中に入る。ここで関数fが呼ばれるが、まだこのfは関数mainのfだ。その後にブロックの中で名前fが宣言される。すると次の関数fの呼び出しはブロックのfだ。ブロックから抜けた後の関数fの呼び出しは関数mainのfだ。

    -

    この章では名前について解説した。名前は難しい。難しいが、プログラミングにおいては名前を向き合わなければならない。

    +

    この章では名前について解説した。名前は難しい。難しいが、プログラミングにおいては名前と向き合わなければならない。

    イテレーターの基礎

    vectorの章ではvectorの要素にアクセスする方法としてメンバー関数at(i)を学んだ。at(i)はi番目の要素にアクセスできる。ただし最初の要素は0番目だ。

    int main()
    @@ -5982,7 +6009,7 @@ 

    なんでもイテレーター

    *output_iter = *iter ; } } ;
    -

    書き換えた関数output_iterは新しくoutput_iterという引数を取る。これはイテレーターだ。std::coutに出力する代わりに、このイテレーターに書き込むように変更している。

    +

    書き換えた関数output_allは新しくoutput_iterという引数を取る。これはイテレーターだ。std::coutに出力する代わりに、このイテレーターに書き込むように変更している。

    こうすることによって、出力にも様々なイテレーターが使える。

    標準出力に出力するイテレーターがある。

    -

    desitination(5)というのは、vectorにあらかじめ5個の要素を入れておくという意味だ。あらかじめ入っている要素の値はintの場合ゼロになる。

    +

    destination(5)というのは、vectorにあらかじめ5個の要素を入れておくという意味だ。あらかじめ入っている要素の値はintの場合ゼロになる。

    この他にもイテレーターは様々ある。自分でイテレーターを作ることもできる。そして、関数output_allはイテレーターにさえ対応していれば様々な処理にコードを一行たりとも変えずに使えるのだ。

    イテレーターと添字の範囲

    イテレーターは順序のある値の集合を表現するために、最初の要素への参照と、最後の次の要素への参照のペアを用いる。

    @@ -6176,7 +6203,7 @@

    lvalueリファレンス

    assign_3( a ) ; // a == 1 -}
    +}

    しかし、時には変数の値を直接書き換えたい場合がある。この時lvalueリファレンス(reference)が使える。lvalueリファレンスは変数に&を付けて宣言する

    int main()
     {
    @@ -6185,7 +6212,7 @@ 

    lvalueリファレンス

    ref = 3 ; - // a == 3 + // a == 3 // refはaなので同じく3 }

    この例で、変数refは変数aへの参照(リファレンス)なので、変数aと同じように使える。

    @@ -6317,7 +6344,7 @@

    const

    // OK、constがついている const int & cref = x ; } -

    constのついているlvalueリファレンスは何の役に立つのかというと、関数の引数を摂るときに役に立つ。

    +

    constのついているlvalueリファレンスは何の役に立つのかというと、関数の引数を取るときに役に立つ。

    例えば以下のコードは非効率的だ。

    +} ;

    これを見ると、for文によるイテレーターのループは全く同じコードだとわかる。

    全く同じfor文を手で書くのは間違いの元だ。同じコードはできれば書きたくない。ここで必要なのは、共通な処理は一度書くだけで済ませ、特別な処理だけを記述すればすむような方法だ。

    この問題を解決するには、問題を分割することだ。問題を「for文によるループ」と「特別な処理」に分けることだ。

    @@ -6586,7 +6613,7 @@

    all_of/any_of/none_of

    { for ( auto iter = first ; iter != last ; ++iter ) { - if ( pref( *iter ) == false ) + if ( pred( *iter ) == false ) return false ; } @@ -6910,7 +6937,7 @@

    equal

    return true ; } ;

    for文の終了条件ではi != last1だけを見ていて、j != last2は見ていないが、これは問題がない。なぜならば、このfor文が実行されるのは、要素数が等しい場合だけだからだ。

    -

    関数predを取るequal(first1, last1, first2, last2, pred)もある。このpredpread(a, b)で、abが等しい場合にtrue、そうでない場合にfalseを返す関数だ。つまりa == bのoperator ==の代わりに使う関数を指定する。

    +

    関数predを取るequal(first1, last1, first2, last2, pred)もある。このpredpred(a, b)で、abが等しい場合にtrue、そうでない場合にfalseを返す関数だ。つまりa == bのoperator ==の代わりに使う関数を指定する。

    equalに関数を渡すことにより、例えば小数点以下の値を誤差として切り捨てるような処理が書ける。

    int main()
     {
    @@ -7100,31 +7127,37 @@ 

    fill

    generate

    generatefillに似ているが、値としてvalueをとるのではなく、関数genを取る。

    generate(first, last, gen)はイテレーター[first, last)の範囲のイテレーターが参照する要素にgen()を代入する。

    -

    ~~~cpp int main() { std::vector v = {1,2,3,4,5} ; auto gen_zero = { return 0 ; } ; std::generate( std::begin(v), std::end(v), gen_zero ) ; // vは{0,0,0,0,0} }

    -

    generate_n(first, n, gen)fill_ngenerete版だ。

    -

    実装例は単純だ。

    - +

    実装例は単純だ。

    +

    remove

    remove(first, last, value)はイテレーター[first,last)の範囲の参照する要素から、値valueに等しいものを取り除く。そして新しい終端イテレーターを返す。

    アルゴリズムremoveが値を取り除くというとやや語弊がある。例えば以下のような数列があり、

    @@ -7137,223 +7170,223 @@

    remove

    以下のようになる。

    1,3,4,?,?,?,?

    removeの戻り値は、新しいイテレーターの終端を返す。

    - -

    この例では、remove[first,last)から値valueに等しい要素を取り除いたイテレーターの範囲を戻り地として返す。その戻り値がlast2だ。[first,last2)が値を取り除いた後の新しいイテレーターの範囲だ。

    + +

    この例では、remove[first,last)から値valueに等しい要素を取り除いたイテレーターの範囲を戻り値として返す。その戻り値がlast2だ。[first,last2)が値を取り除いた後の新しいイテレーターの範囲だ。

    removeを呼び出しても元のvectorの要素数が変わることはない。removeはvectorの要素の値を変更するだけだ。

    以上を踏まえて、以下がremoveを使う例だ。

    - -

    remove_if(first, last, pred)は、[first,last]の範囲の要素を指すイテレーターiのうち、関数predに渡した結果pred(*i)trueになる要素を取り除くアルゴリズムだ。

    + std::vector<int> v = {1,2,3} ; + + auto last = std::remove( std::begin(v), std::end(v), 2 ) ; + + // "12" + std::for_each( std::begin(v), last, + [](auto x) { std::cout << x ; } ) ; + + std::vector<int> w = {1,2,2,3,2,2,4} ; + + auto last2 = std::remove( std::begin(w), std::end(w), 2 ) ; + + // "134" + std::for_each( std::begin(w), last2, + [](auto x) { std::cout << x ; } ) ; + +}
    +

    remove_if(first, last, pred)は、[first,last]の範囲の要素を指すイテレーターiのうち、関数predに渡した結果pred(*i)trueになる要素を取り除くアルゴリズムだ。

    +

    removeは現在知っている知識だけではまだ完全に実装できない。以下は不完全な実装の例だ。removeを完全に理解するためにはムーブセマンティクスの理解が必要だ。

    -
    auto remove_if = []( auto first, auto last, auto pred )
    -{
    -    // removeする最初の要素
    -    auto removing = std::find_if( first, last, pred ) ;
    -    // removeする要素がなかった
    -    if ( removing == last )
    -        return last ;
    -
    -    // removeする要素の次の要素
    -    auto remaining = removing ;
    -    ++remaining ;
    -
    -    // removeする要素に上書きする
    -    for (  ; remaining != last ; ++remaining )
    -    {
    -        // 上書き元も取り除くのであればスキップ
    -        if ( pred( *remaining ) == false )
    -        {
    -            *removing = *remaining ;
    -            ++removing ;
    -        }
    - 
    -    }
    -    // 新しい終端イテレーター
    -    return removing ;
    -} ;
    +

    ラムダ式

    実は以下の形の関数は、「関数」ではない。

    - +

    これはラムダ式と呼ばれるC++の機能で、関数のように振る舞うオブジェクトを作るための式だ。

    基本

    ラムダ式の基本の文法は以下の通り

    - +

    これを細かく分解すると以下のようになる。

    - +

    ラムダ導入子はさておく。

    引数リストは通常の関数と同じように型名と名前を書ける。

    - +

    ラムダ式では、引数リストautoキーワードが使える。

    - +

    このように書くとどんな型でも受け取れるようになる。

    - -

    複合文{}だ。この{}の中に通常の関数と同じように複数の文を書くことができる。

    - +

    複合文{}だ。この{}の中に通常の関数と同じように複数の文を書くことができる。

    +

    最後の文末の最後につけるセミコロンだ。これは“1+1 ;”とするのと変わらない。“1+1”や“”はで、を使うことができる。だけが入ったを専門用語では式文と呼ぶが特に覚える必要はない。

    - +

    ラムダ式なので式文の中に書くことができる。

    ラムダ式なので、そのまま関数呼び出しすることもできる。

    - +

    これはわかりやすくインデントすると以下のようになる。

    - +

    ラムダ式が引数を一つも取らない場合、引数リストは省略できる。

    - +

    ラムダ式の戻り値の型はreturn文から推定される。

    - +

    return文で複数の型を返した場合は推定ができないのでエラーになる。

    - -

    戻り値の型を指定したい場合は引数リストの後に->を書き、型名を書く。

    - +

    戻り値の型を指定したい場合は引数リストの後に->を書き、型名を書く。

    +

    戻り値の型の推定は通常の関数も同じだ。

    - +

    キャプチャー

    ラムダ式は書かれている関数のローカル変数を使うことができる。これをキャプチャーという。キャプチャーは通常の関数にはできないラムダ式の機能だ。

    - +

    キャプチャーにはコピーキャプチャーリファレンスキャプチャーがある。

    コピーキャプチャー

    コピーキャプチャーは変数をコピーによってキャプチャーする。

    コピーキャプチャーをするには、ラムダ式[=]と書く。

    - -

    コピーキャプチャーした変数はラムダ式の中で変更できない。

    +

    コピーキャプチャーした変数はラムダ式の中で変更できない。

    +

    変更できるようにする方法もあるのだが、通常は使われない。

    リファレンスキャプチャー

    リファレンスキャプチャーは変数をリファレンスによってキャプチャーする。

    リファレンスを覚えているだろうか。リファレンスは初期化時の元の変数を参照する変数だ。

    - -

    リファレンスキャプチャーを使うには、ラムダ式[&]と書く。

    -

    リファレンスキャプチャーした変数をラムダ式の中で変更すると、元の変数が変更される。

    + // 通常の変数 + int y = x ; + + // 変数を変更 + y = 1 ; + // xの値は変わらない + + // リファレンス + int & ref = x ; + + // リファレンスを変更 + ref = 1 ; + // xの値が変わる +} +

    リファレンスキャプチャーを使うには、ラムダ式[&]と書く。

    + [&] { return x ; } ; +} +

    リファレンスキャプチャーした変数をラムダ式の中で変更すると、元の変数が変更される。

    +

    ラムダ式についてはまだ色々な機能があるが、本書での解説はここまでとする。

    クラスの基本

    C++はもともとC言語にクラスの機能を追加することを目的とした言語だった。

    @@ -7366,506 +7399,506 @@

    クラスの基本

    変数をまとめる

    2次元座標上の点(x,y)を表現するプログラムを書くとする。

    とりあえずint型で表現してみよう。

    - -

    これはわかりやすい。ところでものは相談だが、点は複数表現したい。

    -

    これはわかりにくい。ところで点はユーザーがいくつでも入力できるものとしよう。

    + // 表現 + int point_x = 0; + int point_y = 0; +} +

    これはわかりやすい。ところでものは相談だが、点は複数表現したい。

    + int x2 = 0 ; + int y2 = 0 ; + + int x3 = 0 ; + int y3 = 0 ; +} +

    これはわかりにくい。ところで点はユーザーがいくつでも入力できるものとしよう。

    +

    これはとてもわかりにくい。

    ここでクラスの出番だ。クラスを使うと点を表現するコードは以下のように書ける。

    - +

    点を複数表現するのもわかりやすい。

    - +

    ユーザーが好きなだけ点を入力できるプログラムもわかりやすく書ける。

    - +

    これがクラスの変数をまとめる機能だ。

    クラスを定義するには、キーワードstructに続いてクラス名を書く。

    - -

    変数は{}の中に書く。

    - +

    変数は{}の中に書く。

    +

    このクラスの中に書かれた変数のことを、データメンバーという。正確には変数ではない。

    定義したクラスは変数として宣言して使うことができる。クラスデータメンバーを使うには、クラス名に引き続いてドット文字を書きデータメンバー名を書く。

    - +

    クラスデータメンバーの定義は変数ではない。オブジェクトではない。つまり、それ自体にストレージが割り当てられてはいない。

    - -

    クラスの変数を定義したときに、その変数のオブジェクトに紐付いたストレージが使われる。

    -

    クラスの変数を定義するときにデータメンバーを初期化できる。

    + // これは変数ではない。 + int data ; +} ; +

    クラスの変数を定義したときに、その変数のオブジェクトに紐付いたストレージが使われる。

    + int data ; +} ; + +int main() +{ + S s1 ; // 変数 + // オブジェクトs1に紐付いたストレージ + s1.data = 0 ; + + S s2 ; + // 別のストレージ + s2.data = 1 ; + + // false + bool b = s1.data == s2.data ; +} +

    クラスの変数を定義するときにデータメンバーを初期化できる。

    +

    クラスの初期化で{1,2,3}と書くと、クラスの最初のデータメンバーが1で、次のデータメンバーが2で、その次のデータメンバーが3で、それぞれ初期化される。

    クラスをコピーすると、データメンバーがそれぞれコピーされる。

    - +

    まとめた変数に関数を提供する

    分数を表現するプログラムを書いてみよう。

    - -

    分子aと分母bはクラスにまとめることができそうだ。そうすれば複数の分数を扱うのも楽になる。

    -
    struct fractional
    +
    -

    ところで、この出力を毎回書くのが面倒だ。こういう処理は関数にまとめたい。

    - +

    分子numと分母denomはクラスにまとめることができそうだ。そうすれば複数の分数を扱うのも楽になる。

    + -

    この関数valueはクラスfractional専用だ。であれば、この関数をクラス事態に関連付けたい。そこでC++にはメンバー関数という機能がある。

    -

    メンバー関数はクラスの中で定義する関数だ。

    - +

    ところで、この出力を毎回書くのが面倒だ。こういう処理は関数にまとめたい。

    + -

    メンバー関数はクラスのデータメンバーを使うことができる。

    - +

    この関数valueはクラスfractional専用だ。であれば、この関数をクラス自体に関連付けたい。そこでC++にはメンバー関数という機能がある。

    +

    メンバー関数はクラスの中で定義する関数だ。

    + -

    メンバー関数を呼び出すには、クラスのオブジェクトに続いてドット文字を書き、メンバー関数名を書く。後は通常の関数のように書く。

    - +

    メンバー関数はクラスのデータメンバーを使うことができる。

    + -

    メンバー関数から使えるデータメンバーは、メンバー関数が呼ばれたクラスのオブジェクトのデータメンバーだ。

    - +

    メンバー関数を呼び出すには、クラスのオブジェクトに続いてドット文字を書き、メンバー関数名を書く。後は通常の関数のように書く。

    + -

    このprintを非メンバー関数として書くと以下のようになる。

    - +

    メンバー関数から使えるデータメンバーは、メンバー関数が呼ばれたクラスのオブジェクトのデータメンバーだ。

    + -

    メンバー関数は隠し引数としてクラスのオブジェクトを受け取っている関数だ。メンバー関数の呼び出しには、対応するクラスのオブジェクトが必要になる。

    - +

    このprintを非メンバー関数として書くと以下のようになる。

    + -

    メンバー関数はデータメンバーを変更することもできる。

    - +

    メンバー関数は隠し引数としてクラスのオブジェクトを受け取っている関数だ。メンバー関数の呼び出しには、対応するクラスのオブジェクトが必要になる。

    + -

    先程の分数クラスに値を設定するためのメンバー関数を追加してみよう。

    - +

    メンバー関数はデータメンバーを変更することもできる。

    + + int data ; + void f() + { + data = 3 ; + } +} ;
    +

    先程の分数クラスに値を設定するためのメンバー関数を追加してみよう。

    +

    メンバー関数set(num)を呼び出すと、値が\(\frac{num}{1}\)になる。メンバー関数set(num, denom)を呼び出すと、値が\(\frac{num}{denom}\)になる。

    ところで上のコードを見ると、データメンバーと引数の名前の衝突を避けるために、アンダースコアを使っている。

    データメンバーと引数の名前が衝突するとどうなるのか。確かめてみよう。

    - +

    結果は0だ。メンバー関数fの中の名前xは引数名のxだからだ。

    すでに名前はスコープに属するということは説明した。実はクラスもスコープを持つ。上のコードは以下のようなスコープを持つ。

    - +

    内側のスコープは外側のスコープの名前を隠す。そのため、クラススコープのxはグローバル名前空間スコープxを隠す。関数のブロックスコープのxはクラススコープのxを隠す。

    名前がどのスコープに属するかを明示的に指定することによって、隠された名前を使うことができる。

    - +

    名前空間スコープを明示するためにnamespace_name::nameを使うように、クラススコープを明示するためにclass_name::nameを使うことができる。

    これを使えば、分数クラスは以下のように書ける。

    - +

    より自然に振る舞うクラス

    整数型のintについて考えてみよう。

    - -

    同様のことを、前章の分数クラスで書いてみよう。

    - +

    これは読みにくい。できれば以下のように書きたいところだ。

    +

    C++ではクラスをこのように自然に振る舞わせることができる。

    より自然な初期化

    int型は初期化にあたって値を設定できる。

    - +

    クラスでこのような初期化をするには、コンストラクターを書く。

    - -

    コンストラクタークラス特殊なメンバー関数として定義する。メンバー関数としてのコンストラクターは、名前がクラス名で、戻り値の型は記述しない。

    -
    struct class_name
    +
    -

    コンストラクターデータメンバーの初期化に特別な文法を持っている。関数の本体の前にコロンを書き、データメンバー名をそれぞれカンマで区切って初期化する。

    + int num ; + int denom ; + + // コンストラクター + fractional( int num ) + : num(num), denom(1) + { } +} ; + +int main() +{ + fractional a = 1 ; + fractional b = 2 ; +}
    +

    コンストラクタークラス特殊なメンバー関数として定義する。メンバー関数としてのコンストラクターは、名前がクラス名で、戻り値の型は記述しない。

    -

    このとき、引数名とデータメンバー名が同じでもよい。

    + // コンストラクター + class_name() { } +} ; +

    コンストラクターデータメンバーの初期化に特別な文法を持っている。関数の本体の前にコロンを書き、データメンバー名をそれぞれカンマで区切って初期化する。

    -

    x(x)の最初のxclass_name::xとして、次のxは引数名のxとして認識される。そのためこのコードは期待通りに動く。

    -

    コンストラクターの特別なメンバー初期化を使わずに、コンストラクターの関数の本体でデータメンバーを変更してもよい。

    + int data_member ; + + class_name( int value ) + : data_member(value) + { } + +} ; +

    このとき、引数名とデータメンバー名が同じでもよい。

    + : x(x) { } +} ; +

    x(x)の最初のxclass_name::xとして、次のxは引数名のxとして認識される。そのためこのコードは期待通りに動く。

    +

    コンストラクターの特別なメンバー初期化を使わずに、コンストラクターの関数の本体でデータメンバーを変更してもよい。

    +

    この場合、xは関数の本体が実行される前に一度初期化され、その後、値を代入されるという挙動の違いがある。

    コンストラクターはクラスが初期化されるときに実行される。例えば以下のプログラムを実行すると、

    - +

    以下のように出力される。

    123

    コンストラクターのついでにデストラクターも学んでおこう。コンストラクターはクラスのオブジェクトが初期化されるときに実行されるが、デストラクターはクラスのオブジェクトが破棄されるときに実行される。

    デストラクターの宣言はコンストラクターと似ている。違う点は、クラス名の前にチルダ文字を書くところだ。

    - -

    関数のローカル変数は、ブロックスコープを抜ける際に破棄される。破棄は構築の逆順に行われる。

    -
    int main()
    +
    -

    さっそく初期化時と終了時に標準出力をするクラスで確かめてみよう。

    - +

    関数のローカル変数は、ブロックスコープを抜ける際に破棄される。破棄は構築の逆順に行われる。

    + -

    このクラスを以下のように使うと、

    - +

    さっそく初期化時と終了時に標準出力をするクラスで確かめてみよう。

    + + int n ; + S( int n ) + : n(n) + { + std::cout << "constructed: "s << n << "\n"s ; + } + + ~S() + { + std::cout << "destructed: "s << n << "\n"s ; + } +} ;
    +

    このクラスを以下のように使うと、

    +

    以下のように出力される

    constructed: 1
     constructed: 2
    @@ -7884,175 +7917,175 @@ 

    より自然な初期化

    bはブロックスコープの終わりに達したのでaの構築の後、cの構築の前に破棄される。破棄は構築の逆順で行われるので、aよりも先にcが破棄される。

    コンストラクターデストラクターは戻り値を返さないので、return文には値を書かない。

    - -

    コンストラクターは複数の引数を取ることもできる。

    - +

    コンストラクターは複数の引数を取ることもできる。

    +

    複数の引数をとるコンストラクターを呼び出すには“=”は使えない。“()”か“{}”を使う必要がある。

    上のコードを見ると、コンストラクターは引数の数以外にやっていることはほとんど同じだ。こういう場合、コンストラクターを一つにする方法がある。

    実はコンストラクターに限らず、関数はデフォルト実引数を取ることができる。書き方は仮引数に“=”で値を書く

    - -

    デフォルト実引数を指定した関数の仮引数に実引数を渡さない場合、デフォルト実引数で指定した値が渡される。

    -

    ところで、仮引数実引数という聞きなれない言葉が出てきた。これは関数の引数を区別するための言葉だ。仮引数は関数の宣言の引数。実引数は関数呼び出しのときに引数に渡す値のことを意味する。

    -
    // xは仮引数
    -void f( int x ) { }
    +
    +

    デフォルト実引数を指定した関数の仮引数に実引数を渡さない場合、デフォルト実引数で指定した値が渡される。

    +

    ところで、仮引数実引数という聞きなれない言葉が出てきた。これは関数の引数を区別するための言葉だ。仮引数は関数の宣言の引数。実引数は関数呼び出しのときに引数に渡す値のことを意味する。

    +

    デフォルト実引数は関数の実引数の一部を省略できる。

    ただし、デフォルト実引数を使った以後の仮引数には、すべてデフォルト実引数がなければならない。

    - +

    デフォルト実引数で途中の引数だけ省略することはできない。

    - +

    デフォルト実引数を使うと、コンストラクターを一つにできる。

    - -

    コンストラクターの数を減らす方法はもう一つある。デリゲートコンストラクターだ。

    -

    デリゲートコンストラクターは初期化処理を別のコンストラクターにデリゲート(丸投げ)する。丸投げ先のコンストラクターの初期化処理が終わり次第、デリゲートコンストラクターの関数の本体が実行される。

    - +

    コンストラクターの数を減らす方法はもう一つある。デリゲートコンストラクターだ。

    + + int num ; + int denom ; + + fractional( int num, int denom ) + : num(num), denom(denom) + { } + + // デリゲートコンストラクター + fractional( int num ) + : fractional( num, 1 ) + { } +} ;
    +

    デリゲートコンストラクターは初期化処理を別のコンストラクターにデリゲート(丸投げ)する。丸投げ先のコンストラクターの初期化処理が終わり次第、デリゲートコンストラクターの関数の本体が実行される。

    +

    このプログラムを実行すると、以下のように出力される。

    constructor
     delegating constructor

    まず“S()”が呼ばれるが、処理を“S(int)”にデリゲートする。“S(int)”の処理が終わり次第“S()”の関数の本体が実行される。そのためこのような出力になる。

    コンストラクターを減らすのはよいが、減らしすぎても不便だ。以下の例を見てみよう。

    - +

    クラスAの変数は問題ないのに、クラスBの変数はエラーになる。これはクラスBには引数を取らないコンストラクターがないためだ。

    クラスBに引数を必要としないコンストラクターを書くと、具体的に引数を渡さなくても初期化ができるようになる。

    - -

    もしくは、デフォルト引数を使ってもよい。

    + B() { } + B( int x ) { } +} ; + +int main() +{ + B b ; // OK +}
    +

    もしくは、デフォルト引数を使ってもよい。

    +

    もちろん、ユーザーが値を指定しなければならないようなクラスは値を指定するべきだ。

    - +

    自然な演算子

    int型は+-*/といった演算子を使うことができる。

    - +

    クラスも演算子を使った自然な記述ができる。クラスを演算子に対応させることを、演算子のオーバーロードという。

    分数クラスの足し算を考えよう。

      @@ -8060,444 +8093,444 @@

      自然な演算子

    • 分母が異なるならば互いの分母を掛けて、分母を揃えて足す。

    コードにすると以下のようになる。

    - -

    しかし、この関数addを使ったコードは以下のようになる。

    - +

    しかし、この関数addを使ったコードは以下のようになる。

    +

    これはわかりにくい。できれば、以下のように書きたい。

    - -

    C++では演算子は関数として扱うことができる。演算子の名前はopeartor opで、例えば+演算子の名前はoperator +になる。

    + +

    C++では演算子は関数として扱うことができる。演算子の名前はoperator opで、例えば+演算子の名前はoperator +になる。

    関数operator +は引数を2つ取り、戻り値を返す関数だ。

    - +

    このようにoperator +を書くと、以下のようなコードが書ける。

    - +

    同様に、引き算はoperator -、掛け算はoperator *、割り算はoperator /だ。

    以下に関数の宣言を示すので実際に分数の計算を実装してみよう。

    - +

    引き算は足し算とほぼ同じだ。

    - -

    掛け算と割り算は楽だ。

    - +

    掛け算と割り算は楽だ。

    +

    演算子のオーバーロード

    二項演算子

    C++には様々な演算子があるが、多くが二項演算子と呼ばれる演算子だ。二項演算子は2つの引数を取り、値を返す。

    - +

    このような演算子はoperator +のように、キーワードoperatorに続いて演算子の文字を書くことで、関数名とする。あとは通常の関数と変わらない。

    - -

    戻り値の型は何でもよい。

    +S add( S a, S b ) ; +S operator + ( S a, S b ) ; +

    戻り値の型は何でもよい。

    +

    演算子としてではなく、関数と同じように呼び出すこともできる。

    - +

    演算子のオーバーロードでは、少なくとも1つのユーザー定義された型がなければならない。つまり以下のような演算子のオーバーロードはできないということだ。

    - +

    二項演算子にはオペランドと呼ばれる式を取る。

    - +

    この場合、二項演算子operator +にはa, bという2つのオペランドがある。

    二項演算子をオーバーロードする場合、最初の引数が最初のオペランド、次の引数が次のオペランドに対応する。

    - +

    そのため、上の例で“x+y”と“y+x”を両方使いたい場合は、

    - +

    も必要だ。

    現実のコードでは、二項演算子のオーバーロードは以下のように書くことが多い。

    - +

    const &という特別な書き方をする。&についてはすでに学んだように、リファレンスだ。リファレンスを使うことによって値をコピーせずに効率的に使うことができる。

    constというのは値を変更しない変数を宣言する機能だ。

    - -

    constをつけると値を変更できなくなる。

    -

    一般にoperator +のような演算子は、オペランドに渡した変数を書き換えない処理をすることが期待されている。

    +

    constをつけると値を変更できなくなる。

    +

    一般にoperator +のような演算子は、オペランドに渡した変数を書き換えない処理をすることが期待されている。

    +

    もちろん、operator +をオーバーロードして引数をリファレンスで取り、値を書き換えるような処理を書くこともできる。ただ、通常はそのような処理をすることはない。

    しかし、処理の効率のためにリファレンスは使いたい。

    そのようなときに、constかつリファレンスを使うと、効率的で値の変更ができないコードが書ける。

    - +

    constリファレンスの変数をうっかり書き換えてしまった場合、コンパイラーが検出してくれるので、バグを未然に発見することができる。

    単項演算子

    単項演算子はオペランドを一つしか取らない演算子のことだ。

    単項演算子についてはまだ説明していないものも多い。例えば、operator +operator -がある。

    - +

    単項演算子は引数を一つしか取らない関数として書く。

    - +

    インクリメント/デクリメント

    インクリメント演算子デクリメント演算子はやや変わっている。この演算子には、オペランドの前に書く前置演算子(++i)と、後に書く後置演算子(i++)がある。

    - +

    前置演算子を評価すると、演算子を評価した後の値になる。

    - -

    一方、後置演算子を評価すると、演算子を評価する前の値になる。

    -

    さらに前置演算子を評価した結果はリファレンスになるので代入やさらなる演算子の適用ができる。

    +

    一方、後置演算子を評価すると、演算子を評価する前の値になる。

    +i++ ; // 0 +i ; // 1 +

    さらに前置演算子を評価した結果はリファレンスになるので代入やさらなる演算子の適用ができる。

    +

    インクリメントとデクリメントの前置演算子は、単項演算子と同じ方法で書くことができる。

    - -

    引数を変更するのでconstではないリファレンスを使う。戻り値は引数をそのままリファレンスで返す。

    -

    もちろん、この実装はインクリメントとデクリメントの挙動を自然に再現したい場合の実装だ。以下のような挙動を実装することも可能だ。

    -
    struct S { } ;
    +
    -

    演算子のオーバーロードは演算子の文法で関数を呼べるという機能で、その呼び出した結果の関数が何をしようとも自由だからだ。

    -

    後置演算子は少し変わっている。以下が後置演算子の実装だ。

    - +

    引数を変更するのでconstではないリファレンスを使う。戻り値は引数をそのままリファレンスで返す。

    +

    もちろん、この実装はインクリメントとデクリメントの挙動を自然に再現したい場合の実装だ。以下のような挙動を実装することも可能だ。

    + -

    後置演算子は2つめの引数としてint型を取る。この引数はダミーで前置演算子と後置演算子を区別する以外の意味はない。意味はないので引数名は省略している。

    - +

    演算子のオーバーロードは演算子の文法で関数を呼べるという機能で、その呼び出した結果の関数が何をしようとも自由だからだ。

    +

    後置演算子は少し変わっている。以下が後置演算子の実装だ。

    + +IntLike operator ++( IntLike & obj, int ) +{ + auto temp = obj ; + ++obj.data ; + return temp ; +} +IntLike operator --( IntLike & obj, int ) +{ + auto temp = obj ; + --obj.data ; + return temp ; +}
    +

    後置演算子は2つめの引数としてint型を取る。この引数はダミーで前置演算子と後置演算子を区別する以外の意味はない。意味はないので引数名は省略している。

    +

    後置演算子はオペランドである引数を変更するが、戻り値は変更する前の値だ。なので変更前の値をまずコピーしておき、そのコピーを返す。

    メンバー関数での演算子のオーバーロード

    実は演算子のオーバーロードはメンバー関数で書くことも可能だ。

    例えば、

    - +

    を可能にするクラスSに対するoperator +は、

    - +

    でも実装できるが、メンバー関数としても実装できる。

    - -

    演算子のオーバーロードをメンバー関数で書く場合、最初のオペランドがメンバー関数の属するクラスのオブジェクト、2つめのオペランドが一つ目の引数になる。

    - +

    演算子のオーバーロードをメンバー関数で書く場合、最初のオペランドがメンバー関数の属するクラスのオブジェクト、2つめのオペランドが一つ目の引数になる。

    +

    この場合、メンバー関数は変数aに対して呼ばれ、変数bがrightとなる。

    普通のメンバー関数のように呼ぶこともできる。

    - +

    一見戸惑うかも知れないが、これは普通のメンバー関数呼び出しと何ら変わらない。

    - +

    演算子のオーバーロードはフリー関数とメンバー関数のどちらで実装すればいいのだろうか。答えはどちらでもよい。ただし、ごく一部の演算子はメンバー関数でしか実装できない。

    こうして、この章の冒頭にある演算子を使った自然な四則演算の記述が、自作のクラスでも可能になる。

    std::array

    std::vector<T>を覚えているだろうか。T型の値をいくつでも保持できるクラスだ。

    - -

    この章では、vectorと似ているクラス、std::array<T, N>を学ぶ。arrayはT型の値をN個保持するクラスだ。

    -

    その使い方は一見vectorと似ている。

    -

    vectorと違う点は、コンパイル時に要素数が固定されるということだ。

    -

    vectorは実行時に要素数を決めることができる。

    +

    この章では、vectorと似ているクラス、std::array<T, N>を学ぶ。arrayはT型の値をN個保持するクラスだ。

    +

    その使い方は一見vectorと似ている。

    -

    一方、arrayはコンパイル時に要素数を決める。標準入力から得た値は実行時のものなので、使うことはできない。

    + // 0番目の値を1に + a.at(0) = 1 ; + + // イテレーターを取る + auto i = std::begin(a) ; +} +

    vectorと違う点は、コンパイル時に要素数が固定されるということだ。

    +

    vectorは実行時に要素数を決めることができる。

    -

    vectorは実行時に要素数を変更することができる。メンバー関数push_backは要素数を1増やす。メンバー関数resize(sz)は要素数をszにする。

    +

    一方、arrayはコンパイル時に要素数を決める。標準入力から得た値は実行時のものなので、使うことはできない。

    -

    arraypush_backresizeも提供していない。

    -

    vectorarrayもメンバー関数at(i)でi番目の要素にアクセスできる。実は、i番目にアクセスする方法は他にもある。[i]を使う方法だ。

    + std::size_T N{} ; + std::cin >> N ; + + // エラー + std::array< int, N > a ; +} +

    vectorは実行時に要素数を変更することができる。メンバー関数push_backは要素数を1増やす。メンバー関数resize(sz)は要素数をszにする。

    -

    at(i)[i]の違いは、要素の範囲外にアクセスしたときの挙動だ。at(i)はエラー処理が行われる。[i]は何が起こるかわからない。

    + // 要素数5 + std::vector<int> v(5) ; + // 要素数6 + v.push_back(1) ; + // 要素数2 + v.resize(2) ; +} +

    arraypush_backresizeも提供していない。

    +

    vectorarrayもメンバー関数at(i)でi番目の要素にアクセスできる。実は、i番目にアクセスする方法は他にもある。[i]を使う方法だ。

    +

    at(i)[i]の違いは、要素の範囲外にアクセスしたときの挙動だ。at(i)はエラー処理が行われる。[i]は何が起こるかわからない。

    +

    この理由は、[i]は要素数が妥当な範囲かどうかを確認する処理を行っていないためだ。その分余計な処理が発生しないが、間違えたときに何が起こるかわからないという危険性がある。通常はat(i)を使うべきだ。

    実はこの[i]operator []というれっきとした演算子だ。演算子のオーバーロードもできる。例えば以下は任意個の要素を持ち、常にゼロを返すarrayのように振る舞う意味のないクラスだ。

    - +

    なぜvectorという実行時に要素数を設定でき実行時に要素数を変更できる便利なクラスがありながら、arrayのようなコンパイル時に要素数が決め打ちで要素数の変更もできないようなクラスもあるのだろうか。その理由はarrayvectorはパフォーマンスの特性が異なるからだ。vectorはストレージ(メモリー)の動的確保をしている。ストレージの動的確保は実行時の要素数を変更できるのだが、そのために予測不可能な非決定的なパフォーマンス特性を持つ。arrayはストレージの動的確保を行わない。この結果実行時に要素数を変更することはできないが、予測可能で決定的なパフォーマンス特性を持つ。

    その他のarrayの使い方は、vectorとほぼ同じだ。

    -

    さて、これから’array’を実装していこう。実装を通じて読者はC++のクラスとその他の機能を学んでいくことになる。

    +

    さて、これからarrayを実装していこう。実装を通じて読者はC++のクラスとその他の機能を学んでいくことになる。

    プログラマーの三大美徳

    プログラミング言語Perlの作者、Larry Wallは著書「プログラミング言語Perl」の初版で以下のように宣言した。

    @@ -8520,91 +8553,91 @@

    配列

    ナイーブなarray実装

    std::arrayを実装してみよう。すでにクラスを作る方法については学んだ。

    std::array<T,N>はT型の要素をN個保持するクラスだ。この<T,N>についてはまだ学んでいないので、今回はint型を3個確保する。今までに学んだ要素だけで実装してみよう。

    - -

    そしてoperator []を実装しよう。引数が0ならm0を、1ならm1を、2ならm2を返す。それ以外の値の場合、プログラムを強制的に終了させる標準ライブラリ、std::abortを呼び出す。

    + int m0 ; + int m1 ; + int m2 ; +} ; +

    そしてoperator []を実装しよう。引数が0ならm0を、1ならm1を、2ならm2を返す。それ以外の値の場合、プログラムを強制的に終了させる標準ライブラリ、std::abortを呼び出す。

    +

    これは動く。では要素数を10個に増やしたarray_int_10はどうなるだろうか。要素数100個はどう書くのだろうか。この方法で実装するとソースコードが膨大になり、ソースコードを出力するソースコードを書かなければならなくなる。これは怠惰で短気なプログラマーには耐えられない作業だ。

    配列

    std::arrayを実装するには、配列(array)を使う。

    int型の要素数10の配列aは以下のように書く。

    - +

    double型の要素数5の配列bは以下のように書く。

    - +

    配列の要素数はstd::array<T,N>のNと同じようにコンパイル時定数でなければならない。

    - +

    配列は={1,2,3}のように初期化できる。

    - +

    配列の要素にアクセスするにはoperator []を使う

    - +

    配列にはメンバー関数はない。at(i)size()のような便利なメンバー関数はない。

    配列のサイズはsizeofで取得できる。配列のサイズは配列の要素の型のサイズかけることの要素数のサイズになる。

    - +

    sizeofは型やオブジェクトのバイト数を取得するのに対し、vectorarrayのメンバー関数size()は要素数を取得する。この違いに注意すること。

    - +

    配列はとても低級な機能だ。その実装はある型を連続してストレージ上に並べたものになっている。

    - +

    のような配列があり、int型が4バイトの環境では、20バイトのストレージが確保され、その先頭の4バイトが最初の0番目の要素に、その次の4バイトが1番目の要素になる。最後の4番目の要素は最後の4バイトになる。

    配列のストレージ上のイメージ図
     
    @@ -8615,241 +8648,225 @@ 

    配列

    |--| |--| 0番目のint 4番目のint

    配列にはメンバー関数がない上、コピーもできない。std::arrayはコピーできる。

    - +

    配列は低級で使いにくいので、std::arrayという配列をラップした高級なライブラリが標準で用意されている。

    さて、配列の使い方は覚えたので、さっそくstd::array_int_10を実装してみよう。

    まずクラスのデータメンバーとして配列を宣言する。

    - +

    配列はコピーできないが、クラスのデータメンバーとして宣言した配列は、クラスのコピーの際に、その対応する順番の要素がそれぞれコピーされる。

    - +

    これはあたかも以下のように書いたかのように動く。

    - -

    operator []も実装しよう。

    - +

    operator []も実装しよう。

    +

    std::arrayにはまだ様々なメンバーがある。一つづつ順番に学んでいこう。

    テンプレート

    問題点

    前回、我々は’std::array’のようなものを実装した。C++を何も知らなかった我々がとうとうクールなキッズは皆やっているというクラスを書くことができた。素晴らしい成果だ。

    しかし、我々の書いた’array_int_10’は’std::array’とは異なる。

    - +

    もし要素数を20個にしたければarray_int_20を新たに書かなければならない。するとarray_int_1とかarray_int_10000のようなクラスを無数に書かなければならないのだろうか。要素の型をdoubleにしたければarray_double_10が必要だ。

    しかし、そのようなクラスはほとんど同じような退屈な記述の羅列になる。

    - +

    これは怠惰で短気なプログラマーには耐えられない作業だ。C++にはこのような退屈なコードを書かなくてもすむ機能がある。しかしその前に、引数について考えてみよう。

    関数の引数

    1を2倍する関数を考えよう。

    - -

    上出来だ。では2を2倍する関数を考えよう。

    -
    int two_twice()
    +
    -

    すばらしい。では3を2倍する関数、4を2倍する関数…と考えていこう。

    -

    ここまで読んでthree_twiceやfour_twiceを思い浮かべた読者にはプログラマーに備わるべき美徳が欠けている。怠惰で短気で傲慢なプログラマーはそんなコードを書かない。引数を使う。

    -
    int twice( int n )
    +

    上出来だ。では2を2倍する関数を考えよう。

    + +

    すばらしい。では3を2倍する関数、4を2倍する関数…と考えていこう。

    +

    ここまで読んでthree_twiceやfour_twiceを思い浮かべた読者にはプログラマーに備わるべき美徳が欠けている。怠惰で短気で傲慢なプログラマーはそんなコードを書かない。引数を使う。

    +

    具体的な値を2倍する関数を値の数だけ書くのは面倒だ。具体的な値は定めず、引数で外部から受け取る。そして引数を2倍して返す。引数は汎用的なコードを任意の値に対して対応させるための機能だ。

    関数のテンプレート引数

    twiceを様々な型に対応させるにはどうすればいいだろう。例えばint型とdouble型に対応させてみよう。

    - -

    整数型にはintの他にも、short, long, long longといった型がある。浮動小数点数型にはfloatとlong doubleもある。ということは以下のような関数も必要だ。

    -
    short twice( short n )
    +
    -

    ところで、整数型には符号付きと符号なしの2種類があるということは覚えているだろうか?

    - +

    整数型にはintの他にも、short, long, long longといった型がある。浮動小数点数型にはfloatとlong doubleもある。ということは以下のような関数も必要だ。

    + +long long twice( long long n ) +{ + return n * 2 ; +} + +float twice( float n ) +{ + return n * 2 ; +} + +long double twice( long double n ) +{ + return n * 2 ; +}
    +

    ところで、整数型には符号付きと符号なしの2種類があるということは覚えているだろうか?

    +

    C++ではユーザーが整数型のように振る舞うクラスを作ることができる。整数型を複数使って巨大な整数を表現できるクラスも作ることができる。

    - +

    このクラスに対応するには当然、以下のように書かなければならない。

    - -

    そろそろ怠惰と短気を美徳とするプログラマー読者は耐えられなくなってきただろう。これまでのコードは、単にある型Tに対して、

    -
    T twice( T n )
    +
    +

    そろそろ怠惰と短気を美徳とするプログラマー読者は耐えられなくなってきただろう。これまでのコードは、単にある型Tに対して、

    +

    と書いているだけだ。型Tがコピーとoperator *(T, int)に対応していればいい。型Tの具体的な型について知る必要はない。

    関数が具体的な値を知らなくても引数によって汎用的なコードをかけるように、具体的な型を知らなくても汎用的なコードを書けるようになりたい。その怠惰と短気に答えるのがテンプレートだ。

    テンプレート

    通常の関数が値を引数に取ることができるように、テンプレートは型を引数に取ることができる。

    テンプレートは以下のように宣言する

    - -

    テンプレートを関数に使う関数テンプレートは以下のように書く。

    -

    template < typename T >は型Tテンプレート引数に取る。テンプレートを使った宣言の中では、Tが型として扱える。

    + 宣言
    +

    テンプレートを関数に使う関数テンプレートは以下のように書く。

    + return n * 2 ; +} + +int main() +{ + twice( 123 ) ; // int + twice( 1.23 ) ; // double +}
    +

    template < typename T >は型Tテンプレート引数に取る。テンプレートを使った宣言の中では、Tが型として扱える。

    +

    関数引数をとるように、テンプレートテンプレート引数を取る。

    - +

    テンプレートが「使われる」ときに、テンプレート引数に対する具体的な型が決定する。

    - -

    テンプレートを使うときに自動でテンプレート引数を推定してくれるが、<T>を使うことで明示的にテンプレート引数をT型に指定することもできる。

    -

    テンプレート引数は型ではなく整数型の値を渡すこともできる。

    -
    template < int N >
    -void f()
    +

    テンプレートを使うときに自動でテンプレート引数を推定してくれるが、<T>を使うことで明示的にテンプレート引数をT型に指定することもできる。

    + -

    ただし、テンプレート引数はコンパイル時にすべてが決定される。なのでテンプレート引数に渡せる値はコンパイル時に決定できるものでなければならない。

    + // Tはint + f<int>(0) ; + + // Tはdouble + // int型0からdouble型0.0への変換が行われる + f<double>( 0 ) ; +}
    +

    テンプレート引数は型ではなく整数型の値を渡すこともできる。

    -

    テンプレート引数がコンパイル時に決定されるということは、配列のサイズのようなコンパイル時に決定されなければならない場面でも使えるということだ。

    -
    template < std::size_t N >
    -void f()
    -{
    -    int buffer[N] ;
    -}
    -
    -int main()
    -{
    -    // 配列bufferのサイズは10
    -    f<10>() ;
    -    // サイズは12
    -    f<12>() ;
    +

    ただし、テンプレート引数はコンパイル時にすべてが決定される。なのでテンプレート引数に渡せる値はコンパイル時に決定できるものでなければならない。

    + -

    テンプレートを使ったコードは、与えられたテンプレート引数に対して妥当でなければならない。

    - +

    テンプレートを使ったコードは、与えられたテンプレート引数に対して妥当でなければならない。

    +

    クラステンプレート

    テンプレートクラスにも使える。関数テンプレート関数の前にテンプレートを書くように、

    - -

    クラステンプレートクラスの前にテンプレートを書く。

    +void f( ) ; // 関数
    +

    クラステンプレートクラスの前にテンプレートを書く。

    +

    関数の中でテンプレート引数名を型や値として使えるように。

    - -

    クラスの中でもテンプレート引数名を型や値として使える。

    -
    template < typename T, std::size_t N >
    -struct array
    +
    -

    なんと、もう’std::array’が完成してしまった。

    -

    arrayをさらに実装

    -

    ’std::array’をもっと実装していこう。前回、以下のような簡単な’array’を実装した。

    - +

    クラスの中でもテンプレート引数名を型や値として使える。

    + +

    なんと、もう’std::array’が完成してしまった。

    +

    arrayをさらに実装

    +

    ’std::array’をもっと実装していこう。前回、以下のような簡単な’array’を実装した。

    +

    実はstd::arrayはこのように書かれていない。この章では、’array’の実装を’std::array’に近づけていく。

    ネストされた型名

    エイリアス宣言を覚えているだろうか。型名に別名をつける機能だ。

    - -

    エイリアス宣言はクラスの中でも使うことができる。

    -
    struct S
    +
    -

    クラスの中で宣言されたエイリアス宣言による型名を、’ネストされた型名’という。std::arrayではテンプレート引数を直接使う代わりに、ネストされた型名が使われている。

    - +

    エイリアス宣言はクラスの中でも使うことができる。

    + + S::number x = s.data ; +}
    +

    クラスの中で宣言されたエイリアス宣言による型名を、ネストされた型名という。std::arrayではテンプレート引数を直接使う代わりに、ネストされた型名が使われている。

    +

    こうすると、T &のようなわかりにくい型ではなくreferenceのようにわかりやすい名前を使える。さらに、クラス外部から使うこともできる。

    - -

    もちろんこれはautoで書くこともできるが、

    +

    もちろんこれはautoで書くこともできるが、

    +

    信じられないことに昔のC++にはautoがなかったのだ。その他、様々な利点があるのだが、そのすべてを理解するには、まだ読者のC++力が足りない。

    要素数の取得: size()

    std::array<T,N>にはsize()というメンバー関数がある。要素数をかえす。

    arrayの場合、Nを返せばよい。

    - -

    早速実装しよう。

    - +

    早速実装しよう。

    +

    ここではsizeの宣言だけをしている。

    関数は宣言と定義が分割できる。

    - +

    メンバー関数も宣言と定義が分割できる。

    - +

    メンバー関数の定義をクラス宣言の外で書くには、関数名がどのクラスに属するのかを指定しなければならない。これにはクラス名::を使う。この場合、S::fだ。

    メンバー関数のconst修飾

    constをつけた変数は値を変更できなくなることはすでに学んだ。

    - +

    constは変更する必要のない場面でうっかり変更することを防いでくれるとても便利な機能だ。’array’は大きいので関数の引数として渡すときにコピーするのは非効率的だ。なのでコピーを防ぐリファレンスで渡したい。

    std::array<T,N>を受け取って要素をすべて出力する関数を書いてみよう。

    - -

    関数printがテンプレートなのは任意のTNを使ったstd::array<T,N>を受け取れるようにするためだ。

    -

    関数のリファレンスを引数として渡すと、関数の中で変更できてしまう。しかし、上の例のような関数printでは、引数を書き換える必要はない。この関数を使う人間も、引数を勝手に書き換えないことを期待している。この場合、constをつけることで値の変更を防ぐことができる。

    - +

    関数printがテンプレートなのは任意のTNを使ったstd::array<T,N>を受け取れるようにするためだ。

    +

    関数のリファレンスを引数として渡すと、関数の中で変更できてしまう。しかし、上の例のような関数printでは、引数を書き換える必要はない。この関数を使う人間も、引数を勝手に書き換えないことを期待している。この場合、constをつけることで値の変更を防ぐことができる。

    +

    ではさっそくこれまで実装してきた自作のarrayクラスを使ってみよう。

    - +

    なぜかエラーになってしまう。

    この理由はメンバー関数を呼び出しているからだ。

    クラスのメンバー関数はデータメンバーを変更できる。

    - -

    ということは、const Sはメンバー関数f()を呼び出すことができない。

    -
    int main()
    +
    -

    ではメンバー関数f()がデータメンバーを変更しなければいいのだろうか。試してみよう。

    - +

    ということは、const Sはメンバー関数f()を呼び出すことができない。

    + + S s ; + S const & ref = s ; + + ++ref.data ; // エラー + ref.f() ; // エラー +}
    +

    ではメンバー関数f()がデータメンバーを変更しなければいいのだろうか。試してみよう。

    +

    まだエラーになる。この理由を完全に理解するためには、まだ説明していないポインターという機能について学ばなければならない。ポインターの説明はこの次の章で行うとして、今はさしあたり必要な機能であるメンバー関数のconst修飾を説明する。

    constをつけていないメンバー関数をconstなクラスのオブジェクトから呼び出せない理由は、メンバー関数がデータメンバーを変更しない保証がないからだ。その保証をつけるのがメンバー関数のconst修飾だ。

    メンバー関数は関数の引数のあと、関数の本体の前にconstを書くことでconst修飾できる。

    - -

    const修飾されたメンバー関数はconstなクラスのオブジェクトからでも呼び出すことができる。

    -

    const修飾されたメンバー関数と、const修飾されていないメンバー関数が両方ある場合、クラスのオブジェクトのconstの有無によって適切なメンバー関数が呼び出される。

    -

    そしてもう一つ重要なのは、const修飾されたメンバー関数がデータメンバーへのリファレンスを返す場合、

    + cs.f() ; // OK + +}
    +

    const修飾されたメンバー関数はconstなクラスのオブジェクトからでも呼び出すことができる。

    +

    const修飾されたメンバー関数と、const修飾されていないメンバー関数が両方ある場合、クラスのオブジェクトのconstの有無によって適切なメンバー関数が呼び出される。

    -

    const修飾されたメンバー関数は自分のデータメンバーを変更できないので、データメンバーの値を変更可能なリファレンスを返すことはできない。そのため以下のようになる。

    + void f() { } // 1 + void f() const { } // 2 +} ; + +int main() +{ + S s ; + s.f() ; // 1 + + S const cs ; + cs.f() ; // 2 +}
    +

    そしてもう一つ重要なのは、const修飾されたメンバー関数がデータメンバーへのリファレンスを返す場合、

    -

    自作の’array’のopeartor []をconstに対応させよう。’std::array’はconstなリファレンスをconst_referenceというネストされた型名にしている。

    - +

    const修飾されたメンバー関数は自分のデータメンバーを変更できないので、データメンバーの値を変更可能なリファレンスを返すことはできない。そのため以下のようになる。

    + + // const版 + // constリファレンスを返すので変更不可 + int const & get() const + { + return data ; + } +} ; +

    自作の’array’のoperator []をconstに対応させよう。’std::array’はconstなリファレンスをconst_referenceというネストされた型名にしている。

    +

    これでconst arrayにも対応できるようになった。

    先頭と末尾の要素:front/back

    メンバー関数frontは最初の要素へのリファレンスを返す。backは最後の要素へのリファレンスを返す。

    - +

    front/backにはreferenceを返すバージョンとconst_referenceを返すバージョンがある。

    - +

    全要素に値を代入: fill

    - +

    すでにアルゴリズムで実装した’std::fill’と同じだ。

    - +

    しかし、せっかくstd::fillがあるのだから以下のように書きたい。

    - +

    残念ながらこれは動かない。なぜならば、自作のarrayはまだbegin()/end()イテレーターに対応していないからだ。これは次の章で学ぶ。

    arrayのイテレーター

    イテレーターの中身

    自作のarrayをイテレーターに対応させる前に、まず’std::array’のイテレーターについて一通り調べよう。

    イテレーターはstd::begin/std::endで取得する

    - -

    std::begin/std::endは何をしているのか見てみよう。

    - +

    std::begin/std::endは何をしているのか見てみよう。

    +

    なんと、単に引数に対してメンバー関数begin/endを呼び出してその結果を返しているだけだ。

    早速確かめてみよう。

    - +

    確かに動くようだ。

    すると自作のarrayでイテレーターに対応する方法がわかってきた。

    - +

    イテレーターに対応するには、おおむねこのような実装になるとみていいだろう。おそらく細かい部分で微調整が必要になるが、今はこれでよしとしよう。ではイテレーターが具体的に何をするかを見ていこう。

    すでに学んだように、イテレーターはoperator *で参照する要素の値を取得できる。また書き込みもできる。

    - +

    問題を簡単にするために、これまでに作った自作のarrayで最初の要素にアクセスする方法を考えてみよう

    - +

    このことから考えると、先頭要素を指すイテレーターはoperator *をオーバーロードして先頭要素をリファレンスで返せば良い。

    - +

    しかし、この実装ではarray<int,5>にしか対応できない。array<int,7>array<double, 10>には対応できない。なぜなら、arrayに渡すテンプレート実引数が違うと、別の型になるからだ。

    array_iteratorで様々なarrayを扱うにはどうすればいいのか。テンプレートを使う。

    - +

    しかしなぜかエラーだとコンパイラーに怒られる。この理由を説明するのはとても難しい。気になる読者は近所のC++グルに教えを乞おう。ここでは答えだけを教える。

    T::Yにおいて、Tがテンプレート引数に依存する名前で、Yがネストされた型名の場合、typenameキーワードをつけなければならない。

    - +

    わかっただろうか。わからなくても無理はない。この問題を理解するにはテンプレートに対する深い理解が必要だ。理解した暁には読者はC++グルとして崇拝されているだろう。

    さしあたって必要なのはArray::referenceの前にtypenameキーワードをつけることだ。

    - +

    どうやら最初の要素を読み書きするイテレーターはできたようだ。array側も実装して試してみよう。

    array側の実装にはまだ現時点では完全に理解できない黒魔術が必要だ。

    template < typename T, std::size_t N >
    @@ -9430,483 +9463,483 @@ 

    イテレーターの中身

    { return iterator(*this) ; } }

    黒魔術1はarray_iterator_begin<array>の中にある。このarrayarray<T,N>と同じ意味になる。つまり全体としては、array_iterator_begin<array<T,N>>と書いたものと同じだ。クラステンプレートの中でクラス名を使うと、テンプレート実引数をそれぞれ指定したものと同じになる。

    - +

    黒魔術2は*thisだ。*thisはメンバー関数を呼んだクラスのオブジェクトへのリファレンスだ。

    - -

    クラスのメンバー関数は対応するクラスのオブジェクトに対して呼ばれる。本来ならばクラスのオブジェクトをリファレンスで取るような形になる。

    -

    というコードは、ほぼ同じことを以下のようにも書ける。

    + // *thisはメンバー関数が呼ばれたSのオブジェクト + S & THIS() { return *this ; } +} ; + +int main() +{ + S s1 ; + + s1.THIS().data = 123 ; + // 123 + std::cout << s1.data ; + + S s2 ; + s2.THIS().data = 456 ; + // 456 + std::cout << s2.data ; +} +

    クラスのメンバー関数は対応するクラスのオブジェクトに対して呼ばれる。本来ならばクラスのオブジェクトをリファレンスで取るような形になる。

    + void set(int x) + { + data = x ; + } +} ; + +int main() +{ + S object ; + object.set(42) ; +} +

    というコードは、ほぼ同じことを以下のようにも書ける。

    +

    クラスの意義は変数と関数を結びつけることだ。このように変数と関数がバラバラではわかりにくいので、メンバー関数という形でobject.set(...)のようにわかりやすく呼び出せるし、その際クラスSのオブジェクトは変数objectであることが文法上わかるので、わざわざ関数の実引数の形で書くことは省略できるようにしている。

    メンバー関数の中で、メンバー関数が呼ばれているクラスのオブジェクトを参照する方法が*thisだ。

    しかしなぜ*thisなのか。もっとわかりやすいキーワードでもいいのではないか。なぜ*がついているのか。この謎を理解するためには、これまたポインターの理解が必要になるが、それは次の章で学ぶ。

    黒魔術3はiterator(*this)だ。クラス名に(){}を続けると、コンストラクターを呼び出した結果のクラスの値を得ることができる。

    - +

    黒魔術の解説が長くなった。本題に戻ろう。

    array_iterator_beginは先頭の要素しか扱えない。イテレーターで先頭以外の別の要素を扱う方法を思い出してみよう。

    イテレーターはoperator ++で次の要素を参照する。operator --で前の要素を参照する。

    - +

    このoperator ++operator --はイテレーターへのリファレンスを返す。なぜならば、以下のように書けるからだ。

    - +

    以上を踏まえて、自作のarray_iteratorの宣言を書いてみよう。

    - +

    イテレーターの実装で先頭の要素を参照するのはa[0]だった。その次の要素を参照するにはa[1]だ。その次の要素はa[2]となり、その前の要素はa[1]だ。

    - +

    では最初の要素の前の要素や、最後の要素の次の要素を参照しようとするとどうなるのか。

    - +

    これはエラーになる。このようなエラーを起こさないように務めるのはユーザーの責任で、イテレーター実装者の責任ではない。しかし、必要であればイテレーターの実装者はこのようなエラーを防ぐような実装もできる。それは後の章で学ぶ。ここでは、こういう場合が起こることは考えなくてもよいとしよう。

    これを考えていくと、イテレーターの実装をどうすればいいのかがわかってくる。

    array_iteratorのoperator *a[i]を返す。

    - +

    istd::size_t型のデータメンバーで、イテレーターが現在参照しているi番目の要素を記録している。

    ということは先程のarray_iteratorの宣言にはデータメンバーiを追加する修正が必要だ。

    - -

    そして、array側にも新しいarray_iteratorへの対応が必要になる。

    -
    template < typename T, std::size_t N >
    -struct array
    +
    +

    そして、array側にも新しいarray_iteratorへの対応が必要になる。

    +

    何度も書くように、インデックスは0から始まる。要素がN個ある場合、最初の要素は0番目で、最後の要素はN-1番目だ。

    -

    インクリメント演算子opeartor ++にも対応しよう。

    - -

    これで最低限のイテレーターは実装できた。早速試してみよう。

    -
    int main()
    +

    インクリメント演算子operator ++にも対応しよう。

    + -

    実はoperator ++は2種類ある。前置演算子と後置演算子だ。

    + ++i ; + return *this ; +}
    +

    これで最低限のイテレーターは実装できた。早速試してみよう。

    +

    実はoperator ++は2種類ある。前置演算子と後置演算子だ。

    +

    int型では、前置operator ++はオペランドの値を1加算した値にする。後置operator ++はオペランドの値を1加算するが、式を評価した結果は前のオペランドの値になる。

    - +

    後置operator ++のオーバーロードは以下のように書く。

    - +

    このコードは慣れないとわかりにくいが、妥当な理由のあるコードだ。順番に説明しよう。

    まず演算子オーバーロードの宣言だ。

    - +

    前置はリファレンスを返す。前置演算子の適用結果は更に変更できるようにするためだ。

    - -

    もちろん、リファレンスを返さない実装は可能だ。そもそも何も値を返さないvoidを使うことも可能だ。

    -
    struct S
    +
    -

    ただし、その場合operator ++に対して通常期待されるコードが書けなくなる。理由がない限り演算子の自然な挙動を目指すべきだ。

    -

    前置と後置は区別できる必要がある。C++はその区別の方法として、int型の仮引数をひとつとるoperator ++を後置演算子だと認識する文法を採用した。このint型の実引数は前置と後置を区別するためだけのもので、値に意味はない。

    + int i { } ; + + ++++i ; +}
    +

    もちろん、リファレンスを返さない実装は可能だ。そもそも何も値を返さないvoidを使うことも可能だ。

    + void operator ++() { } +} ;
    +

    ただし、その場合operator ++に対して通常期待されるコードが書けなくなる。理由がない限り演算子の自然な挙動を目指すべきだ。

    +

    前置と後置は区別できる必要がある。C++はその区別の方法として、int型の仮引数をひとつとるoperator ++を後置演算子だと認識する文法を採用した。このint型の実引数は前置と後置を区別するためだけのもので、値に意味はない。

    +

    値に意味はないが、演算子として使用した場合、値は0になるというどうでもいい仕様がある。メンバー関数として使用すると好きな値を渡せるというこれまたどうでもいい仕様がある。テストには出ないので覚える必要はない。

    前置は自然な挙動のためにリファレンスを返すが、後置はリファレンスではなくコピーした値を返す。

    - +

    このように実装すると、後置として自然な挙動が実装できる。

    ++*thisは後置インクリメント演算子が呼ばれたオブジェクトに対して前置インクリメント演算子を使用している。わかりにくければ前置インクリメントと同じ処理を書いてもいい。

    - +

    IntLikeのように簡単な処理であればこれでもいいが、もっと複雑な何行もある処理の場合は、すでに実装した前置インクリメントを呼び出したほうが楽だ。コードの重複を省けるのでインクリメントの処理を変更するときに、二箇所に同じ変更をしなくても済む。

    以上を踏まえて、array_iteratorに後置インクリメント演算子を実装しよう。

    - +

    デクリメント演算子operator --の実装はインクリメント演算子operator ++と同じだ。ただ処理がインクリメントではなくデクリメントになっているだけだ。

    - +

    ここまでくればイテレーターに必要な操作はあと一つ。比較だ。

    イテレーターは同じ要素を指している場合に等しい。つまり、オペレーターabが同じ要素を指しているならば、a == btruea != bfalseだ。違う要素を指しているならばa == bfalsea != btrueだ。

    - -

    イテレーターは比較ができるので、イテレーターが終端に到達するまでループを回すことができる。

    + auto a = a.begin() ; + auto b = a.begin() ; + + // true + bool b1 = (a == b) ; + // false + bool b2 = (a != b) ; + ++a ; + // false + bool b3 = (a == b) ; + // true + bool b4 = (a != b) ; +} +

    イテレーターは比較ができるので、イテレーターが終端に到達するまでループを回すことができる。

    +

    イテレーターは比較ができるので、各種アルゴリズムに渡すことができる。

    array_iteratorの比較は、単にデータメンバーiの比較でよい。

    - -

    これで自作のarrayarray_iteratorはアルゴリズムに渡せるようになった。

    - +

    これで自作のarrayarray_iteratorはアルゴリズムに渡せるようになった。

    +

    残りのイテレーターの実装

    std::arraystd::vectorのイテレーターはとても柔軟にできている。

    例えばイテレーターiの参照する要素を3つ進めたい場合を考えよう。

    - +

    これは非効率的だ。もっと効率的なイテレーターの進め方として、operator +=がある。

    - +

    i += nはイテレーターiをn回進める。

    operator +もある

    - +

    イテレーターjの値はイテレーターiを3つ進めた値になる。イテレーターiの値は変わらない。

    実装は簡単だ。データメンバーiに対して同じ計算をする。

    - +

    operator +はオペランドの値を変更しないのでconstにできる。

    同様に、operator -=とoperator -もある。上を参考に自分で実装してみよう。

    operator +によって任意のn個先の要素を使うことができるようになったので、イテレーターiのn個先の要素を参照したければ、以下のように*(i+n)も書ける。

    - +

    カッコが必要なのは、演算子の評価順序の都合だ。*i + 3(*i) + 3であり、iの指す要素に対して+3される。*(i+3)iの指す要素の3つ先の要素の値を読む。

    イテレーターiのn個先の要素を読み書きするのにいちいち*(i+n)と書くのは面倒なので、std::arraystd::vectorのイテレーターにはoperator []がある。これを使うとi[n]と書ける。

    - -

    operator []の実装は文字通り*(i+n)と同じことをするだけでよい。

    - +

    operator []の実装は文字通り*(i+n)と同じことをするだけでよい。

    +

    このoperator []は、array_iteratorのデータメンバーを変更しないのでconst修飾できる。

    *thisというのはこのイテレーターのオブジェクトなので、それに対してすでに実装済みのoperator +を適用し、その結果にoperator *を適用している。既存の実装を使わない場合、return文は以下のようになる。

    - +

    こちらのほうが一見簡単なように見えるが、operator +operator *の実装が複雑な場合、この方法では同じコードを複数の箇所に書かなければならず、コードを修正するときは同じ変更を複数の箇所に行わなければならない。すでに実装したメンバー関数は積極的に使って楽をしていこう。

    イテレーターは大小比較ができる。

    - +

    イテレーターの大小はどういう意味を持つのか。arrayのようにイテレーターが線形に順序のある要素を参照している場合で、前の要素を参照しているイテレーターはあとの要素を参照しているイテレーターより小さい。

    - +

    自作のarrayの場合、単にデータメンバーiを比較する。

    - +

    残りの演算子も同様に実装できる。

    constなイテレーター: const_iterator

    std::array<T,N>は通常のイテレーターであるstd::array<T,N>::iteratorの他に、constなイテレーターであるstd::array<T,N>::const_iteratorを提供している。

    - +

    const_iteratorconst iteratorではない。const_iteratorとはそれ自体が型名だ。constというのは型名を修飾する別の機能だ。

    そのため、constの有無の2種類の状態と、iterator, const_iteratorの2つの型をかけ合わせた、以下の型が存在する。

      @@ -9915,245 +9948,245 @@

      constなイテレーター: c
    • const_iterator
    • const const_iterator
    - +

    const_iteratoriteratorとは別の型だ。自作のarrayに実装するならば以下のようになる。

    - +

    それぞれの型に対して、constキーワードをつけた型とそうでない型が存在する。

    const_iteratorを得る方法はいくつかある。

    • constなarrayのbegin/endを呼び出す
    - -
      -
    • cbegin/cendを呼び出す
    • -
    + // constなarray + const std::array<int, 5> a = {1,2,3,4,5} ; + + // const_iterator + auto i = a.begin() ; +}
      -
    • iteratorからconst_iteratorへの変換
    • +
    • cbegin/cendを呼び出す
    -

    constキーワードはすでに学んだように、オブジェクトの値を変更できないようにする機能だ。

    -

    なぜconst_iteratorが存在するのか。const iteratorではだめなのか。その理由は、const iteratorは値の変更ができないためだ。

    + std::array<int, 5> a = {1,2,3,4,5} ; + + // const_iterator + auto i = a.cbegin() ; +} +
      +
    • iteratorからconst_iteratorへの変換
    • +
    -

    const_iteratorならばイテレーター自体の変更はできる。イテレーターが参照する要素の変更はできない。

    + // iterator + Array::iterator i = a.begin() ; + // iteratorからconst_iteratorへの変換 + Array::const_iterator j = i ; +} +

    constキーワードはすでに学んだように、オブジェクトの値を変更できないようにする機能だ。

    +

    なぜconst_iteratorが存在するのか。const iteratorではだめなのか。その理由は、const iteratorは値の変更ができないためだ。

    -

    const const_iteratorconst_iteratorconstだ。const const_iteratorconst iteratorと同じく、イテレーター自体の変更ができない。

    + // const iterator + const Array::iterator iter = a.begin() ; + + // エラー + // constなオブジェクトは変更できない + ++iter ; + + // Ok + // iterは変更していない + auto next_iter = iter + 1 ; +} +

    const_iteratorならばイテレーター自体の変更はできる。イテレーターが参照する要素の変更はできない。

    + auto citer = a.begin() ; + + // OK + // イテレーター自体の変更 + ++citer ; + + // OK + // 要素を変更しない + std::cout << *citer ; + + // エラー + // 要素を変更してる + *citer = 0 ; +} +

    const const_iteratorconst_iteratorconstだ。const const_iteratorconst iteratorと同じく、イテレーター自体の変更ができない。

    +

    auto constもしくはconst autoを使うと、変数の型を自動で推定してくれるが、constがつくようになる。

    const_iteratorはどう実装するのか。まずarrayにネストされた型名const_iteratorを追加する。

    - -

    arrayconst_iteratorを返すcbegin/cendと、const arrayのときにconst_iteratorを返すbegin/endを追加する。

    -

    あとはarray_const_iterator<array>を実装する。その実装はarray_iterator<array>とほぼ同じだ。

    - +

    arrayconst_iteratorを返すcbegin/cendと、const arrayのときにconst_iteratorを返すbegin/endを追加する。

    + + // const arrayのときにconst_iteratorを返す + const_iterator begin() const + { return const_iterator(*this, 0) ; } + const_iterator end() const + { return const_iterator(*this, N-1) ; } + + // 常にconst_iteratorを返す + const_iterator cbegin() const + { return const_iterator(*this, 0) ; } + const_iterator cend() const + { return const_iterator(*this, N-1) ; } + + // その他のメンバー +} ; +

    あとはarray_const_iterator<array>を実装する。その実装はarray_iterator<array>とほぼ同じだ。

    +

    ただし、const_iteratoriteratorから変換できるので、

    - +

    これに対応するために、const_iteratorのコンストラクターはiteratorから変換するためのコンストラクターも持つ。

    - +

    残りのメンバー関数はiteratorとほぼ同じだ。

    例えばoperator ++は完全に同じだ。

    - +

    operator *operator []はconstなリファレンスを返す。

    - +

    このために、arrayクラスにもネストされた型名const_referenceを宣言しておく。

    - +

    残りはiteratorの実装を参考に読者が自分で実装してみよう。

    傲慢なエラー処理: 例外

    例外を投げる

    std::arrayの実装方法はほとんど解説した。読者はstd::arrayの実装方法を知り、確固たる自信のもとにstd::arrayを使えるようになった。ただし、ひとつだけ問題がある。

    “std::array”のユーザーはあらかじめ設定した要素数を超える範囲の要素にアクセスすることができてしまう。

    - +

    arrayを自力で実装できる傲慢な読者としては、ユーザーごときが間違った使い方をできるのが許せない。間違いを起こした時点でエラーを発生させ、問題を知らしめ、対処できるようにしたい。

    operator []に範囲外チェックを入れるのは簡単だ。問題は、エラーをユーザーに通知する方法がない。

    - +

    operator []は伝統的にエラーチェックをしない要素アクセスをするものだ。

    vectorで一番最初に説明した要素アクセスの方法であるメンバー関数atを覚えているだろうか。実はメンバー関数atはエラーチェックをする。試してみよう。

    - +

    以下が実行結果だ。

    terminate called after throwing an instance of 'std::out_of_range'
       what():  array::at: __n (which is 1000) >= _Nm (which is 1)
    @@ -10164,277 +10197,277 @@

    例外を投げる

    このエラー処理は、「例外」を使って行われる。

    例外は通常の処理をすっ飛ばして特別なエラー処理をする機能だ。何もエラー処理をしない場合、プログラムは終了する。例外を発生させることを、「例外を投げる」という。

    例外は文字通り投げるという意味のthrowキーワードを使い、何らかの値を投げる(throw)。

    - +

    この例では、int型、double型、std::array<int,5>型の値を投げている。

    一度例外が投げられると、通常の実行はすっ飛ばされる。

    以下は0を入力すると例外を投げるプログラムだ。

    - -

    このプログラムを実行すると、非0を入力した場合、“Success!”が出力される。0を入力した場合、例外が投げられる。例外が投げられると、通常の実行はすっ飛ばされる。エラー処理はしていないので、プログラムは終了する。

    -

    std::arraystd::vectorのメンバー関数at(n)nが要素数を超える場合、例外を投げている。

    - +

    このプログラムを実行すると、非0を入力した場合、“Success!”が出力される。0を入力した場合、例外が投げられる。例外が投げられると、通常の実行はすっ飛ばされる。エラー処理はしていないので、プログラムは終了する。

    +

    std::arraystd::vectorのメンバー関数at(n)nが要素数を超える場合、例外を投げている。

    +

    投げる例外は、std::out_of_rangeというクラスの値だ。このクラスを完全に説明するのは現時点では難しいが、以下のように振る舞うと考えておこう。

    - +

    とりあえず使ってみよう。

    - -

    コンストラクターでエラー内容を表現した文字列を受け取り、メンバー関数whatでエラー内容の文字列を取得する。

    -

    必要な情報は全て学んだ。あとはメンバー関数atを実装するだけだ。

    -
    array::reference array::at( std::size_t n )
    +
    +

    コンストラクターでエラー内容を表現した文字列を受け取り、メンバー関数whatでエラー内容の文字列を取得する。

    +

    必要な情報は全て学んだ。あとはメンバー関数atを実装するだけだ。

    +

    例外を捕まえる

    現状では、エラーを発見して例外を投げたら即座にプログラムが終了してしまう。投げた例外を途中で捕まえて、プログラムを通常の実行に戻す機能がほしい。その機能が「例外のキャッチ」だ。

    例外のキャッチにはtryキーワードとcatchキーワードを使う。

    - +

    try {}ブロックの中で投げられた例外は、catchで型が一致する場合にキャッチされる。例外がキャッチされた場合、catchのブロックが実行される。そして実行が再開される。

    - -

    catchの型と投げられた例外の型が一致しない場合は、キャッチしない。

    -

    catchは複数書くことができる。

    + + try { + throw 123 ; // int型 + } + // キャッチする + catch( int e ) + { + std::cout << e ; + } + + // 実行される + std::cout << "resumed.\n"s ; +}
    +

    catchの型と投げられた例外の型が一致しない場合は、キャッチしない。

    -

    tryブロックの中で投げられた例外は、たとえ複雑な関数呼び出しの奥底にある例外でもあますところなくキャッチされる。

    - +

    catchは複数書くことができる。

    + +} +

    tryブロックの中で投げられた例外は、たとえ複雑な関数呼び出しの奥底にある例外でもあますところなくキャッチされる。

    +

    関数hは関数gを呼び出し、関数gは関数fを呼び出し、関数fは例外を投げる。このように複雑な関数呼び出しの結果として投げられる例外もキャッチできる。

    すでに学んだように、std::array<T>::atに範囲外のインデックスを渡したときはstd::out_of_rangeクラスが例外として投げられる。これをキャッチしてみよう。

    - +

    例外による巻き戻し

    例外が投げられた場合、その例外が投げられた場所を囲むtryブロックと対応するcatchに到達するまで、関数呼び出しが巻き戻される。これをスタックアンワインディング(stack unwinding)という。

    - +

    この例では、関数mainが関数hを呼び出し、その結果として最終的に関数fの中で例外が投げられる。投げられた例外は関数呼び出しを巻き戻して関数mainの中のtryブロックまで到達し、対応するcatchに捕まる。

    もし関数mainを抜けてもなお対応するcatchがない場合はどうなるのか。

    - -

    その場合、std::terminate()という関数が呼ばれる。この関数が呼ばれた場合、プログラムは終了する。

    -

    tryブロックはネストできる。その場合、対応するcatchが見つかるまで巻き戻しが起こる。

    -
    void f()
    +

    その場合、std::terminate()という関数が呼ばれる。この関数が呼ばれた場合、プログラムは終了する。

    + + // プログラムは終了する + std::terminate() ; +}
    +

    tryブロックはネストできる。その場合、対応するcatchが見つかるまで巻き戻しが起こる。

    +

    上のコードは複雑なtryブロックのネストが行われている。プログラムがどのように実行されるのかを考えてみよう。

    まず関数mainが関数fを呼び出す。関数fは例外を投げる。関数fの中のtryブロックは対応するcatchがないので関数mainに巻き戻る。関数mainの内側のtryブロック、ソースコードでは// try 2 とコメントをしているtryブロックのcatchには対応しない。更に上のtryブロックに巻き戻る。// try 1tryブロックのcatchはint型なので、このcatchに捕まる。

    例外が投げられ、スタックアンワインディングによる巻き戻しが発生した場合、通常のプログラムの実行は行われない。例えば以下のプログラムは何も出力しない。

    - -

    スタックアンワインディング中に通常の実行は行われないが、変数の破棄は行われる。これはとても重要だ。変数が破棄されるとき、デストラクターが実行されるのを覚えているだろうか。

    - +

    スタックアンワインディング中に通常の実行は行われないが、変数の破棄は行われる。これはとても重要だ。変数が破棄されるとき、デストラクターが実行されるのを覚えているだろうか。

    +

    実行結果

    obj is constructed.
     obj is destructed.

    例外のスタックアンワインディングでは関数内の変数が破棄される。つまりデストラクターが実行される。

    - +

    このプログラムを実行した結果は以下のようになる。

    main is constructed.
     g is constructed.
     f is constructed.
     f is destructed.
     g is destructed.
    -catched.
    +caught.
     main is destructed.

    なぜこの順番に出力されるか考えてみよう。

      @@ -10444,7 +10477,7 @@

      例外による巻き戻し

    1. 関数fは例外を投げるので、fは破棄される。
    2. 関数gに巻き戻ったがcatchがないのでさらに巻き戻る。gが破棄される。
    3. 関数mainに巻き戻ったところ対応するcatchがあるのでスタックアンワインディングは停止する。
    4. -
    5. catched.が出力される。
    6. +
    7. caught.が出力される。
    8. mainが破棄される。

    例外が投げられると通常の実行は飛ばされるので、例外が投げられるかもしれない処理のあとに、例外の有無にかかわらず絶対に実行したい処理がある場合は、クラスのデストラクターに書くとよい。

    @@ -10456,47 +10489,47 @@

    意味上のポインター

    リファレンスと同じ機能

    ポインターはオブジェクトを参照するための機能だ。この点ではリファレンスと同じ機能を提供している。

    リファレンスを覚えているだろうか。T型へのリファレンスはT型のオブジェクトそのものではなく、T型のオブジェクトへの参照だ。リファレンスへの操作は、参照したオブジェクトへの操作になる。

    - -

    リファレンスは宣言と同時に初期化する。リファレンスの参照先をあとから返ることはできない。

    + // int型のオブジェクト + int object = 0 ; + + // オブジェクトを変更 + object = 123 ; + + // 123 + std::cout << object ; + + // T型へのリファレンス + // objectを参照する + int & reference = object ; + + // objectが変更される + reference = 456 ; + + // 456 + std::cout << object ; + + // referenceはobjectを参照している + object = 789 ; + + // 参照するobjectの値 + // 789 + std::cout << reference ; +} +

    リファレンスは宣言と同時に初期化する。リファレンスの参照先をあとから返ることはできない。

    +

    最後のr = y ;はリファレンスrの参照先をyに変えるという意味ではない。リファレンスrの参照先にyの値を代入するという意味だ。

    ポインターはリファレンスに似ている。並べてみるとほとんど同じ意味だ。

      @@ -10504,357 +10537,341 @@

      リファレンスと同じ機能

      T型へのポインターはT型のオブジェクトを参照する

    T型へのリファレンス型がT &であるのに対し、T型へのポインター型はT *だ。

    - +

    リファレンスの初期化は、単に参照したい変数名をそのまま書けばよかった。

    - -

    ポインターの場合、参照したい変数名に、&をつける必要がある。

    +int & reference = object ; +

    ポインターの場合、参照したい変数名に、&をつける必要がある。

    +

    リファレンスを経由してリファレンスが参照するオブジェクトを操作するには、単にリファレンス名を使えばよかった。

    - -

    ポインターの場合、ポインター名に*をつける必要がある。

    +int read = reference ; +

    ポインターの場合、ポインター名に*をつける必要がある。

    +

    ポインター名をそのまま使った場合、それは参照先のオブジェクトの値ではなく、ポインターという値になる。

    - +

    このように比較すると、ポインターはリファレンスと同じ機能を提供していることがわかる。実際、リファレンスというのはポインターのシンタックスシュガーにすぎない。ポインターの機能を制限して、文法をわかりやすくしたものだ。

    リファレンスと違う機能

    リファレンスがポインターの機能制限版だというのであれば、ポインターにあってリファレンスにはない機能はなんだろうか。代入と、何も参照しない状態だ。

    代入

    リファレンスは代入ができないが、ポインターは代入ができる。

    - +

    何も参照しない状態

    リファレンスは必ず初期化しなければならない。

    - +

    そのため、リファレンスは常にオブジェクトを参照している。

    ポインターは初期化しなくてもよい。

    - +

    この場合、具体的に何かを参照していない状態になる。この場合にポインターの値はどうなるかはわからない。初期化のない整数の値がわからないのと同じだ。

    - +

    このわからない値が発生することを、専門用語では「未規定の挙動」という。

    わからない値の整数を読むことは推奨できない。書くことはできる。

    - -

    このプログラムは何らかの値を出力するはずだが、具体的にどういう値を出力するのかはわからない。

    -

    そしてここからがポインターの恐ろしいところだが、ポインターの場合にもこのわからない値は発生する。わからない値を持ったポインターの参照先への読み書きは、「未定義の挙動」を引き起こす。

    + // 値はわからない + int data ; + + // 推奨できない + std::cout << data ; + + // OK + data = 0 ; +} +

    このプログラムは何らかの値を出力するはずだが、具体的にどういう値を出力するのかはわからない。

    +

    そしてここからがポインターの恐ろしいところだが、ポインターの場合にもこのわからない値は発生する。わからない値を持ったポインターの参照先への読み書きは、「未定義の挙動」を引き起こす。

    +

    なぜ未定義の挙動になるかというと、わからない値のポインターは、たまたまどこかの妥当なオブジェクトを参照してしまっているかもしれないからだ。

    未定義の挙動は恐ろしい。未定義の挙動が発生した場合、何が起こっても文句は言えない。なぜならばその挙動は本来存在するはずがないのだから。上のプログラムはコンパイル時にエラーになるかもしれないし、実行時にエラーになるかもしれない。いや、もっとひどいことにはエラーにならないかもしれない。そして人生、宇宙、すべてのものの答えと、あろうことか答えに対する質問まで出力するかもしれない。

    明示的に何も参照しないポインター: nullptr

    ポインターを未初期化にしていると、よくわからない値になってしまう。そのため、何も参照していないことを明示的に示すためのポインターの値、nullポインター値がある。nullptrだ。

    - +

    nullptrはどんな型へのポインターに対しても、何も参照していない値となる。

    - +

    C言語とC++では歴史的な理由で、nullptrの他にもNULLもnullポインター値

    - +

    C++ではさらに歴史的な理由で、0もnullポインター値として扱う。

    - +

    ただし、nullポインター値が実際に0である保証はない。ポインターの値についてはあとで詳しく扱う。

    無効な参照先の作り方

    ポインターやリファレンスによって参照先が参照される時点では有効だったが、後に無効になる参照先を作ることができてしまう。

    例えば以下のコードだ。

    - +

    このコードの問題は、関数fの中の変数variableの寿命は関数fの中だけで、呼び出し元に戻ったときには寿命が尽きるというところにある。変数variableへのポインターは変数variableの寿命が尽きたあとも存在してしまうので、存在しないオブジェクトにポインター経由でアクセスしようとしてエラーになる。

    同じ問題はリファレンスでも起きるが、ポインターのほうがこの問題を起こしやすい。

    - +

    文法上のポインター

    ポインターが難しいと言われる理由の一つに、ポインターの文法が難しい問題がある。

    ポインターとconstの関係

    型としてのポインターは、ある型Tがあるときに、Tへのポインター型となる。

    Tへのポインター型はT *と書く。

    - -

    リファレンスやconstも同じだ。

    - +

    リファレンスやconstも同じだ。

    +

    const intint constは同じ型だ。この場合、constはint型のあとにつけても前につけても同じ意味になる。

    すると当然の疑問が生じる。組み合わせるとどうなるのかということだ。

    ポインター型へのリファレンス型はできる。

    - +

    リファレンス型へのポインター型はできない。

    - +

    理由は、リファレンスへのポインターというのは意味がないからだ。ポインターへのリファレンスは意味がある。

    リファレンスからポインターの値を得るには、参照先のオブジェクトと同じく&を使う。

    - +

    リファレンスは参照先のオブジェクトと全く同じように振る舞うのでリファレンス自体のポインターの値を得ることはできない。

    ポインターのリファレンスを得るのは、ポインター以外の値と全く同じだ。

    - +

    constとポインターの組み合わせは難しい。

    まず型Tとそのconst版がある。

    - -

    そして型Tとそのポインター版がある。

    +using const_T = const T ; +

    そして型Tとそのポインター版がある。

    +

    これを組みわせると、以下のようになる。

    - +

    順番に見ていこう。まずは組み合わせない型から。

    - +

    Tはここではint型だ。T型はどんな型でもよい。

    const TT constが同じ型であることを思い出せば、const_T_1const_T_2は同じ型であることがわかるだろう。

    T_pointerはTへのポインターだ。

    次を見ていこう。

    - +

    これはどちらも同じ型だ。constなTへのポインターとなる。わかりにくければ以下のように書いてもよい。

    - +

    実際に使ってみよう。

    - -

    constなintへのポインターなので、このポインターの参照先を変更することはできない。ポインターは変更できる。

    -

    constなのはintであってポインターではない。const int *、もしくはint const *は参照先のintがconstなので、参照先を変更することができない。ポインターはconstではないので、ポインターの値は変更できる。

    -

    constなT型へのリファレンスでconstではないT型のオブジェクトを参照できるように、constなT型へのポインターからconstではないT型のオブジェクトを参照できる。

    + const int data = 123 ; + // int const *でもよい + const int * ptr = &data ; + + // 読み込み + int read = *ptr ; +} +

    constなintへのポインターなので、このポインターの参照先を変更することはできない。ポインターは変更できる。

    -

    この場合、リファレンスやポインターはconst int扱いなので、リファレンスやポインターを経由して読むことはできるが変更はできない。

    + // エラー + // constな参照先を変更できない + *ptr = 0 ; + + int y {} ; + // OK + // ポインターはconstではないので値が変更できる + ptr = &y ; +} +

    constなのはintであってポインターではない。const int *、もしくはint const *は参照先のintがconstなので、参照先を変更することができない。ポインターはconstではないので、ポインターの値は変更できる。

    +

    constなT型へのリファレンスでconstではないT型のオブジェクトを参照できるように、constなT型へのポインターからconstではないT型のオブジェクトを参照できる。

    + // constではない + int data { } ; + + // OK + const int & ref = data ; + // OK + const int * ptr = &data ; +} +

    この場合、リファレンスやポインターはconst int扱いなので、リファレンスやポインターを経由して読むことはできるが変更はできない。

    +

    その次はconstなポインターだ。

    - +

    これはポインターがconstなのであって、Tはconstではない。したがってポインターを経由して参照先を変更することはできるが、ポインターの値自体は変更できない型だ。

    - +

    最後はconstなTへのconstなポインターだ。

    - +

    これはconstなTなので、ポインターを経由して参照先を変更できないし、constなポインターなのでポインターの値も変更できない。

    - +

    ポインターのポインター

    ポインター型というのは、「ある型Tへのポインター」という形で表現できる。この型Tにはどんな型でも使うことができる。ところで、ポインターというのは型だ。もしTがポインター型の場合はどうなるのだろう。

    例えば、「T型へのポインター型」で、型Tが「U型へのポインター型」の場合、全体としては「U型へのポインター型へのポインター型」になる。これはC++の文法ではU **となる。

    C++のコードで確認しよう。

    - +

    具体的に書いてみよう。

    - +

    xはintだ。pはintへのポインターだ。ここまでは今までどおりだ。

    ppはint **という型で、「intへのポインターへのポインター」型だ。このポインターの値のためには「intへのポインターのポインター」が必要だ。変数pのポインターは&pで得られる。この場合、変数pは「intへのポインター」でなければならない。そうした場合、変数pのポインターは「intへのポインターのポインター」型の値になる。

    変数ppは「intへのポインターのポインター」だ。変数ppの参照先の変数pを読み書きするには、*ppと書く。これはまだ「intへのポインター」だ。ここからさらに参照先のint型のオブジェクトにアクセスするには、その結果にさらに*を書く。結果として**ppとなる。

    わかりにくければ変数に代入するとよい。

    - -

    リファレンスを使うという手もある。

    + // cとaは同じ値 + int * c = *pointer_to_pointer_to_object ; + + // objectに1が代入される + *c = 1 ; + // objectに2が代入される + **b = 2 ; +} +

    リファレンスを使うという手もある。

    +

    「ポインターへのポインター」があるということは、「ポインターへのポインターへのポインター」もあるということだろうか。もちろんある。

    - +

    もちろんconstもつけられる。

    - +

    関数へのポインター

    関数へのポインターを説明する前に、まず型としての関数を説明しなければならない。

    関数にも型がある。例えば以下のような関数、

    - +

    の型は、

    - +

    となる。関数から関数名を取り除いたものが関数の型だ。すると関数へのポインター型は以下のようになる。

    - +

    早速試してみよう。

    - +

    動くようだ。最後の関数呼び出しはまず参照先を得て(*ptr)、その後に関数呼び出し(123)をしている。これは面倒なので、C++では特別に関数へのポインターはそのまま関数呼び出しすることができるようになっている。

    - +

    ところで、変数ptrの宣言を、f_pointerというエイリアス宣言を使わずに書くと、以下のようになる。

    - +

    なぜこうなるのか。これを完全に理解するためにはC++の宣言子(declarator)という文法の詳細な理解が必要だ。

    ここでは詳細を飛ばして重要な部分だけ伝えるが、型名のうちポインターであることを指定する*は、名前にかかる。

    - +

    つまり以下のような意味だ。

    - +

    型名だけを指定する場合、名前が省略される。

    - +

    つまり以下のような意味だ。

    - +

    そのため、int * name( int )と書いた場合、これは「int型の引数を取り、int型へのポインターを戻り値として返す関数」となる。

    - +

    そうではなく、「int型の引数をとりint型の戻り値を返す関数へのポインター」を書きたい場合は、

    - +

    としなければならない。

    変数の名前を入れる場所は以下の通り

    - +

    なので、

    - +

    となる。あるいは以下のように書いてもいい。

    - +

    関数へのポインターは型であり、値でもある。値であるということは、関数は引数として関数へのポインターを受け取ったり、関数へのポインターを返したりできるということだ。

    早速書いてみよう。

    - +

    これは動く。ところでこの関数gへのポインターはどう書けばいいのだろうか。つまり、

    auto ptr = &g ;

    autoを使わずに書くとどうなるのだろうか。

    以下のようになる。

    - +

    なぜこうなるのか。分解すると以下のようになる。

    - +

    これはわかりにくい。戻り値の型を後ろに書く文法を使うと少し読みやすくなる。

    - +

    これを分解すると以下のようになる。

    - +

    もちろん、これでもまだわかりにくいので、エイリアス宣言を使ったほうがよい。

    - +

    配列へのポインター

    配列へのポインターについて学ぶ前に、配列の型について学ぶ必要がある。

    配列の型は、要素の型をT、要素数をNとすると、T [N]となる。

    - +

    関数型と同じく、ポインター宣言子である*は名前につく。

    - +

    エイリアス宣言を使わない変数の宣言は以下のようになる。

    - -

    配列とポインターは密接に関係している。そのため、配列名は配列の先頭要素へのポインターに暗黙に変換される。

    + int a[5] ; + int (*p)[5] = &a ; +} +

    配列とポインターは密接に関係している。そのため、配列名は配列の先頭要素へのポインターに暗黙に変換される。

    +

    配列とポインターの関係については、ポインターの詳細で詳しく説明する。

    ポインター型の作り方

    T型へのポインター型はT *で作ることができる。

    ただし、Tがint (int)のような関数型である場合は、int (*)(int)になる。配列型の場合は要素数Nまで必要でT (*)[N]になる。

    エイリアス宣言で型に別名をつけるとT *でよくなる。

    - +

    ポインターの型を書く際に、このようなことをいちいち考えるのは面倒だ。ここで必要のなのは、ある型Tを受け取ったときに型T *を得るような方法だ。ところで、物覚えのいい読者は前にも似たような文章を読んだことに気がつくだろう。そう、テンプレートだ。

    テンプレートは型を引数化できる機能だ。今まではクラスや関数にしか使っていなかったが、実はエイリアス宣言にも使えるのだ。

    - +

    これは引数と同じ型になるエイリアステンプレートだ。使ってみよう。

    - +

    using type = int ;というエイリアス宣言があるときtypeの型はintだ。エイリアス宣言は新しいtypeという型を作るわけではない。

    同様に、上のエイリアステンプレートtypeによるtype<int>の型はintだ。新しいtype<int>という型ができるわけではない。

    もう少し複雑な使い方もしてみよう。

    - +

    type<int>の型はintなので、それを引数に渡したtype< type<int> >intだ。type<T>をいくつネストしようともintになる。

    - +

    type<int>intなので、std::vector<type<int>>std::vector<int>になる。それを更にtype<T>で囲んでも同じ型だ。

    type<T>は面白いがなんの役に立つのだろうか。type<T>は型として使える。つまりtype<T> *はポインターとして機能するのだ。

    - +

    type<int> *int *型だ。type<int(int)> *int(*)(int)型だ。type<int [5]> *int (*) [5]型だ。これでもう*をどこに書くかという問題に悩まされることはなくなった。

    しかしわざわざtype<T> *と書くのは依然として面倒だ。T型は引数で受け取っているのだから、最初からポインターを返してどうだろうか。

    - +

    さっそく試してみよう。

    - +

    どうやら動くようだ。もっと複雑な例も試してみよう。

    - +

    add_pointer_t<int>int *なので、その型をadd_pointer_t<T>で囲むとその型へのポインターになる。結果としてint **になる。

    ここで実装したadd_pointer_t<T>Tがリファレンスのときにエラーになる。

    - +

    実は標準ライブラリにもstd::add_pointer_t<T>があり、こちらはリファレンスU &を渡しても、U *になる。

    - +

    標準ライブラリstd::add_pointer_t<T>は、Tがリファレンス型の場合、リファレンスは剥がしてポインターを付与するという実装になっている。これをどうやって実装するかについてだが、まだ読者の知識では実装できない。テンプレートについて深く学ぶ必要がある。今は標準ライブラリに頼っておこう。

    標準ライブラリには他にも、ポインターを取り除くstd::remove_pointer_t<T>もある。

    - +

    クラスへのポインター

    クラスへのポインターは今までに学んだものと同じ文法だ。

    - +

    ただし、ポインターを経由してメンバーにアクセスするのが曲者だ。

    以下のようなメンバーにアクセスするコードがある。

    - +

    これをポインターを経由して書いてみよう。

    以下のように書くとエラーだ。

    - +

    この理由は演算子の優先順位の問題だ。上の式は以下のように解釈される。

    - +

    ポインターを参照する演算子*よりも、演算子ドット(‘.’)のほうが演算子の優先順位が高い。

    このような式を可能にする変数pointerとは以下のようなものだ。

    - +

    pointer.data_memberはポインターなのでそれに演算子*を適用して参照した上で0を代入している。

    pointer.member_function()は関数呼び出しで戻り値としてポインターを返すのでそれに演算子*を適用している。

    演算子*を先にポインターの値であるpointerに適用するには、括弧を使う。

    - +

    リファレンスを使ってポインターを参照した結果をリファレンスに束縛して使うこともできる。

    - +

    ただし、ポインターを介してクラスを扱う際に、毎回括弧を使ったりリファレンスを使ったりするのは面倒なので、簡単なシンタックスシュガーとして演算子->が用意されている。

    - +

    a->bは、(*(a))->bと同じ意味になる。そのため、上は以下のコードと同じ意味になる。

    - +

    thisポインター

    メンバー関数はクラスのデータメンバーにアクセスできる。このときのデータメンバーはメンバー関数が呼ばれたクラスのオブジェクトのサブオブジェクトになる。

    - +

    すでに説明したように、メンバー関数が自分を呼びだしたクラスのオブジェクトのサブオブジェクトを参照できるのは、クラスのオブジェクトへの参照を知っているからだ。内部的には以下のような隠し引数を持つコードが生成されたかのような挙動になる。

    - +

    つまり、メンバー関数は自分を呼び出したクラスのオブジェクトへの参照を知っている。その参照にアクセスする方法がthisキーワードだ。

    thisキーワードはクラスのメンバー関数の中で使うと、メンバー関数を呼び出したクラスのオブジェクトへのポインターとして扱われる。

    - +

    さきほど、関数C::setの中でdata = n ;と書いたのは、this->data = n ;と書いたのと同じ意味になる。

    thisはリファレンスではなくてポインターだ。この理由は歴史的なものだ。本来ならばリファレンスのほうがよいのだが、今更変更できないのでポインターになっている。わかりにくければリファレンスに束縛してもよい。

    - -

    constなメンバー関数の中では、thisの型もconstなクラス型へのポインターになる。

    + auto & this_ref = *this ; + } +} ; +

    constなメンバー関数の中では、thisの型もconstなクラス型へのポインターになる。

    +

    この理由は、constなメンバー関数はクラスのオブジェクトへの参照としてconstなリファレンスを隠し引数として持つからだ。

    - +

    メンバーへのポインター

    メンバーへのポインターはかなり文法的にややこしい。そもそも、通常のポインターとは概念でも実装でも異なる。

    ここで取り扱うのはメンバーへのポインターという概念で、クラスのオブジェクトのサブオブジェクトへのポインターではない。サブオブジェクトへのポインターは通常のポインターと同じだ。

    - -

    メンバーへのポインターとは、クラスのデータメンバーやメンバー関数を参照するもので、クラスのオブジェクトとともに使うことでそのデータメンバーやメンバー関数を参照できるものだ。

    -

    細かい文法の解説はあとにして例を見せよう。

    -

    細かい文法はあとで学ぶとして、肝心の機能としてはこうだ。クラスのオブジェクトからは独立したデータメンバーやメンバー関数自体へのポインターを取得する。

    + *pointer = 123 ; + int read = object.subobject ; +} +

    メンバーへのポインターとは、クラスのデータメンバーやメンバー関数を参照するもので、クラスのオブジェクトとともに使うことでそのデータメンバーやメンバー関数を参照できるものだ。

    +

    細かい文法の解説はあとにして例を見せよう。

    + void member_function() + { std::cout << data_member ; } +} ; + +int main() +{ + // Object::data_memberメンバーへのポインター + int Object::* int_ptr = &Object::data_member ; + // Object::member_functionメンバーへのポインター + void (Object::* func_ptr)() = &Object::member_function ; + + // クラスのオブジェクト + Object object ; + + // objectに対するメンバーポインターを介した参照 + object.*int_ptr = 123 ; + // objectに対するメンバーポインターを介した参照 + // 123 + (object.*func_ptr)() ; + + // 別のオブジェクト + Object another_object ; + another_object.data_member = 456 ; + // 456 + (another_object.*func_ptr)() ; +} +

    細かい文法はあとで学ぶとして、肝心の機能としてはこうだ。クラスのオブジェクトからは独立したデータメンバーやメンバー関数自体へのポインターを取得する。

    +

    このポインターをクラスのオブジェクトと組み合わせることで、ポインターが参照するクラスのメンバーで、かつオブジェクトのサブオブジェクトの部分を参照できる。

    - +

    では文法の説明に入ろう。

    メンバーへのポインターは文法がややこしい。

    あるクラス名Cの型名Tのメンバーへのポインター型は以下のようになる。

    - +

    以下のクラスの各データメンバーへの型はそれぞれコメントのとおりになる。

    - +

    順を追って説明していこう。まずクラスABCのメンバー、

    - +

    このメンバーへのポインターの型はどちらもint ABC::*になる。データメンバーの型はintで、クラス名がABCなので、型名 クラス名::*に当てはめるとint ABC::*になる。

    - +

    このメンバーへのポインターの型はdouble ABC::*になる。

    最後のクラスABCのメンバー、

    - +

    これがint * ABC::*になる理由も、最初に説明した型名 クラス名::*のルールに従っている。型名がint *、クラス名がABCなので、int * ABC::*だ。

    最後の例はクラスDEFのメンバーとしてクラスABCのポインター型のメンバーだ。ABC DEF::*になる。

    クラス名Cのメンバー名Mのメンバーへのポインターを得るには以下の文法を使う。

    - +

    具体的な例を見てみよう。

    - +

    分かりづらければエイリアス宣言を使うとよい。

    - +

    あるいはauto使うという手もある。

    - +

    メンバー関数へのポインターは、メンバーへのポインターと関数へのポインターを組み合わせた複雑な文法となるので、とてもわかりづらい。

    復習すると、int型の引数を一つ受け取りint型の戻り値を返す関数へのポインターの型はint (*)(int)だ。

    - +

    この関数がクラスCのメンバー関数の場合、以下のようになる。

    - -

    ところで、メンバーへのポインターは型名 クラス名::*だった。この2つを組み合わせると、以下のように書ける。

    +} ; +

    ところで、メンバーへのポインターは型名 クラス名::*だった。この2つを組み合わせると、以下のように書ける。

    +

    メンバー関数へのポインターは難しい。

    関数fの型はint (int)で、そのポインターの型はint (*)(int)だ。するとクラス名Cのメンバー関数fへのポインターの型は、int (C::*)(int)になる。

    メンバー関数へのポインター型の変数を宣言してその値をC::fへのポインターに初期化しているのが以下の行だ。

    - +

    このptrを経由したメンバー関数fの呼び出し方だが、まずクラスのオブジェクトが必要になるので作る。

    - +

    そして演算子のoperator .*を使う。

    - +

    object.*ptrを括弧で囲んでいるのは、演算子の優先順位のためだ。もしこれを以下のように書くと、

    - +

    これはptr(123)という式を評価した結果をメンバーへのポインターと解釈してクラスのオブジェクトを介して参照していることになる。例えば以下のようなコードだ。

    - +

    演算子の優先順位の問題のために、(object.*ptr)と括弧で包んで先に評価させ、その後に関数呼び出し式である(123)を評価させる。

    実は演算子operator .*の他に、operator ->*という演算子がある。

    .*はクラスのオブジェクトがリファレンスの場合の演算子だが、`->*はクラスのオブジェクトがポインターの場合の演算子だ。

    - +

    演算子a->b(*(a)).bとなるように、演算子a->*b(*(a)).*bと置き換えられるシンタックスシュガーだ。

    上の例で、

    - +

    は、以下と同じだ。

    - +

    .*->*の文法を覚えるのが面倒な場合、標準ライブラリにstd::invoke( f, t1, ... )という便利な関数が用意されている。

    fがデータメンバーへのポインターで、t1がクラスのオブジェクトの場合、std::invoke(f, t1)は以下のような関数になる。

    - +

    なので以下のように書ける。

    - +

    便利なことにt1がポインターの場合は、

    - +

    という関数として振る舞う。そのため、リファレンスでもポインターでも気にせずに使うことができる。

    - +

    std::invokeが更に凄いことに、メンバー関数へのポインターにも対応している。

    std::invoke( f, t1, ... )で、fがメンバー関数へのポインターで、t1がクラスのオブジェクトへのリファレンスで、...が関数呼び出しの際の引数の場合、以下のような関数として振る舞う。

    - +

    厳密にはこの宣言は間違っているのだが、まだ知らない機能を使っているので気にしなくてもよい。大事なことは、std::invokeの第三引数以降の実引数が、関数呼び出しの実引数として使われるということだ。

    - +

    この場合も、objectがCへのリファレンスではなく、Cへのポインターでも自動で認識していいように処理してくれる。

    ポインターの内部実装

    ポインターの意味上と文法上の解説は終えた。ここからはポインターの内部実装についてだ。ポインターの値とは外でもない、メモリー上のアドレスのことだ。

    @@ -11573,20 +11606,20 @@

    メモリとアドレス

    ポインターのサイズ

    ポインターの値というのはアドレスの値だ。ポインターの値を格納するのにもメモリが必要だ。ではポインターのサイズは何バイトあるのだろう。

    型Tのサイズを調べるにはsizeof(T)を使う。

    - +

    筆者の環境でこのプログラムを実行した結果は以下のようになった。

    8
     8
    @@ -11595,20 +11628,20 @@ 

    ポインターのサイズ

    ポインターの値

    ポインターが8バイト、つまり64ビットの値であるならば、それを8バイトの符号なし整数として解釈した値はどうなるのだろう。

    C++にはすべてのポインターの値を格納できるサイズの符号なし整数型が用意されている。std::uintptr_tだ。

    - +

    筆者の環境でこのプログラムを実行した結果も8が出力される。

    ポインターもstd::uintptr_tも8バイトだ。ポインターのバイト列をstd::uintptr_tとして強引に解釈すれば、符号なし整数としての値を出力してみよう。

    ある値fromのバイト列を、同じバイト数のある型toの値として強引に解釈するC++20で追加された標準ライブラリに、std::bit_cast<to>(from)がある。

    - +

    このプログラムを何度か実行した結果、以下のような結果を得た。

    $ make run
     140725678382588
    @@ -11619,125 +11652,125 @@ 

    ポインターの値

    私の環境ではポインターの具体的な値は実行ごとに異なる。これは私の使っているOSがASLR(Address Space Layout Randomization)を実装しているためだ。興味のある読者は調べてみるとよい。

    この値はint型の変数dataのポインターの整数としての値だ。このアドレスの場所に、int型のオブジェクトの最初の1バイトがあり、その次の場所に次の1バイトがある。

    筆者の環境ではint型は4バイトだ。

    - -

    int型のオブジェクトは4バイトの連続したメモリー上に構築されている。つまり、本質的には以下のようなコードと同等になる。

    + std::cout << sizeof(int) ; +}
    +

    int型のオブジェクトは4バイトの連続したメモリー上に構築されている。つまり、本質的には以下のようなコードと同等になる。

    +

    std::byteというのはsizeof(std::byte)の結果が1になる、サイズが1バイトの符号なし整数型だ。

    std::byteはC++で1バイトの生の値を表現するために使うことができる。配列は連続したバイト列なので、4バイトのint型は、本質的には上のようなコードになる。ただし上のコードはアライメントという概念が欠けている。これについては後で説明する。

    ところで、std::bit_castは2020年に制定される国際標準規格C++20から入った。しかるに筆者がこの文章を書いているのは2018年だ。まだC++20を完全に実装したC++コンパイラーは存在しない。この本が出版されてしばらくは、読者の手元にもC++20コンパイラーは存在しないだろう。

    std::bit_castの実装

    ないものは自分で実装すればいい。std::bit_castに近いものを実装してみよう。

    今回実装するbit_castは以下のような関数テンプレートだ。

    - -

    bit_castの実装にはポインターが必要だ。Fromの値を表現するバイト列への先頭のポインターをとり、バイト単位でToの値を表現するバイト列にコピーすればよい。

    -

    標準ライブラリにはそのような処理を行ってくれるstd::memcpy(dest, src, n)がある。ポインターsrcからnバイトをポインターdestからnバイトに書き込む関数だ。

    + // 値fromのバイト列をTo型の値として解釈して返す。 +}
    +

    bit_castの実装にはポインターが必要だ。Fromの値を表現するバイト列への先頭のポインターをとり、バイト単位でToの値を表現するバイト列にコピーすればよい。

    +

    標準ライブラリにはそのような処理を行ってくれるstd::memcpy(dest, src, n)がある。ポインターsrcからnバイトをポインターdestからnバイトに書き込む関数だ。

    +

    これでstd::bit_castの実装はできた。しかしこの実装は問題をstd::memcpyにたらい回しにしただけだ。std::memcpyも実装できて初めてstd::bit_castを自前で実装できたと言える。

    std::memcpyの実装

    std::memcpyはC++コンパイラーによって効率の良いコードに置き換えられる。そのため自分で実装したstd::memcpyを標準ライブラリと同じ効率にすることは難しいが、機能的にはほとんど同じものを作ることができる。

    memcpyの実装にはポインターの詳細な理解が必要だ。

    std::memcpy関数は以下のようになっている。

    - +

    みなれないvoid *という型が出てきた。まずはこれについて学ぼう。

    void型

    voidは特別な型だ。void型は何も値を持たない型という意味を持つ。例えば関数が戻り値を何も返さない場合、void型を返す関数として宣言される。

    - +

    あらゆる値はvoid型に変換することができる。変換した結果は、何も値を持たない。

    - +

    C++17では、void型の変数は作れない。

    - +

    ところで、読者が本書を読む頃には、C++規格ではvoid型の変数が作れるようになっているかもしれない。これはvoid型だけ変数を作れないのが面倒だからつくれるようになるだけで、具体的な値のない変数になる。

    void *型

    void *型は「void型へのポインター型」だ。int *が「int型へのポインター型」であるのと同じだ。

    void *型の値は、ある型Tへのポインター型から型Tという情報が消え去ったポインターの値だ。ポインターの値というのはアドレスで、アドレスというのは単なるバイト単位のメモリを指す整数値だということを学んだ。void *型は特定の型を意味しないポインター型だ。

    ある型Tへのポインター型の値は、void *型に変換できる。

    - -

    void *型の値eから元の型Tへのポインターに変換するにはstatic_cast<T *>(e)が必要だ。

    -

    もしstatic_cast<T *>(e)のeがT *として妥当なアドレスの値であれば、変換後も正しく動く。

    -

    T const *型はvoid const *型に変換できる。その逆変換もできる。

    +

    void *型の値eから元の型Tへのポインターに変換するにはstatic_cast<T *>(e)が必要だ。

    +

    もしstatic_cast<T *>(e)のeがT *として妥当なアドレスの値であれば、変換後も正しく動く。

    +

    T const *型はvoid const *型に変換できる。その逆変換もできる。

    +

    ポインター間の型変換でconstを消すことはできない。

    memcpyはvoid *を使うことで、どんなポインターの値でも取れるようにしている。C++にはテンプレートがあるので以下のように宣言してもよいのだが、

    - +

    memcpyはC++以前からあるC言語ライブラリなので、こうなっている。

    std::byte型

    void *型はアドレスだけを意味するポインター型なので、参照することができない。memcpyの実装にはポインターを経由して参照先を1バイトづつ読み書きする必要がある。そのための型としてstd::byteがある。

    std::byte型は1バイトを表現するための型だ。sizeof(std::byte)の結果は1になる。

    1バイトというのは10進数で\(0 \leqq n \leqq 255\)までの値を扱う。

    std::byteはとても厳格に1バイトの符号なし整数として振る舞うので、普通の整数で初期化や代入をすることができない。

    - +

    std::byteに具体的な値で初期化するには{x}を使う。

    - +

    std::byteに値を代入するにはstd::byte{x}を使う

    - +

    static_cast<std::byte>(x)std::byte(x)はコンパイルできるが、使ってはならない。

    - +

    何故使ってはならないかというと、範囲外の値を無理やり変換してしまうからだ。

    - +

    配列のメモリ上での表現

    配列は要素型を表現するバイト列をメモリ上に連続して配置する。

    例えばint [3]という配列があり、sizeof(int)が4の場合、全体で12バイトのメモリが確保される。

    - +

    最初の4バイト(0バイト目から3バイトまで)の領域は0番目の要素であるdata[0]で、その値は1だ。

    次の4バイト(4バイト目から7バイト目まで)の領域は1番目の要素であるdata[1]で、その値は2だ。

    最後の4バイト(8バイト目から11バイト目まで)の領域は2番めの要素であるdata[2]で、その値は3だ。

    @@ -11751,20 +11784,20 @@

    配列のメモリ上での表現

    実際にアドレスの生の値を出力して確かめてみよう。

    - +

    このプログラムを筆者の環境で実行すると以下のように出力された。

    140736120015884
     140736120015888
    @@ -11773,31 +11806,31 @@ 

    配列のメモリ上での表現

    ポインターと整数の演算

    ポインターと整数を加減算することができる。

    ポインターT *に整数nを足すと、ポインターのアドレスがsizeof(T) * n加算される。この結果、ポインターは要素が配列のように配置された場合にn個先の要素を指すようになる。

    - +

    これを筆者の環境で実行すると以下のように出力された。

    140722117900224
     140722117900236
    @@ -11812,96 +11845,96 @@ 

    いよいよmemcpyの実装

  • srcの参照先からnバイトをdestの参照先にコピーする
  • destを返す
  • - +

    memcpyの別の実装

    ポインターはoperator []に対応している。

    ポインターpと整数iに対してp[i]と書いたとき、*(p + i)という意味になる。

    - -

    memcpyoperator []を使って書くこともできる。

    - +

    memcpyoperator []を使って書くこともできる。

    +

    データメンバーへのポインターの内部実装

    データメンバーへのポインターの整数としての値は少し変わっている。

    ポインターの生の値は、メモリー上で値を表現しているバイト列の先頭アドレスだ。

    データメンバーへのポインターは、具体的なクラスのオブジェクトへのポインターやリファレンスがあって初めて意味がある。

    - +

    配列が要素型のバイト列を連続して配置したメモリレイアウトをしているように、クラスもデータメンバーを連続して配置したメモリーレイアウトをしている。

    たとえば以下のようなクラスObjectがある場合、

    - -

    このクラスのサイズはsizeof(Object)だ。このクラスはint型のサブオブジェクトを3つ持っているので、そのサイズは少なくともsize(int)*3はある。

    -

    実際に確かめてみよう。

    +} ;
    +

    このクラスのサイズはsizeof(Object)だ。このクラスはint型のサブオブジェクトを3つ持っているので、そのサイズは少なくともsize(int)*3はある。

    +

    実際に確かめてみよう。

    +

    このプログラムを筆者の環境で実行すると以下のように出力された。

    sizeof(int): 4
     sizeof(Object): 12
    @@ -11909,123 +11942,123 @@

    データメ

    全体で12バイトということは、配列int [3]と同じように、最初の4バイトにx,y,zのどれかが、次の4バイトに残りのどちらかが、最後の4バイトに残りが配置されている。

    データメンバーへのポインターというのは、このクラスのオブジェクトを表現するバイト列の先頭から何バイト目に配置されているかというオフセット値になっている。

    具体的な値を見てみよう。

    - +

    このプログラムを筆者の環境で実行すると以下のように出力される。

    0
     4
     8

    筆者の環境では、xはクラスの先頭アドレスからオフセット0バイトに、yはオフセット4バイトに、zはオフセット8バイトに配置されているようだ。

    確かめてみよう。

    - +

    筆者の環境では以下のように出力される

    123456789

    このプログラムの実行結果は環境によって変わる。読者の使っている環境でデータメンバーへのポインターが筆者の環境と同じように実装されているとは限らない。

    イテレーター詳細

    イテレーターとポインターの関係

    arrayのイテレーターの実装を振り返ろう。前回実装したイテレーターは、リファレンスとインデックスを使うものだった。

    - -

    このコードは単にポインターをクラスで実装しているだけではないだろうか。ならば、ポインターでイテレーターを実装することもできるのではないか。

    -
    template < typename Array >
    +
    -

    このコードは本当にポインターをクラスで実装しているだけだ。ならばイテレータークラスの代わりにポインターでもいいのではないだろうか。

    - +

    このコードは単にポインターをクラスで実装しているだけではないだろうか。ならば、ポインターでイテレーターを実装することもできるのではないか。

    + + using pointer = typename Array::pointer ; + using reference = typename Array::reference ; + pointer p ; + + array_iterator( pointer p ) + : p(p) { } + + reference operator *() + { return *p ; } + + array_iterator & operator ++() + { + ++p ; + return *this ; + } + + reference operator[] ( std::size_t n ) + { return p[n] ; } +} ;
    +

    このコードは本当にポインターをクラスで実装しているだけだ。ならばイテレータークラスの代わりにポインターでもいいのではないだろうか。

    +

    これは動く。そして実際のstd::arrayの実装もこうなっている。

    実はイテレーターはポインターを参考にして作られた。インクリメントで次の要素を参照、operator *で参照先の要素にアクセスといった操作は、すべてポインターの操作をより抽象化したものだ。

    ポインターの操作をすべてサポートしたイテレーターは、ランダムアクセスイテレーターと呼ばれる。

    @@ -12048,1071 +12081,1071 @@

    イテレーターカテゴリー

    AはBであることに加えて、追加の操作をサポートしている。

    ランダムアクセスイテレーター

    ランダムアクセスイテレーターは名前の通りランダムアクセスができる。イテレーターがn番目の要素を指すとき、n+m番目の要素を指すことができる。mは負数でもよい。

    - -

    と書ける。nの型が符号付き整数型でよい。i + (-5)i-5と同じ意味だ。

    -

    イテレーター間の距離を計算したいときはイテレーター同士を引き算する。

    -

    イテレーター間の距離は負数にもなる。

    + i + n ; + i - n ; + n + i ; // i+nと同じ + n - i ; // n-iと同じ + + i + (-n) ; // i - nと同じ + + // i = i + n ; と同じ + i += n ; + // i = i - n ; と同じ + i -= n ; +}
    +

    と書ける。nの型が符号付き整数型でよい。i + (-5)i-5と同じ意味だ。

    +

    イテレーター間の距離を計算したいときはイテレーター同士を引き算する。

    -

    イテレーターbはaより3進んでいるので、aからbまでの距離であるb - aは3になる。ではbからaまでの距離であるa - bはどうなるかというと、-3になる。bにとってaは3戻っているからだ。

    -

    イテレーターiのn個先の要素を参照したい場合は、

    + b - a ; // aからbまでの距離 + a - b ; // bからaまでの距離。 +} +

    イテレーター間の距離は負数にもなる。

    -

    と書ける。

    -

    ランダムアクセスイテレーターは大小比較ができる。

    + auto b = a ; + // bはaより3進んでいる + ++b ; ++b ; ++b ; + b - a ; // 3 + a - b ; // -3 +} +

    イテレーターbはaより3進んでいるので、aからbまでの距離であるb - aは3になる。ではbからaまでの距離であるa - bはどうなるかというと、-3になる。bにとってaは3戻っているからだ。

    +

    イテレーターiのn個先の要素を参照したい場合は、

    + // *(i + n) ; と同じ + i[n] ; +} +

    と書ける。

    +

    ランダムアクセスイテレーターは大小比較ができる。

    +

    イテレーターの比較は、イテレーターが参照する要素の値の比較ではない。イテレーターが参照する要素の順番の比較だ。

    n番目の要素を参照するイテレーターは、n+1番目の要素を参照するイテレーターより小さい。n-1番目を参照するイテレーターより大きい。

    - -

    ここまでの操作はランダムアクセスイテレーターにしかできない。

    -

    双方向イテレーター以下のイテレーターができる比較は同値比較だけだ。

    -

    イテレーターは同じn番目の要素を指しているときに等しいと比較される。

    + // jはn+1番目を指す + auto j = i + 1 ; + + i < j ; // true + i > j ; // false +} +

    ここまでの操作はランダムアクセスイテレーターにしかできない。

    +

    双方向イテレーター以下のイテレーターができる比較は同値比較だけだ。

    + i == j ; + i != j ; +} +

    イテレーターは同じn番目の要素を指しているときに等しいと比較される。

    +

    双方向イテレーター

    双方向イテレーターは名前の通り双方向のイテレーターの移動ができる。双方向というのはイテレーターが参照しているn番目の要素のn-1番目の要素とn+1番目の要素だ。

    - +

    と書ける。この操作は前方イテレーターにはできない。

    1個づつ移動できるのであれば、イテレーターをn個進めることもできそうなものだ。実際、双方向イテレーターを以下のようにしてn個進めることができる。

    - +

    たしかにこれはできる。できるが、効率的ではない。双方向イテレーターが提供される場合というのは、ランダムアクセスが技術的に可能ではあるが非効率的な場合だ。具体的なデータ構造を出すと、例えばリンクリストがある。リンクリストに対するランダムアクセスは技術的に可能であるが非効率的だ。

    前方イテレーター

    -

    前方イテレーターは前方にしか移動できない。イテレーターが0番目の要素を指しているならば1番目、1番目の要素を指しているならば2番めに移動できる。

    - -

    前方イテレーターにはマルチパス保証がある。イテレーターの指す要素を動かす前のイテレーターの値を保持しておき、保持した値を動かしたとき、ふたつのイテレーターは同一になるという保証だ。

    +

    前方イテレーターは前方にしか移動できない。イテレーターが0番目の要素を指しているならば1番目、1番目の要素を指しているならば2番目に移動できる。

    + ++i ; +} +

    前方イテレーターにはマルチパス保証がある。イテレーターの指す要素を動かす前のイテレーターの値を保持しておき、保持した値を動かしたとき、ふたつのイテレーターは同一になるという保証だ。

    +

    入力イテレーター、出力イテレーターにはこの保証がない。

    入力イテレーター

    入力イテレーターはイテレーターの比較、イテレーターの参照、イテレーターのインクリメントができる。

    - -

    入力イテレーターの参照は、読み込みことしか保証されていない。

    + // 比較 + bool b1 = (i == j) ; + bool b2 = (i != j) ; + + // 参照 + *i ; + // (*i).m と同じ + i->m ; + + // インクリメント + ++i ; + i++ ; +} +

    入力イテレーターの参照は、読み込みことしか保証されていない。

    +

    書き込みは出力イテレーターの仕事だ。

    出力イテレーター

    出力イテレーターはイテレーターのインクリメントと、イテレーターの参照への代入ができる。

    - -

    出力イテレーターを参照した結果は定められていない。voidかもしれない。したがって出力イテレーターの値を読むのは意味がない。

    + // 参照への代入 + *i = v ; + + // インクリメント + ++i ; + i++ ; +} +

    出力イテレーターを参照した結果は定められていない。voidかもしれない。したがって出力イテレーターの値を読むのは意味がない。

    +

    iterator_traits

    イテレーターカテゴリーやイテレーターの参照する値を見分けるためのライブラリとして、iterator_traits<T>がある。これは以下のようになっている。

    - +

    difference_typeはイテレーター同士の距離を指す数値だ。

    - -

    value_typeはイテレーターの参照する値の型、pointerはそのポインター型、referenceはそのリファレンス型だ。

    + // イテレーター同士の距離 + typename std::iterator_traits<Iterator>::difference_type diff = j - i ; +} +

    value_typeはイテレーターの参照する値の型、pointerはそのポインター型、referenceはそのリファレンス型だ。

    +

    iterator_categoryはイテレーターカテゴリーを示す型で、以下のようになっている。

    - +

    forward_iterator_tag以降のコロン文字の後に続くコードについては、今は気にしなくてもよい。これは派生というまだ説明していないクラスの機能だ。

    あるイテレーターがあるイテレーターカテゴリーを満たすかどうかを調べるには以下のようにする。

    - +

    このコードはまだ学んでいないC++の機能をふんだんに使っているので、現時点で理解するのは難しい。

    イテレーターカテゴリーの実例

    イテレーターカテゴリーについて学んだので、イテレーターカテゴリーの実例について見ていこう。

    出力イテレーター

    前方イテレーター以上のイテレーターカテゴリーを満たすイテレーターはすべて、出力イテレーターとして使える。例えばstd::arrayの内容をstd::vectorにコピーしたければ以下のように書ける。

    - +

    std::vectorのイテレーターは出力イテレーターとして振る舞う。

    出力イテレーターの要件しか満たさないイテレーターは、例えば以下のようなものだ。

    - +

    cout_iterator*i = x;と書いたときに、値xをstd::coutで出力する。

    cout_iteratorは出力イテレーターの要件を満たすのでstd::copyに渡せる。std::copyはイテレーターを順番に*out = *i ;のように実行するので、結果として値が全てstd::coutで出力される。

    cout_iteratorはとても便利なので、標準ライブラリにはstd::ostream_iterator<T>がある。

    - +

    ostream_iteratorは出力ストリーム(ostream)に対するイテレーターだ。コンストラクターに出力先の出力ストリームを渡すことで値を出力先に出力してくれる。今回はstd::coutだ。

    上のような出力イテレーターがoperator =で以下のようなことをしていたらどうだろう。

    - +

    このコードが何をするかわかるだろうか。コンテナーcの全要素を出力イテレーターで出力する。出力イテレーターは渡された値valueをtemp.push_back(value) ;する。その結果、tempcのすべての要素を保持していることになる。

    C++の標準ライブラリにはstd::back_inserterがある。

    - +

    std::back_inserter(c)はコンテナーcに出力イテレーターとして渡された値をpuch_backする。

    ただし、std::back_inserterは古いライブラリなので、ここで示した方法とは少し違う実装がされている。

    - -

    この理由は、C++17以前のC++ではクラスのコンストラクターからテンプレート実引数の推定ができなかったためだ。

    -
    template < typename T >
    -void f( T ) { }
    -
    -template < typename T >
    -struct S
    -{
    -    S( T ) { }
    -} ;
    -
    -int main()
    -{
    -    // f<int>と推定
    -    f(0) ;
    -
    -    // S<int>と推定
    -    S s(0) ;
    +
    +

    この理由は、C++17以前のC++ではクラスのコンストラクターからテンプレート実引数の推定ができなかったためだ。

    +

    C++17以前のC++では関数の実引数からテンプレート仮引数Tの型を推定することはできたが、クラスのコンストラクターから推定することはできなかった。C++17以降は可能だ。

    std::coutに出力したり、コンテナーにpush_backする実装のイテレーターは、マルチパス保証を満たさない。実装を見ればわかるように、イテレーターをコピーして別々にインクリメントした結果のイテレーターのオブジェクトに対する操作は同一ではないからだ。

    入力イテレーター

    入力イテレーターの実例はどうか。

    std::cinからT型を読み込む入力イテレーターの実装は以下のようになる。

    - +

    以下のように使える。

    - +

    実装としては、まずボイラープレートコード

    - +

    difference_typeはイテレーターの距離を表現する型で、通常はstd::ptrdiff_tという型が使われる。これはポインターの距離を表現する型だ。

    value_typeは参照している要素の型、referencepointerは要素に対するリファレンスとポインターだ。

    iterator_categoryは今回は入力イテレーターなのでstd::input_iterator_tagになる。

    データメンバーが2つ。

    - +

    failstd::cinが失敗状態のときにtrueになる。通常はfalseだ。std::cinが失敗状態かどうかは、メンバー関数failで確かめることができる。

    - +

    std:cinが失敗状態になる理由はいくつかあるが、EOFが入力された場合や、指定した型の値を読み込めなかった場合、例えばint型を読み込むのに入力が“abcd”のような文字列だった場合にtrueになる。

    valuestd::cinから読み込んだ値だ。

    イテレーターから値を読み込むのはoperator *の仕事だ。これは単にvalueを返す。

    - +

    入力イテレーターでは値の読み込みのみをサポートしている。書き込みはサポートしない。イテレーターiに対して*iは書けるが、*i = xとは書けない。

    実際にstd::cinから値を読み込むのはoperator ++で行われる。

    - +

    まずstd::cinが失敗状態でないかどうかを確認する。失敗状態となったstd::cinからは読み込めないからだ。失敗状態でなければ値を読み込み、失敗状態かどうかを確認する。結果の値はvalueに、失敗状態かどうかはfailに保持される。

    後置インクリメントは前置インクリメントを呼び出すだけの汎用的な実装だ。

    - +

    コンストラクターにtrueを渡すと、イテレーターを最初から失敗状態にしておく

    - +

    コンストラクターは最初の値を読み込むために自分自身にインクリメントを呼び出す。

    入力イテレーターは同値比較ができる。

    - +

    イテレーターが同値比較できると、イテレーターが終了条件に達したかどうかの判定ができる。

    - +

    このような関数printに、vectorのbegin/endを渡すと、vectorの要素をすべて標準出力する。

    - -

    cin_iteratorを渡した場合、失敗状態になるまで標準出力する。

    -

    cin_iteratorが比較するのはstd::cinの失敗状態の有無だ。この比較によって、cin_iteratorで標準入力から失敗するまで値を読み込み続けることができる。

    -

    このようなイテレーターは標準にstd::istream_iterator<T>として存在する。

    +

    cin_iteratorを渡した場合、失敗状態になるまで標準出力する。

    + cin_iterator iter, fail(true) ; + print( iter, fail ) +}
    +

    cin_iteratorが比較するのはstd::cinの失敗状態の有無だ。この比較によって、cin_iteratorで標準入力から失敗するまで値を読み込み続けることができる。

    +

    このようなイテレーターは標準にstd::istream_iterator<T>として存在する。

    +

    標準ライブラリは読み込むストリームをコンストラクターで取る。何も指定しない場合は失敗状態になる。

    前方イテレーター

    前方イテレーター以上のイテレーターの例として、iota_iterator<T>を実装してみよう。

    このイテレーターはT型の整数を保持し、operator *でリファレンスを返し、operator ++でインクリメントする。

    以下のように使える。

    - +

    早速実装してみよう。まずはネストされた型名と初期化から。

    - +

    これでイテレーターとしてオブジェクトを作ることができるようになる。コピーは自動的に生成されるので書く必要はない。

    - +

    残りのコードも書いていこう。operator *は単にvalueを返すだけだ。

    - +

    非const版とconst版があるのは、constなiota_iteratorのオブジェクトからも使えるようにするためだ。

    - +

    noexceptはこの関数は例外を外に投げないという宣言だ。今回、例外を投げる処理は使わないので、noexceptを指定できる。

    operator ++を実装しよう。

    - +

    すでに説明したようにインクリメント演算子には前置後置の2種類が存在する。

    - +

    前置インクリメント演算子は引数を取らず、後置インクリメント演算子は区別のためだけに特に意味のないint型の引数を取る。

    インクリメント演算子も例外を投げないのでnoexceptを指定する。

    インクリメント演算子はデータメンバーを変更するのでconstは指定しない。

    最後は比較演算子だ。

    - +

    前方イテレーターがサポートする比較演算子は2つ、operator ==operator !=だ。!===で実装してしまうとして、==は単にvalueを比較する。通常、イテレーターの比較は要素の値の比較ではなく、同じ要素を参照するイテレーターかどうかの比較になるが、iota_iteratorの場合、vectorarrayのようなメモリ上に構築された要素は存在しないので、valueの比較でよい。

    前方イテレーターが提供される実例としては、前方リンクリストがある。

    - -

    このforward_link_list<T>というクラスはT型の値を保持するvalueと、次のクラスのオブジェクトを参照するポインターnextを持っている。このクラスlistの次の要素は*(list.next)で、listの2つ次の要素は*(*list.next).next)だ。

    -

    このようなforward_link_listへのイテレーターの骨子は以下のように書ける。

    + T value ; + forward_link_list * next ; +} ; + +int main() +{ + forward_link_list<int> list3{ 3, nullptr } ; + forward_link_list<int> list2{ 2, &list3 } ; + forward_link_list<int> list1{ 1, &list2 } ; + forward_link_list<int> list0{ 0, &list1 } ; +} +

    このforward_link_list<T>というクラスはT型の値を保持するvalueと、次のクラスのオブジェクトを参照するポインターnextを持っている。このクラスlistの次の要素は*(list.next)で、listの2つ次の要素は*(*list.next).next)だ。

    +

    このようなforward_link_listへのイテレーターの骨子は以下のように書ける。

    +

    前方リンクリストはvectorやarrayのように要素の線形の集合を表現できる。n番目の要素からn+1番目の要素を返すことはできる。

    - +

    ただしn-1番目の要素を返すことはできない。その方法がないからだ。

    前方イテレーターが入力/出力イテレーターと違う点は、マルチパス保証があることだ。イテレーターのコピーを使いまわして複数回同じ要素をたどることができる。

    - +

    前方イテレーター以上のイテレーターにはこのマルチパス保証がある。

    双方向イテレーター

    双方向イテレーターはn番目の要素を指すイテレーターからn-1番目を指すイテレーターを得られるイテレーターだ。n-1番目を指すにはoperator --を使う。

    - -

    iota_iteratorを双方向イテレーターにするのは簡単だ。

    - +

    iota_iteratorを双方向イテレーターにするのは簡単だ。

    +

    イテレーターカテゴリーは双方向イテレーターを表現するstd::bidirectional_iterator_tagを指定する。

    operator --の実装はoperator ++の実装と要領は同じだ。

    これでiota_iteratorが双方向イテレーターになった。

    双方向イテレーターが提供される実例としては、双方向リンクリストがある。前方リンクリストが前方の要素への参照を持つのに対し、双方向リンクリストは後方の要素への参照も持つ。

    - +

    双方向リンクリストに対するイテレーター操作の骨子は以下のようになる。

    - +

    ランダムアクセスイテレーター

    ランダムアクセスイテレーターにできることは多い。すでにランダムアクセスイテレーターでできることは解説したので、iota_iteratorを対応させていこう。

    イテレーターの参照する要素の移動の部分。

    - +

    ランダムアクセスイテレーター iとdifference_type nがあるとき、i + nn + iは同じ意味だ。i + nはイテレーターのメンバー関数としても、クラス外のフリー関数としても実装できる。どちらでも好きな方法で実装してよい。

    参考に、クラス外のフリー関数として実装する場合は以下のようになる。

    - +

    n + iは必ずクラス外のフリー関数として実装しなければならない。クラスのメンバー関数として演算子のオーバーロードをする場合はオペランドがthisになるからだ。

    イテレーターの距離の実装はiota_iteratorの場合、単にvalueの差だ。

    メンバー関数として実装する場合は以下の通り。

    - -

    クラス外のフリー関数として実装する場合は以下の通り。

    -

    大小比較の実装もvalueを比較するだけだ。

    +struct iota_iterator +{ + difference_type operator - ( iota_iterator const & i ) + { + return value - i.value ; + } +} ; +

    クラス外のフリー関数として実装する場合は以下の通り。

    +typename iota_iterator<T>::difference_type +( iota_iterator<T> const & a, iota_iterator<T> const & b ) +{ + return a.value - b.value ; +} +

    大小比較の実装もvalueを比較するだけだ。

    +

    ランダムアクセスイテレーターの実例としては、連続したメモリ上に構築された要素の集合に対するイテレーターがある。標準ライブラリでは、vectorやarrayが該当する。

    vectorやarrayの中身は連続したメモリ上に確保された要素で、要素の参照にはポインターか、ポインターとインデックスが用いられる。

    - +

    vectorやarrayのイテレーターの実装は、ポインターとほぼ同じ処理をしている。その実装は上にあるように、単にポインターに処理をデリゲートするだけだ。

    そこで、C++標準ライブラリの実装によっては、vectorやarrayの実装は単に生のポインターを返す。

    - +

    イテレーターはクラスであり、そのネストされた型名にvalue_typeやdifference_typeやiterator_categoryなどの型がある。

    - -

    vectorやarrayのイテレーターが単に生のポインターを返す実装の場合、上のコードは動かない。

    -

    こういうときのために、iterator_traits<T>がある。もしTがポインターの場合は、ネストされた型名を都合のいいように宣言してくれる。

    +

    vectorやarrayのイテレーターが単に生のポインターを返す実装の場合、上のコードは動かない。

    +

    こういうときのために、iterator_traits<T>がある。もしTがポインターの場合は、ネストされた型名を都合のいいように宣言してくれる。

    +

    そのため、イテレーターのネストされた型名を使うときには、直接使うのではなく、一度iterator_traitsを経由してつかうとよい。

    イテレーター操作

    イテレーターはそのまま使うこともできるが、一部の操作を簡単に行うための標準ライブラリがある。

    advance( i, n ): n移動する

    イテレーター iをn回移動したいとする。ランダムアクセスイテレーターならば以下のようにする。

    - +

    しかし前方イテレーターの場合、operator +=は使えない。n回operator ++を呼び出す必要がある。

    - +

    双方向イテレーターの場合、nは負数の場合がある。nが負数の場合、n回operator --を呼び出すことになる。

    - +

    双方向イテレーター用のコードはランダムアクセスイテレーターでも動くが非効率的だ。

    今使っているイテレーターの種類を把握して適切な方法を選ぶコードを書くのは面倒だ。そこで標準ライブラリには、イテレーター iをn回移動してくれるadvance(i, n)がある。

    - +

    nが正数の場合は前方(i+1の方向)に、nが負数の場合は後方(i-1の方向)に、それぞれn回移動させる。

    advance(i,n)はi自体が書き換わる。

    - +

    distance( first, last ): firstからlastまでの距離

    イテレーター firstからlastまでの距離を求めたいとする。

    ランダムアクセスイテレーターならば以下のようにする。

    - +

    それ以外のイテレーターならば、firstがlastと等しくなるまでoperator ++を呼び出す。

    - +

    これをやるのも面倒なので標準ライブラリがある。

    distance( first, last )はfirstからlastまでの距離を返す。

    - +

    ランダムアクセスイテレーターならばj - iと同じで、そうでなければiがjと等しくなるまでoperator ++を呼び出す。

    distanceに渡したイテレーターは変更されない。

    next/prev : 移動したイテレーターを返す

    advance(i, n)はイテレーターiを変更してしまう。イテレーターを変更させずに移動後のイテレーターも欲しい場合、以下のように書かなければならない。

    - -

    標準ライブラリのnext/prevは、引数に渡したイテレーターを変更せず、移動後のイテレーターを返してくれる。

    -

    prevはその逆だ。

    + auto j = i ; + std::advance( j, 3 ) ; + // jはiより3前方に移動している +} +

    標準ライブラリのnext/prevは、引数に渡したイテレーターを変更せず、移動後のイテレーターを返してくれる。

    -

    next/prevに第二引数を渡さない場合、前後に1だけ移動する。

    + auto j = std::next( i, 3 ) ; + // jはiより3前方に移動している +} +

    prevはその逆だ。

    -

    リバースイテレーター

    -

    イテレーターは要素を順番通りにたどる。例えば以下は要素を順番に出力する関数テンプレートprintだ。

    + auto j = std::prev( i, 3 ) ; + // jはiより3後方に移動している + // jはstd::advance(i, 3)した後のiと同じ値 +} +

    next/prevに第二引数を渡さない場合、前後に1だけ移動する。

    -

    逆順に出力するにはどうすればいいのだろうか。

    -

    双方向イテレーター以上ならば逆順にたどることはできる。すると逆順に出力する関数テンプレート’reverse_print’は以下のように書ける。

    - +

    リバースイテレーター

    +

    イテレーターは要素を順番通りにたどる。例えば以下は要素を順番に出力する関数テンプレートprintだ。

    + + for ( auto iter = first ; iter != last ; ++iter ) + std::cout << *iter ; +} +

    逆順に出力するにはどうすればいいのだろうか。

    +

    双方向イテレーター以上ならば逆順にたどることはできる。すると逆順に出力する関数テンプレートreverse_printは以下のように書ける。

    +

    しかしイテレーターを正順にたどるか逆順にたどるかという違いだけで、本質的に同じアルゴリズム、同じコードを二度も書きたくはない。そういうときに役立つのがリバースイテレーターだ。

    std::reverse_iterator<Iterator>はイテレーターIteratorに対するリバースイテレーターを提供する。リバースイテレーターはイテレーターのペア [first,last)を受け取り、lastの1つ前の要素が先頭でfirstの要素が末尾になるような順番のイテレーターにしてくれる。

    - -

    これで、printreverse_printのような本質的に同じコードを重複して書かずに済む。

    -

    リバースイテレーターはとても便利なので、std::vectorのような標準ライブラリのコンテナーには最初からネストされた型名としてリバースイテレーター::reverse_iteratorがある。リバースイテレーターを返すrbegin/rendもある。

    + // 54321 + std::for_each( first, last, + [](auto x ){ std::cout << x ; } ) ; +} +

    これで、printreverse_printのような本質的に同じコードを重複して書かずに済む。

    +

    リバースイテレーターはとても便利なので、std::vectorのような標準ライブラリのコンテナーには最初からネストされた型名としてリバースイテレーター::reverse_iteratorがある。リバースイテレーターを返すrbegin/rendもある。

    +

    動的メモリ確保

    概要

    動的メモリ確保は任意のサイズのメモリを確保できる機能だ。

    -

    例えば’std::vector’は任意個の要素を保持できる。

    - +

    例えばstd::vectorは任意個の要素を保持できる。

    +

    このプログラムは任意個のint型の値を保持する。いくつ保持するかはコンパイル時にはわからないし、実行途中にもわからない。プログラムが終了するまで、実際にいくつ値を保持したのかはわからない。

    このような事前にいくつの値を保持するかわからない状況では、動的メモリ確保を使う。

    malloc/free

    -

    malloc/freeはC言語から受け継いた素朴な動的メモリ確保のライブラリだ。

    - -

    `malloc(n)’はnバイトの生のメモリを確保して、その先頭バイトへのポインターを返す。

    - +

    malloc/freeはC言語から受け継いだ素朴な動的メモリ確保のライブラリだ。

    + +

    malloc(n)はnバイトの生のメモリを確保して、その先頭バイトへのポインターを返す。

    +

    これによって確保されるメモリは、1バイトごとのメモリが配列のように連続したメモリだ。型で書くと、std::byte [5]のようなものだ。

    確保したメモリはfreeで解放するまで有効だ。free(ptr)mallocが返したポインターptrを解放する。その結果、メモリはまた再びmallocによって再利用できるようになる。

    - +

    operator new/operator delete

    C++の追加した生のメモリを確保する方法が、operator newoperator deleteだ。

    - +

    使い方はmallocとほぼ同じだ。“operator new”までが名前なので少し混乱するが、通常の関数呼び出しと同じだ。

    - +

    グローバル名前空間であることを明示するために::を使っている。

    operator newで確保したメモリは、operator deleteで解放するまで有効だ。

    - +

    生のバイト列を基本的な型の値として使う方法。

    intdoubleのような基本的な型は、生のバイト列のポインターを型変換するだけで使える。

      @@ -13120,65 +13153,65 @@

      生の
    1. ポインターを型変換
    2. 値を代入
    - +

    int型のサイズはsizeof(int)バイトなので、sizeof(int)バイトのメモリを確保する。void *型からint *型に型変換する。あとはポインターを経由して使うだけだ。

    ポインターの文法がわかりにくい場合、リファレンスを使うこともできる。

    - +

    mallocoperator newが返すメモリの値は不定だ。なので、確保した生のメモリーへのポインターを、実際に使う型のポインターに型変換して、その値を参照しようとすると、結果は未定義だ。

    - +

    このプログラムを実行した結果、何が起こるかはわからない。

    メモリ確保の失敗

    メモリ確保は失敗する可能性がある。現実のコンピューターは有限のリソースしか持たないために、メモリも当然有限のリソースだ。

    mallocが失敗すると、nullptrが返される。mallocが失敗したかどうかを調べるには、戻り値をnullptrと比較すればよい。

    - -

    operator newが失敗すると、std::bad_allocが投げられる。

    -

    大抵の環境ではメモリ確保が失敗したときにできることは少ない。そのままプログラムを終了するのが最も適切な処理だ。というのも、ほとんどの処理にはメモリ確保が必要だからだ。

    -

    例外の場合、catchしなければプログラムは終了する。mallocの場合、自分でメモリ確保が失敗したかどうかを調べてプログラムを終了しなければならない。プログラムを途中で強制的に終了するには、std::abortが使える。

    -
    void f()
    +

    operator newが失敗すると、std::bad_allocが投げられる。

    + +

    大抵の環境ではメモリ確保が失敗したときにできることは少ない。そのままプログラムを終了するのが最も適切な処理だ。というのも、ほとんどの処理にはメモリ確保が必要だからだ。

    +

    例外の場合、catchしなければプログラムは終了する。mallocの場合、自分でメモリ確保が失敗したかどうかを調べてプログラムを終了しなければならない。プログラムを途中で強制的に終了するには、std::abortが使える。

    +

    クラス型の値の構築

    動的に確保したメモリをintdoubleのような基本的な型の値として使うには以下のように書けばよいことはすでに学んだ。

      @@ -13187,558 +13220,558 @@

      クラス型の値の構築

    1. 適切な値を代入

    より汎用的にテンプレートを使って書くと以下のようになる。

    - +

    この方法は、ほとんどのクラスには使えない。例えばstd::vector<T>には使えない。

    - +

    「ほとんどのクラス」と書いたからには、使えるクラスもあるということだ。例えば以下のようなクラスでは使える。

    - +

    なぜSimpleのようなクラスでは使えるのだろうか。std::vector<T>とはどう違うのか。この違いを厳密に解説するためには、とても長くて厳密なC++の標準規格の理解が必要だ。とても難しいため、本書では解説しない。

    クラスの値を使うためには、メモリ上にクラスのオブジェクトを構築する必要がある。クラスの構築にはコンストラクター呼び出し以外にも、そのメモリをクラスのオブジェクトとして使うのに必要な何らかの初期化が含まれる。

    - +

    生のメモリ上にクラスのような複雑な型を構築するには、newプレイスメントを使う。

    - +

    new初期化子というのは(){}で囲んだコンストラクターへの引数だ。引数がない場合は省略もできる。

    例えばstd::vector<int>型を構築するには以下のようにする。

    - +

    こうすればクラスが適切にメモリ上に構築され、コンストラクターも呼ばれる。コンストラクターが呼ばれることを確かめてみよう。

    - +

    このプログラムを実行すると、“Alice is constructed.”と出力される。

    クラスのオブジェクトを適切に破棄するためには、デストラクターを呼ばなければならない。通常の変数ならば、変数が寿命を迎えたときに自動的にデストラクターが呼ばれてくれる。

    - +

    このプログラムを実行すると、以下のように出力される。

    Alice is constructed.
     Bob is constructed.
     Bob is destructed.
     Alice is destructed.

    動的に確保されるメモリ上に構築されたオブジェクトは自動的に破棄されてくれない。クラスのオブジェクトの場合デストラクターを呼び出さなければならないが、動的メモリ確保したメモリ上に構築したクラスのオブジェクトの場合は、明示的に呼び出さなければならない。

    - +

    このようにすれば、コンストラクター、デストラクターが適切に呼ばれる。また確保したメモリも解放される。

    new/delete

    クラスのオブジェクトを動的確保するのに、生の文字列の確保/解放と、クラスのオブジェクトの構築/破棄をすべて自前で行うのは面倒だ。幸い、確保と構築、破棄と解放を同時にやってくれる機能がある。new式delete式だ。

    new 型 new初期化子
     delete ポインター

    new式は生のメモリを確保し、型のオブジェクトを構築し、型へのポインターを返す。

    - +

    delete式new式で返されたポインターの指し示すオブジェクトを破棄し、生のメモリを解放する。

    - +

    new式がメモリの確保に失敗すると、std::bad_alloc例外を投げる。

    - +

    配列版new/delete

    new式は配列型を動的確保することもできる。

    - +

    配列型をnew式で動的確保した場合、delete式は通常のdeleteではなく、delete[]を使わなければならない。

    - +

    スマートポインター

    クラスのオブジェクトの動的確保は、解放を明示的にしなければならないので間違いをしやすい。この問題はクラスを使って解決できる。

    クラスのコンストラクターで動的確保し、デストラクターで解放すればよいのだ。

    - +

    このクラスは様々な点で実用的ではない。例えばこのクラスはコピーできてしまう。

    - +

    このコードの何がまずいかというと、smart_ptr::ptrがコピーされてしまうということだ。p2が破棄されると、delete ptrが実行される。その後にp1が破棄されるのだが、もう一度delete ptrが実行されてしまうのだ。一度deleteを呼び出したポインターはもう無効になっているので、それ以上deleteを呼び出すことはできない。よってエラーになる。

    この問題を解決するには、まだ学んでいないC++の機能がたくさん必要になる。この問題は必要な機能をすべて学び終えた後の章で、もう一度挑戦することにしよう。

    -

    vectorの実装

    +

    vectorの実装 : 基礎

    クラス、ポインター、メモリ確保を学んだので、とうとうコンテナーの中でも一番有名なstd::vectorを実装する用意ができた。しかしその前に、アロケーターについて学ぶ必要がある。

    std::vectorstd::vector<T>のように要素の型Tを指定して使うので、以下のようになっていると思う読者もいるだろう。

    - -

    実際には以下のようになっている。

    +

    実際には以下のようになっている。

    +

    std::allocator<T>というのは標準ライブラリのアロケーターだ。アロケーターは生のメモリの確保と解放をするライブラリだ。デフォルトでstd::allocator<T>が渡されるので、普段ユーザーはアロケーターを意識することはない。

    std::vectormallocoperator newを直接使わずアロケーターを使ってメモリ確保を行う。

    アロケーターはテンプレートパラメーターで指定できる。何らかの理由で独自のメモリ確保を行いたい場合、独自のアロケーターを実装してコンテナーに渡すことができる。

    - +

    std::allocator<T>の概要

    std::allocator<T>T型を構築できる生のメモリを確保するための以下のようになっている。

    - +

    constexprというキーワードがあるが、ここでは気にする必要はない。あとで学ぶ。

    重要なのはメモリ確保をするallocateと、メモリ解放をするdeallocateだ。

    std::allocator<T>の使い方

    標準ライブラリのアロケーター、std::allocator<T>は、T型を構築できる生のメモリの確保と解放をするライブラリだ。重要なメンバーは以下の通り。

    - +

    allocate(n)はT型のn個の配列を構築できるだけの生のメモリを確保してその先頭へのポインターを返す。

    deallocate(p, n)allocate(n)で確保されたメモリを解放する。

    - -

    allocateには[[nodiscard]]という属性がついている。これにより戻り値を無視すると警告が出る。

    -

    確保されるのが生のメモリだということに注意したい。実際にT型の値として使うには、newによる構築が必要だ。

    +

    allocateには[[nodiscard]]という属性がついている。これにより戻り値を無視すると警告が出る。

    + std::allocator<int> a ; + // 警告、戻り値が無視されている + a.allocate(1) ; + + // OK + int * p = a.allocate(1) ; +}
    +

    確保されるのが生のメモリだということに注意したい。実際にT型の値として使うには、newによる構築が必要だ。

    +

    このように書くのはとても面倒だ。特にstd::stringの明示的なデストラクター呼び出しs->basic_stringが面倒だ。なぜs->stringではだめなのか。

    実はstd::stringは以下のようなクラステンプレートになっている。

    - +

    本当のクラス名はbasic_stringなのだ。

    普段は使っているstd::stringというのは、以下のようなエイリアスだ。

    - +

    明示的なデストラクター呼び出しにエイリアスは使えないので、本当のクラス名であるbasic_stringを直接指定しなければならない。

    この問題はテンプレートで解決できる。

    - +

    このようにテンプレートで書くことによって、クラス名を意識せずに破棄ができる。

    - +

    このようなコードを書くのは面倒なので、標準ライブラリにはstd::destory_atがある。また、これらをひっくるめたあロケーターを使うためのライブラリであるallocator_traitsがある。

    std::allocator_traits<Alloc>

    std::allocator_traits<Alloc>はアロケーターAllocを簡単に使うためのライブラリだ。

    allocator_traits<Alloc>はアロケーターの型Allocを指定して使う。

    - -

    と書くかわりに、

    +int * p = a.allocate(1) ; +

    と書くかわりに、

    +

    と書く。

    これはとても使いづらいので、allocator_traitsのエイリアスを書くとよい。

    - +

    これもまだ書きにくいので、decltypeを使う。decltype(expr)は式exprの型として使える機能だ。

    - +

    decltypeを使うと以下のように書ける。

    - +

    allocator_traitsはアロケーターを使った生のメモリの確保、解放と、そのメモリ上にオブジェクトを構築、破棄する機能を提供している。

    - +

    T型のN個の配列を構築するには、まずN個の生のメモリを確保し、

    - +

    N回の構築を行う。

    - -

    破棄もN回行う。

    -
    for ( auto i = p + N, first = p ; i != first ; --i )
    +
    +

    破棄もN回行う。

    +

    生のメモリを破棄する。

    - +

    簡易vectorの概要

    準備はできた。簡易的なvectorを実装していこう。以下が本書で実装する簡易vectorだ。

    - +

    これだけの簡易vectorでもかなり便利に使える。

    例えば要素数を定めて配列のようにアクセスできる。

    - +

    イテレーターも使える。

    - +

    要素を際限なく追加できる。

    - +

    classとアクセス指定

    簡易vectorの概要では、まだ学んでいない機能が使われていた。classpublicprivateだ。

    -

    C++のクラスにはアクセス指定がある。public:private:だ。アクセス指定書かれた後、別のアクセス指定が現れるまでの間のメンバーは、アクセス指定の影響を受ける。

    - -

    publicメンバーはクラスの外から使うことができる。

    +

    C++のクラスにはアクセス指定がある。public:private:だ。アクセス指定が書かれた後、別のアクセス指定が現れるまでの間のメンバーは、アクセス指定の影響を受ける。

    -

    privateメンバーはクラスの外から使うことができない。

    +public : + // publicなメンバー + int public_member1 ; + int public_member2 ; +private : + // privateなメンバー + int private_member1 ; + int private_member2 ; +public : + // 再びpublicなメンバー + int public_member3 ; +} ;
    +

    publicメンバーはクラスの外から使うことができる。

    -

    コンストラクターもアクセス指定の対象になる。

    + C c; + // クラスの外から使う + c.data_member = 0 ; + c.member_function() ; +} +

    privateメンバーはクラスの外から使うことができない。

    -

    この例では、C::C(int)はpublicメンバーなのでクラスの外から使えるが、C::C(double)はprivateメンバーなのでクラスの外からは使えない。

    -

    privateメンバーはクラスの中から使うことができる。クラスの中であればどのアクセス指定のメンバーからでも使える。

    +

    コンストラクターもアクセス指定の対象になる。

    -

    privateメンバーの目的はクラスの外から使ってほしくないメンバーを守ることだ。例えば以下のようにコンストラクターでnewしてデストラクターでdeleteするようなクラスがあるとする。

    - +

    この例では、C::C(int)はpublicメンバーなのでクラスの外から使えるが、C::C(double)はprivateメンバーなのでクラスの外からは使えない。

    +

    privateメンバーはクラスの中から使うことができる。クラスの中であればどのアクセス指定のメンバーからでも使える。

    + -

    もしdynamic_int::ptrがpublicメンバーだった場合、以下のようなコードのコンパイルが通ってしまう。

    - +

    privateメンバーの目的はクラスの外から使ってほしくないメンバーを守ることだ。例えば以下のようにコンストラクターでnewしてデストラクターでdeleteするようなクラスがあるとする。

    + +private : + int * ptr ; +public : + dynamic_int( int value = 0 ) + : ptr( new int(value) ) + { } + ~dyamic_int() + { + delete ptr ; + } +} ; +

    もしdynamic_int::ptrがpublicメンバーだった場合、以下のようなコードのコンパイルが通ってしまう。

    +

    このプログラムがdynamic_intのデストラクターを呼ぶと、main関数のローカル変数のポインターに対してdeleteを呼び出してしまう。これは未定義の挙動となる。

    外部から使われては困るメンバーをprivateメンバーにすることでこの問題はコンパイル時にエラーにでき、未然に回避できる。

    クラスを定義するにはキーワードとしてstructもしくはclassを使う。

    - +

    違いはデフォルトのアクセス指定だ。

    structはデフォルトでpublicとなる。

    - -

    classはデフォルトでprivateとなる。

    -
    class bar
    +
    +

    classはデフォルトでprivateとなる。

    +

    structclassの違いはデフォルトのアクセス指定だけだ。アクセス指定を明示的に書く場合、違いはなくなる。

    ネストされた型名

    std::vectorには様々なネストされた型名がある。

    - +

    自作の簡易vectorでstd::vectorと同じようにネストされた型名を書いていこう。

    要素型に関係するネストされた型名

    - -

    本物のstd::vectorとは少し異なるが、ほぼ同じだ。要素型がvalue_typeで、あとは要素型のポインター、constポインター、リファレンス、constリファレンスがそれぞれエイリアス宣言される。

    -

    アロケーター型もallocator_typeとしてエイリアス宣言される。

    + using value_type = T ; + using pointer = T *; + using const_pointer = const pointer; + using reference = value_type & ; + using const_reference = const value_type & ; +} ;
    +

    本物のstd::vectorとは少し異なるが、ほぼ同じだ。要素型がvalue_typeで、あとは要素型のポインター、constポインター、リファレンス、constリファレンスがそれぞれエイリアス宣言される。

    +

    アロケーター型もallocator_typeとしてエイリアス宣言される。

    +

    size_typeは要素数を表現する型だ。

    - +

    通常std::size_tが使われる。

    - +

    difference_typeはイテレーターのdifference_typeと同じだ。これはイテレーター間の距離を表現する型だ。

    - +

    通常std::ptrdiff_tが使われる。

    - +

    イテレーターのエイリアス。

    - +

    今回実装する簡易vectorでは、ポインター型をイテレーター型として使う。std::vectorの実装がこのようになっている保証はない。

    reverse_iteratorconst_reverse_iteratorはリバースイテレーターだ。

    簡易vectorのデータメンバー

    @@ -13750,291 +13783,949 @@

    簡易vectorのデータメンバ
  • アロケーター
  • これを素直に考えると、ポインター1つ、整数2つ、アロケーター1つの4つのデータメンバーになる。

    - -

    確かにstd::vectorはこのようなデータメンバーでも実装できる。しかし多くの実装では以下のようなポインター3つとアロケーター1つになっている。

    +

    確かにstd::vectorはこのようなデータメンバーでも実装できる。しかし多くの実装では以下のようなポインター3つとアロケーター1つになっている。

    +

    このように実装すると、現在有効な要素数はlast - firstで得られる。確保したストレージのサイズはreserved_last - firstだ。ポインターで持つことによってポインターが必要な場面でポインターと整数の演算を必要としない。

    効率的な実装はC++が実行される環境によっても異なるので、すべての環境に最適な実装はない。

    簡単なメンバー関数の実装

    簡易vectorの簡単なメンバー関数を実装していく。ここでのサンプルコードはすべて簡易vectorのクラス定義の中に書いたかのように扱う。例えば

    - +

    とある場合、これは、

    - +

    のように書いたものとして考えよう。

    イテレーター

    簡易vectorは要素の集合を配列のように連続したストレージ上に構築された要素として保持する。したがってイテレーターは単にポインターを返すだけでよい。

    まず通常のイテレーター

    - +

    これは簡単だ。iterator型は実際にはT *型へのエイリアスだ。このメンバー関数は例外を投げないのでnoexceptを指定する。

    vectorのオブジェクトがconstの場合、begin/endconst_iteratorが返る。

    - -

    これを実現するには、メンバー関数をconst修飾する。

    - +

    これを実現するには、メンバー関数をconst修飾する。

    +

    すでに学んだようにconst修飾はthisポインターを修飾する。オブジェクトのconst性によって、適切な方のメンバー関数が呼ばれてくれる。

    -

    簡易vectorでの実装は単にconst就職するだけだ。

    - +

    簡易vectorでの実装は単にconst修飾するだけだ。

    +

    constではないvectorのオブジェクトからconst_iteratorがほしいときに、わざわざconstなリファレンスに変換するのは面倒なので、const_referenceを返すcbegin/cendもある。

    - +

    この実装はメンバー関数名以外同じだ。

    - +

    std::vectorにはリバースイテレーターを返すメンバー関数rbegin/rendcrbegin/crendがある。

    - -

    beginに対するrbeginの実装は以下のようになる。残りは自分で実装してみよう。

    - + +

    beginに対するrbegin/rendの実装は以下のようになる。crbegin/crendは自分で実装してみよう。

    +

    return文でT{e}という形の明示的な型変換を使っている。これには理由がある。

    C++では引数が1つしかないコンストラクターを変換コンストラクターとして特別に扱う。

    例えば以下は数値のように振る舞うNumberクラスの例だ。

    - +

    このNumberは初期値をコンストラクターで取る。そのとき、int型、double型、はては文字列で数値を表現したstd::string型まで取る。この3つのコンストラクターは引数が1つしかないため変換コンストラクターだ。

    クラスは変換コンストラクターの引数の型から暗黙に型変換できる。

    例えばNumberクラスを引数に取る関数があると、

    - +

    変換コンストラクターの型の値を渡せる。

    - +

    intやdoubleやstd::stringはNumberではないが、変換コンストラクターによって暗黙に型変換される。

    戻り値として返すときにも変換できる。

    - -

    しかし、場合によってはこのような暗黙の型変換を行いたくないこともある。そういう場合、コンストラクターにexplicitキーワードをつけると、暗黙の型変換を禁止させることができる。

    - +

    しかし、場合によってはこのような暗黙の型変換を行いたくないこともある。そういう場合、コンストラクターにexplicitキーワードをつけると、暗黙の型変換を禁止させることができる。

    +

    実はstd::reverse_iterator<Iterator>のコンストラクターにもexplicitキーワードがついている。

    - +

    explicitキーワード付きの変換コンストラクターを持つクラスは、暗黙の型変換ができないので、明示的に型変換しなければならない。

    +

    容量確認

    +

    std::vectorには容量を確認するメンバー関数がある。

    + +

    早速実装していこう。

    +

    sizeは要素数を返す。イテレーターの距離を求めればよい。

    + +

    イテレーターライブラリを使ってもよい。本物のstd::vectorでは以下のように実装されている。

    + +

    emptyは空であればtrue、そうでなければfalseを返す。「空」というのは要素数がゼロという意味だ。

    + +

    しかしsize() == 0というのは、begin() == end()ということだ。なぜならば要素数が0であれば、イテレーターのペアはどちらも終端のイテレーターを差しているからだ。本物のstd::vectorでは以下のように実装されている。

    + +

    capacityは、追加の動的メモリ確保をせずに追加できる要素の最大数を返す。これを計算するには、動的確保したストレージの末尾の1つ次のポインターであるデータメンバーであるreserved_lastを使う。最初の要素へのポインターであるfirstからreserved_lastまでの距離が答えだ。ポインターの距離はイテレーターと同じく引き算する。

    + +

    要素アクセス

    +

    operator []

    +

    std::vectoroperator []相当のものを簡易vectorにも実装しよう。

    + +

    operator []は非const版とconst版の2種類がある。

    + +

    at

    +

    メンバー関数at(i)operator [](i)と同じだが、範囲外のインデックスを指定した場合、std::out_of_rangeが例外として投げられる。

    + +

    実装はインデックスをsize()と比較して、範囲外であればstd::out_of_rangeをthrowする。operator []と同じく、非const版とconst版がある。

    + +

    front/back

    +

    front()は先頭要素へのリファレンスを返す。

    +

    back()は末尾の要素へのリファレンスを返す

    + +

    これにもconst版と非const版がある。vectorlastが最後の要素の次のポインターを指していることに注意。

    + +

    data

    +

    data()は先頭の要素へのポインターを返す。

    + +

    実装はfirstを返すだけだ。

    + +

    vectorの実装 : メモリ確保

    +

    メモリ確保の起こるタイミング

    +

    std::vectorはどこでメモリを確保しているのだろうか。

    +

    デフォルト構築すると空になる。

    + +

    コンストラクターに要素数を渡すことができる。

    + +

    するとstd::vectorは指定した要素数の有効な要素をもつ。

    +

    コンストラクターに要素数と初期値を渡すことができる。

    + +

    すると、指定した要素数で、要素の値はすべて初期値になる。

    +

    vectorのオブジェクトを構築した後でも、メンバー関数resize(size)で要素数をsize個にできる。

    + +

    resizeで要素数が増える場合、増えた要素の初期値も指定できる。

    + +

    resizeで要素数が減る場合、末尾が削られる。

    + +

    メンバー関数push_back(value)を呼び出すと要素数が1増え、要素の末尾の要素が値valueになる。

    + +

    reserve(size)は少なくともsize個の要素が追加の動的メモリ確保なしで追加できるようにメモリを予約する。

    + +

    この章ではここまでの実装をする。

    +

    デフォルトコンストラクター

    +

    簡易vectorのデフォルトコンストラクターは何もしない。

    + +

    何もしなくてもポインターはすべてnullptrで初期化され、アロケーターもデフォルト構築されるからだ。

    +

    これで簡易vectorの変数を作れるようになった。ただしまだ何もできない。

    + +

    アロケーターを取るコンストラクター

    +

    std::vectorのコンストラクターは最後の引数にアロケーターを取れる。

    + +

    これを実装するには、アロケーターを取ってデータメンバーにコピーするコンストラクターを書く。

    + +

    他のコンストラクターはこのコンストラクターにまずデリゲートすればよい。

    + +

    要素数と初期値を取るコンストラクターの実装

    +

    要素数と初期値を取るコンストラクターはresizeを使えば簡単に実装できる。

    + +

    しかしこれは実装をresizeに丸投げしただけだ。resizeの実装をする前に、実装を楽にするヘルパー関数を実装する。

    +

    ヘルパー関数

    +

    ここではvectorの実装を楽にするためのヘルパー関数をいくつか実装する。このヘルパー関数はユーザーから使うことは想定しないので、privateメンバーにする。

    + +

    ネストされた型名traits

    +

    アロケーターはallocator_traitsを経由して使う。実際のコードはとても冗長になる。

    + +

    この問題はエイリアス名を使えば解決できる。

    + +

    allocate/deallocate

    +

    allocate(n)はアロケーターからn個の要素を格納できる生のメモリの動的確保をして先頭要素へのポインターを返す。

    +

    deallocate(ptr)はポインターptrを解放する。

    + +

    construct/destroy

    +

    construct(ptr)は生のメモリへのポインターptrvectorvalue_type型の値をデフォルト構築する。

    +

    construct(ptr, value)は生のメモリへのポインターptrに値valueのオブジェクトを構築する。

    + +

    destroy(ptr)ptrの指すオブジェクトを破棄する。

    + +

    destroy_all/destroy_until

    +

    destroy_all()vectorの要素を末尾から先頭に向けて順番に破棄する。

    +

    std::vectorの初期化では、要素は先頭から末尾に向けて順番に構築される。C++では破棄は構築の逆順に行われるので、std::vectorの破棄にあたっては、要素は末尾から先頭に向けて順番に破棄される。

    + +

    このコードでは、v[0], v[1], v[2]の順番に要素が構築され、v[2], v[1], v[0]の順番で破棄される。

    +

    destroy_allの実装は、この次に説明するdestroy_untilを使う。

    + +

    destroy_until(rend)は、vectorが保持するrbegin()からリバースイテレーターrendまでの要素を破棄する。リーバスイテレーターを使うので、要素の末尾から先頭に向けて順番に破棄される。

    + +

    &*riterはやや泥臭い方法だ。簡易vector<T>iteratorは単なるT *だが、riterはリバースイテレーターなのでポインターではない。ポインターを取るために*riterでまずT &を得て、そこに&を適用することでT *を得ている。

    +

    デストラクター

    +

    ヘルパー関数を組み合わせることでデストラクターが実装できるようになった。

    +

    std::vectorのデストラクターは、

    +
      +
    1. 要素を末尾から先頭に向かう順番で破棄
    2. +
    3. 生のメモリを解放する
    4. +
    +

    この2つの処理はすでに実装した。デストラクターの実装は単にヘルパー関数を並べて呼び出すだけでよい。

    + +

    reserveの実装

    +

    reserveの実装は生の動的メモリを確保してデータメンバーを適切に設定する。

    +

    ただし、いろいろと考慮すべきことが多い。

    +

    現在のcapacityより小さい要素数がreserveされた場合、無視してよい。

    + +

    すでに指定された要素数以上に予約されているからだ。

    +

    動的メモリ確保が行われていない場合、単に動的メモリ確保をすればよい。

    + +

    「おそらく」というのは、C++の規格はvectorのデフォルトコンストラクターが予約するストレージについて何も言及していないからだ。すでに要素数10000を超えるストレージが予約されている実装も規格準拠だ。本書で実装しているvectorは、デフォルトコンストラクターでは動的メモリ確保をしない実装になっている。

    +

    有効な要素が存在する場合、その要素の値は引き継がなければならない。

    + +

    つまり動的メモリ確保をした後に、既存の要素を新しいストレージにコピーしなければならないということだ。

    +

    まとめよう。

    +
      +
    1. すでに指定された要素数以上に予約されているなら何もしない
    2. +
    3. まだ動的メモリ確保が行われていなければ動的メモリ確保をする
    4. +
    5. 有効な要素がある場合は新しいストレージにコピーする。
    6. +
    + +

    resize

    +

    resize(sz)は要素数をsz個にする。

    + +

    resizeは呼び出し前より要素数を増やすことも減らすこともある。また変わらないこともある。

    +

    要素数が増える場合、増えた要素数の値はデフォルト構築された値になる。

    + +

    このプログラムを実行すると、“default constructed.”は5回標準出力される。

    +

    resize(sz, value)resizeを呼び出した結果要素が増える場合、その要素をvalueで初期化する。

    + +

    要素数が減る場合、要素は末尾から順番に破棄されていく。

    + +

    このプログラムを実行すると、以下のように出力される。

    + +

    最初のv.resize(2)で、v[4], v[3], v[2]が書いた順番で破棄されていく。main関数を抜けるときに残りのv[1], v[0]が破棄される。

    +

    resize(sz)を呼び出したときにszが現在の要素数と等しい場合は何もしない。

    + +

    まとめるとresizeは以下のように動作する。

    +
      +
    1. 現在の要素数より少なくリサイズする場合、末尾から要素を破棄する
    2. +
    3. 現在の要素数より大きくリサイズする場合、末尾に要素を追加する
    4. +
    5. 現在の要素数と等しくリサイズする場合、何もしない。
    6. +
    +

    実装しよう。

    + +

    要素を破棄する場合、破棄する要素数だけ末尾から順番に破棄する。

    +

    要素を増やす場合、reserveを呼び出してメモリを予約してから、追加の要素を構築する。

    +

    sz == size()の場合は、どちらのif文の条件にも引っかからないので、何もしない。

    +

    size(sz, value)は、追加の引数を取るほか、construct( iter )の部分がconstrcut( iter, value )に変わるだけだ。

    + +

    これで自作のvectorはある程度使えるようになった。コンストラクターで要素数を指定できるし、リサイズもできる。

    + +

    push_back

    +

    push_backvectorの末尾に要素を追加する。

    + +

    push_backの実装は、末尾の予約された未使用のストレージに値を構築する。もし予約された未使用のストレージがない場合は、新しく動的メモリ確保する。

    +

    追加の動的メモリ確保なしで保持できる要素の個数はすでに実装したcapacity()で取得できる。push_backは要素をひとつ追加するので、size() + 1 <= capacity()ならば追加の動的メモリ確保はいらない。逆に、size() + 1 > capacity()ならば追加の動的メモリ確保をしなければならない。追加の動的メモリ確保はすでに実装したreserveを使えばよい。

    + +

    これは動く。ただし、効率的ではない。自作のvectorを使った以下のような例を見てみよう。

    + +

    たった1つの要素を追加するのに、毎回動的メモリ確保と既存の全要素のコピーをしている。これは無駄だ。

    +

    std::vectorpush_backで動的メモリ確保が必要な場合、size()+1よりも多くメモリを確保する。こうすると、push_backを呼び出すたびに毎回動的メモリ確保と全要素のコピーを行う必要がなくなるので、効率的になる。

    +

    ではどのくらい増やせばいいのか。10個づつ増やす戦略は以下のようになる。

    + +

    しかしこの場合、以下のようなコードで効率が悪い。

    + +

    10個づつ増やす戦略では、この場合に1000回の動的メモリ確保と全要素のコピーが発生する。

    +

    上のような場合、vectorの利用者が事前にv.reserve(10000)とすれば効率的になる。しかし、コンパイル時に要素数がわからない場合、その手も使えない。

    + +

    よくある実装は、現在のストレージサイズの2倍のストレージを確保する戦略だ。

    + +

    size()0を返す場合もあるということに注意。単にreserve(size()*2)としたのではsize() == 0のときに動かない。

    +

    shrink_to_fit

    +

    shrink_to_fit()vectorが予約しているメモリのサイズを実サイズに近づけるメンバー関数だ。

    +

    本書で実装してきた自作のvectorは、push_back時に予約しているメモリがなければ、現在の要素数の2倍のメモリを予約する実装だった。すると以下のようなコードで、

    + +

    ユーザーが4万個のint型の値を入力した場合、65536個のint型の値を保持できるだけのメモリが確保されてしまい、差し引きsizeof(int) * 25536バイトのメモリが未使用のまま確保され続けてしまう。

    +

    メモリ要件の厳しい環境ではこのようなメモリの浪費を避けたい。しかし、実行時にユーザーから任意の個数の入力を受けるプログラムを書く場合には、push_backを使いたい。

    +

    こういうとき、shrink_to_fitはvectorが予約するメモリを切り詰めて実サイズに近くする、かもしれない。「かもしれない」というのは、C++の標準規格はshrink_to_fitが必ずメモリの予約サイズを切り詰めるよう規定してはいないからだ。

    +

    自作のvectorでは必ず切り詰める実装にしてみよう。

    +

    まず予約するメモリを切り詰めるとはどういうことか。現在予約しているメモリで保持できる最大の要素数はcapacity()で得られる。実際に保持している要素数を返すのはsize()だ。するとsize() == capacity()になるようにすればいい。

    + +

    shrink_to_fit()を呼んだとき、すでにsize() == capacity()trueである場合は、何もしなくてもよい。

    +

    それ以外の場合は、現在の有効な要素数文の新しいストレージを確保し、現在の値を新しいストレージにコピーし、古いメモリは破棄する。

    + +

    この実装はreserveと似ている。

    Cプリプロセッサー

    CプリプロセッサーはC++がC言語から受け継いだ機能だ。CプリプロセッサーはソースコードをC++としてパースする前に、テキストをトークン単位で変形する処理のことだ。この処理はソースファイルをC++としてパースする前処理として行われる。CプリプロセッサーはC++ではなく別言語として認識すべきで、そもそもプログラミング言語ではなくマクロ言語だ。

    C++ではCプリプロセッサーが広く使われており、今後もしばらくは使われるだろう。読者がC++で書かれた既存のコードを読む時、Cプリプロセッサーは避けて通れない。Cプリプロセッサーはいずれ廃止したい機能ではあるが、C++は未だに廃止できていない。

    Cプリプロセッサーはプリプロセッシングディレクティブ(preprocessing directive)を認識し、トークン列を処理する。ディレクティブはソースファイルの文頭に文字#から始まり、改行文字で終わる。#とディレクティブの間に空白文字を入れてもよい。

    - +

    #includeディレクティブ

    #includeは指定したファイルの内容をその場に挿入する。本質的にはコピペだ。C++では#includeはライブラリを利用するのに使われる。

    #includeは以下のいずれかの文法を持つ。

    - +

    #includeは指定したファイルパスのファイルの内容をその場所に挿入する。このファイルをヘッダーファイルという。<>によるファイルパスは、標準ライブラリやシステムのヘッダーファイルを格納したディレクトリーからヘッダーファイルを探す。""によるファイルパスは、システム以外のディレクトリーからもヘッダーファイルを探す。例えばカレントディレクトリーなどだ。

    例えば、以下のようなヘッダーファイルfoo.hがあり、

    - +

    以下のようなソースファイルbar.cppがある場合、

    - +

    bar.cppをCプリプロセッサーにかけると、以下のようなソースファイルが出力される

    - +

    このソースファイルはC++のソースファイルとしてはエラーとなるが、Cプリプロセッサーは単純にトークン列で分割したテキストファイルとしてソースファイルを処理するため、Cプリプロセッサーとしてはエラーにはならない。

    冒頭で述べたように、#includeの本質はコンパイラーによるコピペである。あるテキストファイルの内容をその場に挿入するコピペ機能を提供する。

    #includeは、他の言語でモジュール、importなどと呼ばれている機能を簡易的に提供する。C++の標準ライブラリを使うには、<iostream><string><vector>のようなヘッダーファイルを#includeする必要がある。

    - +

    すでに述べたように#includeはファイルの内容をその場に挿入するだけであり、他の言語にあるモジュールのための高級な機能ではない。本書を執筆時点で規格策定中のC++20では、より高級なモジュール機能を追加する予定がある。

    同じヘッダーファイルを複数回#includeすると、当然複数回挿入される。

    以下のようなval.hを、

    - +

    以下のように複数回#includeすると、

    - +

    以下のように置換される。

    - +

    これはvalの定義が重複しているためエラーとなる。

    しかし、ヘッダーファイルを一度しか#includeしないようにするのは困難だ。なぜならば、ヘッダーファイルは他のヘッダーファイルから間接的に#includeされることもあるからだ。

    - - - + + +

    このmain.cppをCプリプロセッサーにかけると以下のように置換される。

    - +

    これはvalの定義が重複しているためエラーとなる。

    この問題に対処するためには、複数回#includeされると困るヘッダーファイルでは、インクルードガード(include guard)と呼ばれている方法を使う。

    - -

    このように記述したval.hを複数回#includeしても、最初のifdefのみがコンパイル対象になるため、問題は起こらない。

    + +

    このように記述したval.hを複数回#includeしても、最初のifndefのみがコンパイル対象になるため、問題は起こらない。

    インクルードガードは以下の様式を持つ。

    - +

    十分にユニークなマクロ名は全ソースファイル中で衝突しないそのヘッダーに固有のマクロ名を使う。慣習的に推奨される方法としてはすべて大文字を使い、十分に長いマクロ名にすることだ。

    #define

    #defineはマクロ置換を行う。マクロにはオブジェクト風マクロ(object-like macro)と関数風マクロ(function-like macro)がある。風というのは、マクロはオブジェクトでも関数でもないからだ。ただ、文法上オブジェクトや関数の似ているだけで、実態はトークン列の愚直な置換だ。

    @@ -14042,76 +14733,76 @@

    オブジェクト風マクロ

    オブジェクト風マクロの文法は以下の通り

    #define マクロ名 置換リスト 改行文字

    #define以降の行では、マクロ名が置換リストに置き換わる

    - +

    これをプリプロセスすると以下のソースコードになる。

    - +

    マクロ名ONE1に置換される。

    マクロ名ONE_PLUS_ONEONE + ONEに置換される。置換された結果に別のマクロ名があれば、そのマクロ名も置換される。

    あるマクロ名を置換した結果、そのマクロ名が現れても再帰的に置換されることはない。

    - +

    これは以下のように置換される。

    - +

    マクロ名GNUを展開するとトークン`GNU’が現れるが、これは置換されたマクロ名と同じなので、再帰的に置換されることはない。

    関数風マクロ

    関数風マクロの文法は以下の通り。

    #define マクロ名( 識別子リスト ) 置換リスト 改行文字

    関数風マクロはあたかも関数のように記述できる。関数風マクロに実引数として渡したトークン列は、置換リスト内で仮引数としての識別子で参照できる。

    - +

    これは以下のように置換される。

    - +

    複数の引数を取るマクロへの実引数は、カンマで区切られたトークン列を渡す。

    - +

    これは以下のように置換される。

    - +

    ただし、括弧で囲まれたトークン列の中にあるカンマは、マクロの実引数の区切りとはみなされない。

    - +

    これは以下のように置換される。

    (a,b)

    __VA_ARGS__(可変長引数マクロ)

    #defineの識別子リストを...だけにしたマクロは、可変長引数マクロになる。このときマクロの実引数のトークン列は、置換リストのなかで__VA_ARGS__として参照できる。

    - +

    これは以下のように置換される。

    You can write , and ,, or even ,,,,

    カンマも含めてすべてのトークン列がそのまま__VA_ARGS__で参照できる。

    可変長引数マクロの識別子リストに仮引数と...を書いたマクロの置換リストでは、仮引数の数だけの実引数は仮引数で参照され、残りが__VA_ARGS__で参照される。

    - +

    これは以下のように置換される

    1 2 3 and 4,5,6

    X, Y, Zにそれぞれ1, 2, 3が入り、__VA_ARGS__には4,5,6が入る。

    __VA_OPT__

    __VA_OPT__は可変長引数マクロで__VA_ARGS__にトークン列が渡されたかどうかで置換結果を変えることができる。

    __VA_OPT__は可変引数マクロの置換リストでのみ使える。__VA_OPT__(content)__VA_ARGS__にトークンがない場合はトークンなしに置換され、トークンがある場合はトークン列contentに置換される。

    - +

    これは以下のように置換される。

    f( 1 )
     f( 1, 2 )
    @@ -14121,193 +14812,193 @@

    __VA_OPT__

    #演算子

    #はマクロ実引数を文字列リテラルにする。

    #は関数風マクロの置換リストの中のみで使うことができる。#は関数風マクロの仮引数の識別子の直前に書くことができる。#が直前に書かれた識別子は、マクロ実引数のトークン列の文字列リテラルになる。

    - +

    これは以下のように置換される。

    "hello"
     "hello world"

    また、可変長マクロと組み合わせた場合、

    - +

    以下のように置換される。

    - +

    ##演算子

    ##はマクロ実引数の結合を行う。

    ##は関数風マクロの置換リストの中にしか書けない。##は両端にマクロの仮引数の識別子を書かなければならない。##は両端の識別子の参照するマクロ実引数のトークン列を結合した置換を行う。

    - +

    これは以下のように置換される。

    foobar
     aaa bbbccc ddd

    結合した結果のトークンは更にマクロ置換の対象となる。

    - +

    これは以下のように置換される。

    hello

    CONCAT(FOO,BAR)FOOBARに置換され、FOOBARという名前のマクロ名があるためにさらにhelloに置換される。

    複数行の置換リスト

    #defineディレクティブの置換リストは複数行に渡って書くことができない。これは文法上の制約によるものだ。#defineディレクティブは改行文字で終端される。

    しかし、関数やクラスを生成するような複雑なマクロは、複数行に分けて書きたい。

    - +

    この場合、行末にバックスラッシュに続けて改行を書くと、バックスラッシュと改行がプリプロセッサーによって削除される。

    上の例は以下のように、プリプロセッサーとしては比較的わかりやすく書くことができる。

    - +

    C++ではテンプレートがあるために、このようなマクロを書く必要はない。

    #undefディレクティブ

    #undefはそれ以前に定義されたマクロを削除する。

    - +

    これは以下のように置換される。

    BAR
     FOO

    条件付きソースファイル選択

    -

    #if, #elif, #else, #endif, #ifdef, #ifndefは条件付きのソースファイルの選択(conditinal inclusion)を行う。これは条件付きコンパイルに近い機能を提供する。

    +

    #if, #elif, #else, #endif, #ifdef, #ifndefは条件付きのソースファイルの選択(conditional inclusion)を行う。これは条件付きコンパイルに近い機能を提供する。

    プリプロセッサーの定数式

    プリプロセッサーで使える条件式は、C++の条件式とは比べてだいぶ制限がある。基本的には整数定数式で、true, falseが使える他、123, 1+1, 1 == 1, 1 < 1のような式も使える。ただし、識別子はすべてマクロ名として置換できるものは置換され、置換できない識別子は、true, false以外はキーワードも含めてすべて0に置換される。

    したがって、プリプロセッサーで以下のように書くと、

    - +

    以下のように書いたものと同じになる。

    - +

    プリプロセッサーであるので、C++としてのconstexpr変数やconstexpr関数も使えない。

    - +

    これは以下のように置換される。

    constexpr int x = 1 ;

    プリプロセッサーはC++の文法と意味を理解しない。単にトークン列として処理する。

    以下の例はエラーになる。

    - +

    なぜならば、0()は整数定数式として合法なコードではないからだ。何度も言うように、プリプロセッサーはC++の文法と意味を理解しない。

    プリプロセッサーの定数式では、特殊なマクロ風の式を使うことができる。defined__has_includeだ。

    definedは以下の文法を持つ

    defined 識別子
     defined ( 識別子 )

    definedは識別子がそれ以前の行で#defineでマクロとして定義されていて#undefで取り消されていない場合1になり、それ以外の場合0になる。

    - +

    __has_includeは以下の文法を持つ。

    - +

    1番目と2番目は、指定されたヘッダーファイル名がシステムに存在する場合1に、そうでない場合0になる。

    - +

    3番目と4番目は、1番目と2番目が適用できない場合に初めて考慮される。その場合、まず通常通りにプリプロセッサーのマクロ置換が行われる。

    - +

    #ifディレクティブ

    #ifディレクティブは以下の文法を持つ。

    #if 定数式 改行文字
     
     #endif

    もし定数式がゼロの場合、#if#endifで囲まれたトークン列は処理されない。定数式が非ゼロの場合、処理される。

    - +

    これをプリプロセスすると以下のようになる。

    - +

    #if 0は処理されないので、#endifまでのトークン列は消える。

    #elifディレクティブ

    #elifディレクティブは、C++でいうelse ifに相当する。

    - +

    #elifディレクティブは#ifディレクティブと#endifディレクティブの間に複数書くことができる。#elifのある#ifが処理される場合、#ifから#elifの間のトークン列が処理される、#ifが処理されない場合、#elif#ifと同じように定数式を評価して処理されるかどうかが判断される。#elifが処理される場合、処理されるトークン列は次の#elifもしくは#endifまでの間のトークン列になる。

    以下の例は、すべてYESのトークンがある行のみ処理される。

    - +

    プリプロセスした結果は以下の通り、

    YES
     YES
    @@ -14317,25 +15008,25 @@ 

    #elseディレクティブ

    #elseディレクティブはC++でいうelseに相当する。

    #elseディレクティブは#ifディレクティブと#endifディレクティブの間に書くことができる。もし#if#elifディレクティブが処理されない場合で#elseディレクティブがある場合、#elseから#endifまでのトークン列が処理される。

    以下の例は、YESのトークンがある行のみ処理される。

    - +

    #ifdef, #ifndefディレクティブ

    #ifdef 識別子
     #ifndef 識別子
    @@ -14343,62 +15034,62 @@

    #ifdef, #ifndefディレクティブ<
    #if defined 識別子
     #if !defined 識別子

    例、

    - +

    #lineディレクティブ

    #lineディレクティブはディレクティブの次の行の行番号と、ソースファイル名を変更する。これは人間が使うのではなく、ツールによって生成されることを想定した機能だ。

    以下の文法の#lineディレクティブは、#lineディレクティブの次の行の行番号をあたかも数値で指定した行番号であるかのように振る舞わせる。

    #line 数値 改行文字

    数値として0もしくは2147483647より大きい数を指定した場合の挙動は未定義となる。

    以下の例はコンパイルエラーになるが、コンパイルエラーメッセージはあたかも102行目に問題があるかのように表示される。

    - +

    以下の例は999を出力するコードだ。

    - +

    以下の文法の#lineディレクティブは、次の行の行番号を数値にした上で、ソースファイル名をソースファイル名にする。

    #line 数値 "ソースファイル名" 改行文字

    例、

    - +

    以下の文法の#lineディレクティブは、プリプロセッサートークン列をプリプロセスし、上の2つの文法のいずれかに合致させる。

    - +

    例、

    - +

    #errorディレクティブ

    #errorディレクティブはコンパイルエラーを引き起こす。

    #error 改行文字
     #error トークン列 改行文字

    #errorによるコンパイラーのエラーメッセージには#errorのトークン列を含む。

    #errorの利用例としては、#ifと組み合わせるものがある。以下の例はCHAR_BITが8でなければコンパイルエラーになるソースファイルだ。

    - +

    #ifが処理されなければ、その中にある#errorも処理されないので、コンパイルエラーにはならない。

    -

    #pragmra

    -

    #pragmraディレクティブは実装依存の処理を行う。#pragmaはコンパイラー独自の拡張機能を追加する文法として使われている。

    +

    #pragma

    +

    #pragmaディレクティブは実装依存の処理を行う。#pragmaはコンパイラー独自の拡張機能を追加する文法として使われている。

    文法は以下の通り。

    #pragma プリプロセッサートークン列 改行文字

    C++では属性が追加されたために、#pragmaを使う必要はほとんどなくなっている。

    @@ -14472,30 +15163,30 @@

    単一のソースフ

    ヘッダーファイルはコピペ

    すでに、ソースファイルの他にヘッダーファイルというファイルも使っている。ヘッダーファイルはソースファイルではない。コンパイル前にソースファイルにコピペされるだけのものだ。

    例えば以下のような内容のheader.hというヘッダーファイルがあるとして、

    - -

    soruce.cppが以下のようであるとき、

    - + +

    source.cppが以下のようであるとき、

    +

    source.cppをコンパイルすると、まずヘッダーファイルが以下のように展開される。

    - +

    ヘッダーファイルとはこれだけのものだ。コンパイラーが#includeされた場所に、ヘッダーファイルの中身を愚直にコピペするだけだ。

    複数のソースファイルのコンパイル

    2つのソースファイル、foo.cppbar.cppからなるプログラムをコンパイルするには、

    @@ -14510,7 +15201,7 @@

    オブジェクトファイル

    GCCではC++コンパイラーの名前はg++で、リンカーの名前はldだ。ただし、C++のオブジェクトファイルをリンクするのにリンカーを直接使うことはない。g++ldを適切に呼び出してくれるからだ。

    ソースファイルsource.cppをコンパイルしてオブジェクトファイルを生成するには、-cオプションを使う。

    $ g++ -c source.cpp
    -

    生成されるオブジェクトファイルの名前はソースファイルの名前の拡張子を.oに置き換えたものになる。上のコマンドをじっこうした結果、オブジェクトファイルsouce.oが生成される。

    +

    生成されるオブジェクトファイルの名前はソースファイルの名前の拡張子を.oに置き換えたものになる。上のコマンドを実行した結果、オブジェクトファイルsource.oが生成される。

    生成したオブジェクトファイルは、g++の入力として使うことで、リンクしてプログラムにすることができる。g++は裏でリンカーldを適切に呼び出してくれる。

    $ g++ -o program source.o

    オブジェクトファイル名を別の名前にしたい場合は、-o object-file-nameオプションを使う。

    @@ -14547,190 +15238,190 @@

    複数のソースファイ

    C++のひとつのソースファイルは、1つの翻訳単位(translation unit)として扱われる。別の翻訳単位の定義を使うには、様々な制約がある。具体的な例で学ぼう。

    関数

    以下のコードを見てみよう。

    - +

    このコードには2つの定義がある。print_intmainだ。

    関数print_intを別のソースファイルであるprint_int.cppに分割してみよう。

    - +

    このコードは問題なくコンパイルできる。

    $ g++ -c print_int.cpp

    すると残りのソースファイルをmain.cppとすると以下のようになる。

    - +

    このコードはコンパイルできない。なぜならば、C++では名前は使う前に宣言しなければならないからだ。

    関数を宣言するには、関数の本体以外の部分を書き、セミコロンで終端する。

    - +

    これでコンパイル、リンクができるようになった。

    $ g++ -c main.cpp
     $ g++ -o program main.o print_int.o

    このとき、main.cppで関数print_intを定義することはできない。

    - +

    C++では定義は全翻訳単位にひとつしか書くことができないルール、ODR(One Definition Rule、単一定義原則)があるからだ。

    - +

    なぜODRがあるのか。なぜ定義はひとつしか書けないのか。理由は簡単だ。もし定義が複数書けるならば、異なる定義を書くことができてしまうからだ。

    - +

    もし定義を複数書くことができる場合、この関数ftrueを返すべきだろうか。それともfalseを返すべきだろうか。

    この問題を防ぐために、C++にはODRがある。

    複数のソースファイル、つまり複数の翻訳単位からなるプログラムの場合でもODRは適用される。定義はすべての翻訳単位内でひとつでなければならない。

    引数リストが違う関数は別の関数で、別の定義になる。

    - +

    名前は使う前に宣言が必要だが、肝心の定義は別のソースファイルに書いてある。宣言と定義を間違えてしまった場合はエラーになる。

    - +

    このような間違いを防ぐためのお作法として、宣言はヘッダーファイルに書いて#includeする。 ~~~c++ // print_int.h bool print_int( int x ) ;

    // main.cpp #include “print_int.h”

    int main() { // 間違えない bool result = print_int( 123 ) ; } ~~~

    変数

    変数にも宣言と定義がある。通常、変数の宣言は定義を兼ねる。

    - +

    そのため、別の翻訳単位の変数を使うために変数を書くと、定義が重複してしまい、ODR違反になる。

    - +

    変数を定義せずに宣言だけしたい場合は、externキーワードを使う。

    - +

    externキーワードを名前空間スコープで宣言された変数に使うと、定義せずに別の翻訳単位の定義を参照する意味になる。

    変数の場合も、間違いを防ぐためにヘッダーファイルに書いて#includeするとよい。

    - +

    インライン関数/インライン変数

    変数や関数の定義はODRにより重複できない。ということはヘッダーファイルに書いて複数の翻訳単位で#includeできないということだ。

    - +

    library.hには宣言だけを書いて、別途翻訳単位となるソースファイル、例えばlibrary.cppを用意しなければならない。

    - +

    小さなライブラリの場合、この制約は煩わしい。できればヘッダーファイルだけで済ませてしまいたい。このためにC++には特別なODRを例外的に回避する方法がある。

    キーワードinlineをつけて定義した関数と変数は、インライン関数、インライン変数となる。

    - +

    インライン関数とインライン変数は、複数の翻訳単位で重複して定義できる。

    - +

    inlineはODRを例外的に回避できるとは言え、強い制約がある。

    1. 異なる翻訳単位に限る

    同じ翻訳単位の中で重複することはできない。

    - +
    1. 同じトークン列である
    @@ -14741,50 +15432,50 @@

    インライン関数/イン
  • 意味が同じである。
  • 同じトークン列でも意味が異なることがある。

    - +

    foo.cppのインライン関数gf(int)を呼び出すが、bar.cppのインライン関数gf(double)を呼び出す。インライン関数gのトークン列はどちらも同じだが、意味が異なる。

    ODRの例外的な回避の怖いところは、間違えてしまってもコンパイラーがエラーメッセージを出してくれる保証がないところだ。上の同じトークン列で違う意味のような関数は、そのままコンパイルが通ってリンクされ、実行可能なプログラムが生成されてしまうかも知れない。そのようなプログラムの挙動がどうなるかはわからない。この理由は、ODR違反を完全に発見するコンパイラーの実装が技術的に困難だからだ。ODR違反をしないのはユーザーの責任だ。

    インライン変数とインライン関数はわざわざ翻訳単位を分けて分割コンパイルするまでもないライブラリに使うとよい。

    クラス

    クラスにも宣言と定義がある。

    - +

    クラスを複数の翻訳単位で使うには、関数と同じように宣言と定義に分ければよいと考えるかも知れないが、残念ながらクラスの宣言だけでできることは少ない。

    クラスの宣言だけでできることは、クラス名を型名として使うとか、クラスのポインター型を作るぐらいのものだ。

    - +

    宣言だけされたクラスのオブジェクトを作ることはできないし、ポインターの演算もできない。

    - +

    この理由は、宣言だけされたクラスは不完全型(Incomplete type)という特別な扱いの型になるからだ。クラスのオブジェクトを作ったりポインター演算をするには、クラスのオブジェクトのサイズを決定する必要があるが、そのための情報はまだコンパイラーが得ていないために起こる制約だ。

    クラスの定義では、インライン変数やインライン関数と同じく、ODRの例外的な回避が認められている。条件も同じで、1. 異なる翻訳単位で、2. 同じトークン列で、3. 意味も同じ場合だ。

    ODR違反を起こさないために、クラス定義はインクルードファイルに書いて#includeするのがお作法だ。

    @@ -14815,162 +15506,162 @@

    クラス

    int value = foo.member_function() ; }

    クラス定義の中で定義されたメンバー関数は、自動的にインライン関数になる。

    - +

    このように書くと、ヘッダーファイルFoo.h#includeするだけでどこでもクラスFooが使えるようになる。メンバー関数を定義するためのFoo.cppは必要がなくなる。

    クラスのデータメンバーは具体的なオブジェクトではないので、インライン変数ではない。

    - +
    staticメンバー

    クラスのメンバーは非staticメンバーとstaticメンバーにわけることができる。staticメンバーはstaticキーワードをつけて宣言する。

    - +

    staticメンバー関数はクラスのオブジェクトには依存していない。そのため、クラスのオブジェクトなしで呼び出すことができる

    - +

    staticメンバー関数の呼び出しにクラスのオブジェクトを必要としない。そのため、thisも使うことはできない。

    - +

    staticデータメンバーはクラスのオブジェクトの外の独立したオブジェクトだ。staticデータメンバーのクラス定義内での宣言は定義ではないので、クラスの定義外で定義する必要がある。

    - +

    複数の翻訳単位からなるプログラムの場合、ODRにより定義はひとつしか書けないので、どこか1つのソースファイルだけに定義を書くことになる。

    - +

    これは面倒なので、通常はstatic変数はインライン変数にする。

    - +

    これでstatic変数を定義するだけのソースファイルを用意する必要はない。ただしインライン変数はC++17以降の機能なので、読者が昔のC++で書かれたコードを読む際には、まだ昔ながらのstaticデータメンバーの定義に出くわすだろうから、覚えておこう。

    staticメンバーはクラススコープの下に関数と変数というだけで、その実態は名前空間スコープ内の関数と変数と同じだ。

    - +

    テンプレート

    テンプレートにもODRの例外が認められている。

    テンプレートは具体的なテンプレート引数が与えられて実体化する。

    - +

    このため、翻訳単位ごとに、同じトークン列で同じ意味のテンプレートコードが必要だ。インクルードファイルに書いて#includeするお作法も同じだ。

    - +

    C++に将来的に追加される予定のモジュールが入るまでは、テンプレートコードはすべてをインクルードファイルに書いて#includeして使う慣習が続くだろう。

    デバッガー

    本書はこのあと、更に複雑な機能やアルゴリズムを解説していく。読者は複雑な機能やアルゴリズムを使おうとして、間違ったコードを書くことだろう。間違ったコードは直せばよい。問題は、どこが間違っているのかわからない場合だ。

    例えば以下のコードは1から10までの整数を標準出力するはずのプログラムだ。

    - +

    しかし実際に実行して見ると、1から9までの整数しか標準出力しない。なぜだろうか。

    読者の中にはコード中の問題のある箇所に気がついた人もいるだろう。これはたったの5行のコードで、問題の箇所も一箇所だ。これが数百行、数千行になり、関数やクラスを複雑に使い、問題の原因は複数の箇所のコードの実行が組み合わさった結果で、しかも自分で書いたコードなので正しく書いたはずだという先入観がある場合、たとえコードとしてはささいな間違いであったとしても、発見は難しい。

    こういうとき、実際にコードを一行ずつ実行したり、ある時点でプログラムの実行を停止させて変数の値を見たりしたいものだ。

    @@ -14983,15 +15674,15 @@

    デバッガー

    $ make clean

    GDBのチュートリアル

    では具体的にデバッガーを使ってみよう。以下のようなソースファイルを用意する。

    - +

    このプログラムをコンパイルする。

    $ g++ -g program.cpp -o program

    GDBを使ってプログラムのデバッグを始めるには、GDBのオプションとして“-g”オプション付きでコンパイルしたプログラムのファイル名を指定する。

    @@ -15075,7 +15766,7 @@

    GDBのチュートリアル

    (gdb) print $2 $4 = 10

    現在、プログラムは5行目を実行する手前で止まっている。このまま“continue”コマンドを使うとプログラムの終了まで実行されてしまう。もう一度1行だけ実行するには“break 6”で6行目にブレイクポイントを設定すればよいのだが、次の一行だけ実行したいときにいちいちブレイクポイントを設定するのは面倒だ。

    -

    そこで使うのが“step”だ。次の5行目を実行すると、変数valの値は11担っているはずだ。

    +

    そこで使うのが“step”だ。次の5行目を実行すると、変数valの値は11になっているはずだ。

    (gdb) step
     6       val *= 2 ;
     (gdb) print val
    @@ -15156,16 +15847,16 @@ 

    関数名へのブレイクポ
    (gdb) ファイル名:関数名

    と書く。

    C++では異なる引数で同じ名前の関数が使える。

    - +

    このようなプログラムで関数fにブレイクポイントを設定すると、fという名前の関数すべてにブレイクポイントが設定される。

    ブレイクポイントの一覧を表示する“info breakpoints”コマンドで確かめてみよう。

    (gdb) break f