diff --git a/000-preface.md b/000-preface.md index 44457aa..b566ad0 100644 --- a/000-preface.md +++ b/000-preface.md @@ -1,7 +1,20 @@ # 序 -本書はプログラミング経験者向けのC++入門書である。 +本書はプログラミングの経験はあるがC++は知らない読者を対象にしたC++を学ぶための本である。本書はすでに学んだことのみを使って次の知識を説明する手法で書かれた。C++コンパイラーをC++で書く場合、C++コンパイラーのソースコードをコンパイルする最初のC++コンパイラーをどうするかというブートストラップ問題がある。本書はいわばC++における知識のブートストラップを目指した本だ。これにより読者は本を先頭から読んでいけば、まだ学んでいない概念が突如として無説明のまま使われて混乱することなく読み進むことができるだろう。 -本書の対象読者は、すでに何らかの実用的なプログラミング言語を習得していることを想定し、プログラミングの初歩的な概念はすべて理解しているものとして説明しない。そのため、本書には、「変数は箱のようなものである」といったような説明は出てこない。ただし、主要なほかの言語とC++として特別に注意が必要な差は解説している。 +C++知識のブートストラップを意識した入門書の執筆はなかなかに難しかった。ある機能Xを教えたいが、そのためには機能Yを知っていなければならず、機能Yを理解するためには機能Zの理解が必要といった具合に、C++の機能の依存関係の解決をしなければならなかったからだ。著者自身も苦しい思いをしながらできるだけ今までに説明した知識のみを使って次の知識を教えるように牡蠣進めていった結果、意外な再発見をした。ポインターを教えた後はC++のほとんどの機能を教えることに苦労しなくなったのだ。結局C++では未だにポインターの機能は様々な機能の土台になっているのだろう。 +本書の執筆時点でC++は現在、C++20の規格制定に向けて大詰めを迎えている。C++20では`#include`に変わるモジュール、軽量な実行媒体であるコルーチン、高級なassert機能としてのコントラクトに加え、とうとうコンセプトが入る。ライブラリとしてもコンセプトを活用したレンジ、span、flat_mapなど様々なライブラリが追加される。その詳細は、次に本を出す機会があるならば江添亮の詳説C++17と似たようなC++20の参考書を書くことになるだろう。C++はまだまだ時代に合わせて進化する言語だ。 + + +本書の執筆はGitHub上で公開した状態で行われた。 + + + +本書のライセンスはGPLv3である。ただし、本書の著者近影はGPLv3ではなく撮影者が著作権を保持している。 + + +本書の著者近影の撮影は、著者の古くからの友人でありプロのカメラマンである三浦大に撮影してもらった。 + +三浦大のWebサイト: diff --git a/002-build.md b/002-build.md index b5645eb..78d71ff 100644 --- a/002-build.md +++ b/002-build.md @@ -466,7 +466,7 @@ GNU Makeはカレントディレクトリーにあるファイル`Makefile`の ~~~ $ touch source01 source02 source03 $ ls -Makefile source01 source03 source03 +Makefile source01 source02 source03 $ make cat source01 source02 source03 > source cat source > program diff --git a/003-guide-to-c++.md b/003-guide-to-c++.md index d1514ef..a8a071c 100644 --- a/003-guide-to-c++.md +++ b/003-guide-to-c++.md @@ -158,7 +158,6 @@ ccc ~~~cpp int main() { - // std::cout << "\\n is a new-line.\n"s ; } ~~~ diff --git a/004-debug-compile-error.md b/004-debug-compile-error.md index 54a53a1..e53b4dc 100644 --- a/004-debug-compile-error.md +++ b/004-debug-compile-error.md @@ -306,11 +306,11 @@ main.cpp:7:40: error: expected ‘;’ before ‘)’ token ~~~ std::cout << // 標準出力 f // 関数名 - ( // 空き括弧 + ( // 開き括弧 1+(2*3), // 第1引数 4-5, // 第2引数 6/(7-8) // 第3引数 - ) // 空き括弧に対応する閉じ括弧 + ) // 開き括弧に対応する閉じ括弧 ) // ??? ; // 終端文字 ~~~ diff --git a/007-standard-input.md b/007-standard-input.md index 1a84319..4412171 100644 --- a/007-standard-input.md +++ b/007-standard-input.md @@ -26,7 +26,7 @@ int main() double bmi = mass / (height*height) ; // BMIの出力 - std::cout << "BMI=" << bmi << "\n"s ; + std::cout << "BMI="s << bmi << "\n"s ; } ~~~ @@ -44,7 +44,7 @@ BMI 状態 ~~~cpp int main() { - // 身長1.6m + // 身長1.63m double height = 1.63 ; // 体重73kg double mass = 73.0 ; @@ -53,7 +53,7 @@ int main() double bmi = mass / (height*height) ; // BMIの出力 - std::cout << "BMI=" << bmi << "\n"s ; + std::cout << "BMI="s << bmi << "\n"s ; // 状態の判定をする関数 auto status = []( double bmi ) @@ -93,7 +93,7 @@ int main() double bmi = mass / (height*height) ; // BMIの出力 - std::cout << "BMI=" << bmi << "\n"s ; + std::cout << "BMI="s << bmi << "\n"s ; } ~~~ @@ -335,7 +335,7 @@ $ cat bodymass.txt 1.63 73 $ ./bmi < bodymass.txt > index.txt -$ cat index.text +$ cat index.txt 27.4756 ~~~ diff --git a/009-vector.md b/009-vector.md index 75ff34e..2aef288 100644 --- a/009-vector.md +++ b/009-vector.md @@ -440,7 +440,7 @@ for ( std::size_t head = 0 ; head != size ; ++head ) // 現在の先頭の次の要素から探すのでhead + 1 for ( std::size_t index = head + 1 ; index != size ; ++index ) { - if ( v.at(index) < v.at(min) + if ( v.at(index) < v.at(min) ) min = index ; } diff --git a/011-integer.md b/011-integer.md index 8151d63..c2aa4a7 100644 --- a/011-integer.md +++ b/011-integer.md @@ -119,7 +119,7 @@ int main() また、ビットという単位も扱いづらい。コンピューターは膨大な情報を扱うので、ビットをいくつかまとめたバイト(byte)を単位として情報を扱っている。1バイトが何ビットであるかは環境により異なる。本書では最も普及している1バイトは8ビットを前提にする。 -1ビットは2種類の状態を表現できるので、1バイトの中の8ビットは$2^8 = 256$種類の状態を表現できる。2バイトならば16ビットとなり、$2^16 = 65536$種類の状態を表現できる。 +1ビットは2種類の状態を表現できるので、1バイトの中の8ビットは$2^8 = 256$種類の状態を表現できる。2バイトならば16ビットとなり、$2^{16} = 65536$種類の状態を表現できる。 ### 1バイトで表現された整数 @@ -140,12 +140,13 @@ auto max = 0b11111111 ; 正数だけを表現するならば話は簡単だ。1バイトの整数は0から255までの値を表現できる。これを符号なし整数(unsigned integer)という。 + では負数を表現するにはどうしたらいいだろう。正数と負数を両方扱える整数表現のことを、符号付き正数(signed integer)という。1バイトは256種類の状態しか表現できないので、もし$-1$を表現したい場合、$-1$から254までの値を扱えることになる。 $-1$しか扱えないのでは実用的ではないので、負数と正数を同じ種類ぐらい表現したい。256の半分は128だが、1バイトで表現された整数は$-128$から128までを表現することはできない。0があるからだ。0を含めると、1バイトの整数は最大で$-128$から127までか、$-127$から128までを表現できる。どちらかに偏ってしまう。 -では実際に1バイトで負数も表現できる正数表現を考えてみよう。 +では実際に1バイトで負数も表現できる整数表現を考えてみよう。 #### 符号ビット diff --git a/013-names.md b/013-names.md index e7dab77..a692770 100644 --- a/013-names.md +++ b/013-names.md @@ -504,7 +504,7 @@ int f() { return 1 ; } int main() { - using namespace std ; + using namespace abc ; // エラー、名前が曖昧 f() ; @@ -523,7 +523,7 @@ int f() { return 1 ; } int main() { - using namespace std ; + using namespace abc ; // OK、名前空間abcのf abc::f() ; diff --git a/014-iterator.md b/014-iterator.md index 3caa026..00029f2 100644 --- a/014-iterator.md +++ b/014-iterator.md @@ -409,7 +409,7 @@ c) 2 \le i \le 12 $$ $$ -d) 2 \lt i \lt 13 +d) 1 \lt i \lt 13 $$ C++のイテレーターはa)を元にしている。 diff --git a/016-algorithm.md b/016-algorithm.md index e2611b4..63146a6 100644 --- a/016-algorithm.md +++ b/016-algorithm.md @@ -70,7 +70,7 @@ int main() } ~~~ -関数`printf_all`は便利だが、重要な処理がハードコードされている。例えば要素の集合のうち100以下の値だけ出力したいとか、値を2倍して出力したいとか、値を出力するたびに改行を出力したいという場合、それぞれに関数を書く必要がある。 +関数`print_all`は便利だが、重要な処理がハードコードされている。例えば要素の集合のうち100以下の値だけ出力したいとか、値を2倍して出力したいとか、値を出力するたびに改行を出力したいという場合、それぞれに関数を書く必要がある。 ~~~cpp // 値が100以下なら出力 @@ -969,6 +969,7 @@ int main() } ~~~ + `result`に代入されるのは関数`op`の戻り値だ。関数`op`は値を1つの引数受け取り値を返す関数だ。 ## replace @@ -1150,7 +1151,7 @@ int main() } ~~~ -`remove_if(first, last, pred)`は、`[first, last]`の範囲の要素を指すイテレーター`i`のうち、関数`pred`に渡した結果`pred(*i)`が`true`になる要素を取り除くアルゴリズムだ。 +`remove_if(first, last, pred)`は、`[first, last)`の範囲の要素を指すイテレーター`i`のうち、関数`pred`に渡した結果`pred(*i)`が`true`になる要素を取り除くアルゴリズムだ。 ~~~cpp int main() diff --git a/025-array-iterator.md b/025-array-iterator.md index 54837cb..6fcec18 100644 --- a/025-array-iterator.md +++ b/025-array-iterator.md @@ -34,7 +34,7 @@ namespace std { return c.end() ;} template < typename C > - auto begin( C const & c ) + auto end( C const & c ) { return c.end() ;} } ~~~ diff --git a/028-pointer-semantics.md b/028-pointer-semantics.md index 8552679..a9ec1ee 100644 --- a/028-pointer-semantics.md +++ b/028-pointer-semantics.md @@ -45,7 +45,7 @@ int main() int x = 0 ; // rはxを参照する - int r = x ; + int & r = x ; int y = 1 ; @@ -216,7 +216,7 @@ int * pointer = nullptr ; double * p1 = nullptr ; // std::stringへのポインター -std::string p2 = nullptr ; +std::string * p2 = nullptr ; ~~~ C言語とC++では歴史的な理由で、`nullptr`のほかにも`NULL`もnullポインター値 diff --git a/029-pointer-syntax.md b/029-pointer-syntax.md index da35e9e..5fd4685 100644 --- a/029-pointer-syntax.md +++ b/029-pointer-syntax.md @@ -607,7 +607,7 @@ using pointer_to_array_type = int (*)[5] ; int main() { int a[5] ; - pointer_to_function_type = &a ; + pointer_to_array_type ptr = &a ; } ~~~ diff --git a/030-pointer-details.md b/030-pointer-details.md index 631d7b9..f49411b 100644 --- a/030-pointer-details.md +++ b/030-pointer-details.md @@ -439,7 +439,7 @@ int main() 140722117900228 ~~~ -最初の値が`a0`, 次の値が`a3`, 最後の値が`a3`だ。 +最初の値が`a0`, 次の値が`a3`, 最後の値が`a1`だ。 筆者の環境では`sizeof(int)`は`4`だ。すると`a3`の値は`a0`の値より12多い値になっているはずだ。実際にそうなっている。`a1`は`a3`に対して8少ない値になっているはずだ。実際にそうなっている。 diff --git a/033-vector-implementation.md b/033-vector-implementation.md index e6ac951..d15bafd 100644 --- a/033-vector-implementation.md +++ b/033-vector-implementation.md @@ -187,7 +187,7 @@ void destroy_at( T * location ) destroy_at( s ) ; ~~~ -このようなコードを書くのは面倒なので、標準ライブラリには`std::destory_at`がある。また、これらをひっくるめたアロケーターを使うためのライブラリである`allocator_traits`がある。 +このようなコードを書くのは面倒なので、標準ライブラリには`std::destroy_at`がある。また、これらをひっくるめたアロケーターを使うためのライブラリである`allocator_traits`がある。 ## `std::allocator_traits` diff --git a/034-vector-memory-allocation.md b/034-vector-memory-allocation.md index 1fb2ef4..2a974a4 100644 --- a/034-vector-memory-allocation.md +++ b/034-vector-memory-allocation.md @@ -836,3 +836,69 @@ void shrink_to_fit() この実装は`reserve`と似ている。 +# vectorのその他のコンストラクター + +## イテレーターのペア + +`std::vector`はイテレーターのペアを取り、その参照する値で要素を初期化できる。 + +~~~cpp +int main() +{ + std::array a {1,2,3,4,5} ; + std::vector v( std::begin(a), std::end(a) ) ; + // vは{1,2,3,4,5} +} +~~~ + +これはすでに実装したメンバー関数を使えば簡単に実装できる。 + +~~~c++ +template < typename InputIterator > +vector( InputIterator first, InputIterator last, const Allocator & = Allocator() ) +{ + reserve( std::distance( first, last ) ; + for ( auto i = first ; i != last ; ++i ) + { + push_back( *i ) ; + } +} +~~~ + +## 初期化リスト + +`std::vector`は配列のように初期化できる。 + +~~~cpp +int main() +{ + std::vector v = {1,2,3} ; +} +~~~ + +このような初期化を*リスト初期化*と呼ぶ。 + +リスト初期化に対応するためには、`std::initializer_list`を引数に取るコンストラクターを追加する。 + +~~~c++ +template < typename T, Allocator = std::allocator > +{ +// コンストラクター +vector( std::initializer_list init, const Allocator & = Allocator() ) ; + // 省略... +} ; +~~~ + +`std::initializer_list`は`T`型の要素を格納する標準ライブラリで、`{a,b,c,...}`のようなリスト初期化で構築することができる。 + +~~~c++ +std::initializer_list init = {1,2,3,4,5} ; +~~~ + +`std::initializer_list`は`begin/end`によるイテレーターを提供しているので、すでに実装したコンストラクターにデリゲートすればよい。 + +~~~c++ +vector( std::initializer_list init, const Allocator & alloc = Allocator() ) ; + : vector( std::begin(init), std::end(init), alloc ) +{ } +~~~ diff --git a/036-move.md b/036-move.md index e7bb5c1..cc9e20c 100644 --- a/036-move.md +++ b/036-move.md @@ -279,4 +279,4 @@ C++ではコピーはコピー元を変更しないという慣習がある。 このため、C++はコピーのほかにムーブを定めている。ムーブを使うにはムーブ元の変数`x`を`std::move(x)`のようにしてコピーする。`std::move`はこのコピーはコピーではなくムーブしてもよいというヒントになる。 -ムーブを実装するためには、まず基礎知識として次の章で学ぶr`value`リファレンス、値カテゴリー、テンプレートのフォワードリファレンスの深い理解が必要になる。 +ムーブを実装するためには、まず基礎知識として次の章で学ぶ`rvalue`リファレンス、値カテゴリー、テンプレートのフォワードリファレンスの深い理解が必要になる。 diff --git a/041-move-support.md b/041-move-support.md index 73a1805..6357385 100644 --- a/041-move-support.md +++ b/041-move-support.md @@ -109,19 +109,19 @@ Integer & operator -=( const Integer & r ) 複合代入演算子をムーブ代入演算子として実装する理由は、通常はない。 -## 単行演算子 +## 単項演算子 -演算を表現するクラスでオーバーロードしたい単行演算子には`operator +`と`operator -`がある。特に`operator -`は実用上の意味があるので実装してみよう。 +演算を表現するクラスでオーバーロードしたい単項演算子には`operator +`と`operator -`がある。特に`operator -`は実用上の意味があるので実装してみよう。 ~~~cpp Integer a(10) ; auto b = -a ; // これは二項演算子 operator +の結果に -// 単行演算子operator -を適用 +// 単項演算子operator -を適用 auto c = -(a + a) ; ~~~ -`*this`が`lvalue`の場合の単行演算子の実装は以下のようになる。 +`*this`が`lvalue`の場合の単項演算子の実装は以下のようになる。 ~~~cpp Integer operator -() const @@ -200,9 +200,9 @@ c += a ; c.make_it_negative() ; ~~~ -こんなコードを書くのは面倒だ。単に`-(a+a)`と書いて効率的に動いてほしい。そのために単行演算子`operator -`をムーブに対応させる。 +こんなコードを書くのは面倒だ。単に`-(a+a)`と書いて効率的に動いてほしい。そのために単項演算子`operator -`をムーブに対応させる。 -単行演算子はクラスのメンバー関数として実装する。 +単項演算子はクラスのメンバー関数として実装する。 ~~~cpp class Integer @@ -357,7 +357,7 @@ struct X struct X { // エラー、リファレンス修飾子がない - void f() + void f() ; void f() & ; // OK、リファレンス修飾子がある @@ -393,7 +393,7 @@ public : } ; ~~~ -`rvalue`リファレンス修飾子を使った単行演算子`operator -`の実装は、`*this`自身が`rvalue`であるので、自分自身をムーブしている。ムーブ以降、`this->ptr`は`nullptr`になる。なぜならば、`Integer`のムーブ代入演算子がそのような実装になっているからだ。 +`rvalue`リファレンス修飾子を使った単項演算子`operator -`の実装は、`*this`自身が`rvalue`であるので、自分自身をムーブしている。ムーブ以降、`this->ptr`は`nullptr`になる。なぜならば、`Integer`のムーブ代入演算子がそのような実装になっているからだ。 ~~~cpp /// 上で示したのと同じムーブ代入演算子の抜粋 diff --git a/042-string-intro.md b/042-string-intro.md index 2cc968a..c63d799 100644 --- a/042-string-intro.md +++ b/042-string-intro.md @@ -48,7 +48,7 @@ null文字は整数の`0`に等しいという特別な特徴を持つ文字だ ASCIIはとても広く普及した文字のエンコード方法だ。ASCIIでは7ビットの整数値で1文字を表現する。 -C++の基本実行文字セットは特定の文字エンコードであると規定されていはいないが、ASCIIを参考にしている。ただしASCIIには基本実行文字セットにはない、ダラーサイン($)、アットマーク(@)、バッククオート(\`)といくつかの制御文字がある。 +C++の基本実行文字セットは特定の文字エンコードであると規定されてはいないが、ASCIIを参考にしている。ただしASCIIには基本実行文字セットにはない、ダラーサイン($)、アットマーク(@)、バッククオート(\`)といくつかの制御文字がある。 ### Unicode @@ -149,7 +149,7 @@ UTF-8は現在最も普及している文字コードだ。 C++プログラムが実行できるOSとしては以下のようなものがある。 + GNU/Linux -+ Andorid ++ Android + FreeBSD + DragonflyBSD + OpenBSD @@ -175,9 +175,9 @@ Microsoft WindowsはUTF-16を使用している。ただし、この状況はMic 通常の文字リテラルは単一引用符で1つの文字を囲む。 ~~~c++ -`a` -`b` -`c` +'a' +'b' +'c' ~~~ 通常の文字リテラルの型は`char`だ。 @@ -225,10 +225,10 @@ char c = '\\' ; ~~~c++ char oct = '\101' ; -char hex = "\x41" ; +char hex = '\x41' ; ~~~ -このコードは、8進数で`101`、16進数で`41`になる何らかの文字を表現している。もし通常の文字列リテラルがASCIIかUTF-8でエンコードされている場合、この文字は`A`になる。 +このコードは、8進数で`101`、16進数で`41`になる何らかの文字を表現している。もし通常の文字リテラルがASCIIかUTF-8でエンコードされている場合、この文字は`A`になる。 ### ユニバーサルキャラクター名 @@ -271,17 +271,18 @@ auto s = "abcdef" ; と書くのと同じだ。 -文字リテラルの中のエスケープシーケンスは対応する文字になる。 +文字列リテラルの中のエスケープシーケンスは対応する文字になる。 ~~~cpp "\n" ; ~~~ -という通常の文字リテラルは、バックスラッシュとラテンアルファベットnではなく、改行文字1文字になる。 +という通常の文字列リテラルは、バックスラッシュとラテンアルファベットnではなく、改行文字1文字になる。 通常の文字列リテラルは末尾にnull文字(`\0`)が付与される。このために、配列のサイズは文字数+1になる。 + 具体的な例では、`"abc"`という通常の文字列リテラルの型は``const char [4]`'になる。これは以下のような配列に等しい。 ~~~c++ @@ -657,9 +658,10 @@ class basic_string あるいは、配列のサイズを表現するために、配列の最後の要素の1つ次のポインターを使っているかもしれない。 -~~~cpp - CharT * ptr ; - CharT * last ; + +~~~c++ + charT * ptr ; + charT * last ; Allocator alloc ; ~~~ @@ -1171,7 +1173,7 @@ int main() std::string s ; s.size() ; s.resize(10) ; - s.clar() ; + s.clear() ; } ~~~ diff --git a/043-random.md b/043-random.md index 9d5eca6..65cd0b0 100644 --- a/043-random.md +++ b/043-random.md @@ -23,6 +23,7 @@ $ ./dice コンピューターで使われる乱数のほとんどは疑似乱数と呼ばれる方法で生成されている。さまざまなアルゴリズムがあるが、とても簡単に理解できる疑似乱数のアルゴリズムに、線形合同法(Linear congruential generator)がある。 + 線形合同法ではいまの乱数を$X_n$、次の乱数$X_{n+1}$とすると、$X_{n+1}$は以下のように求められる。 $$ @@ -50,7 +51,7 @@ $$X_3 = 3 \times X_2 + 5 \bmod 2^{32}-1 = 65$$ ~~~cpp template < typename Engine > void f( Engine & e ) - +{ // 最小値 auto a = e.min() ; // 最大値 @@ -522,7 +523,7 @@ d.b() ; // 6 int main() { std::uniform_int_distribution a( 1, 6 ) ; - std::uniform_int_distributio b( 1, 6 ) ; + std::uniform_int_distribution b( 1, 6 ) ; std::mt19937 x ; @@ -551,7 +552,7 @@ a.reset() ; auto bool = ( a(x) == b(y) ) ; ~~~ -また、この内部状態を取り出すこともできる。内部状態はネストされた型名`param_type`で保持できる。内部状態を取り出すにはメンバー関数`param()`を呼び出す。分布クラスのコンストラクターこの`param_type`の値を渡すと、同じ内部状態の分布クラスを作り出すことができる。またメンバー関数`param(parm)`で`param_type`の値を渡して内部状態を設定することも可能だ。 +また、この内部状態を取り出すこともできる。内部状態はネストされた型名`param_type`で保持できる。内部状態を取り出すにはメンバー関数`param()`を呼び出す。分布クラスのコンストラクターにこの`param_type`の値を渡すと、同じ内部状態の分布クラスを作り出すことができる。またメンバー関数`param(parm)`で`param_type`の値を渡して内部状態を設定することも可能だ。 ~~~cpp template < typename Distribution > @@ -941,7 +942,7 @@ std::geometric_distribution d( p ) ; `IntType`は整数型でデフォルトは`int`、`p`は確率で値の範囲は$0 < p < 1$だ。`p`の値の範囲に注意すること。0と1であってはならない。幾何分布は成功するまでベルヌーイ試行した回数を返すので、$p=0$の場合、必ず失敗するベルヌーイ試行になり意味がない。$p=1$のときは必ず成功するベルヌーイ試行であり、やはり意味がない。 -`geometric_distribution`の生成する乱数の範囲にも注意が必要だ。生成される乱数$i$の範囲は$i \geq 0$だ。0もあり得る。0ということは、最初のベルヌーイ試行が成功したということだ。1は2回めのベルヌーイ試行が成功したということだ。幾何分布はベルヌーイ試行が初めて成功するまでのベルヌーイ試行の回数を返すので、成功したベルヌーイ試行は回数に含めない。 +`geometric_distribution`の生成する乱数の範囲にも注意が必要だ。生成される乱数$i$の範囲は$i \geq 0$だ。0もあり得る。0ということは、最初のベルヌーイ試行が成功したということだ。1は2回目のベルヌーイ試行が成功したということだ。幾何分布はベルヌーイ試行が初めて成功するまでのベルヌーイ試行の回数を返すので、成功したベルヌーイ試行は回数に含めない。 使い方。 diff --git a/045-random-part3.md b/045-random-part3.md index d832030..3f787c3 100644 --- a/045-random-part3.md +++ b/045-random-part3.md @@ -163,6 +163,7 @@ $$ std::fisher_f_distribution d( m, n ) ; ~~~ + `RealType`は浮動小数点数型でデフォルトは`dobule`。`m`, `n`は`RealType`型。値の範囲は$0 < m$ かつ $0 < n$。 使い方。 diff --git a/046-random-part4.md b/046-random-part4.md new file mode 100644 index 0000000..0538e73 --- /dev/null +++ b/046-random-part4.md @@ -0,0 +1,544 @@ +## サンプリング分布(sampling distributions) + +サンプリング分布(sampling distributions)とは、標本から分布の特徴が分かっている場合に、その特徴を指定することにより、望みの分布を作り出す分布のことだ。 + +### 離散分布(`std::discrete_distribution`) + +#### 簡単な説明 + +離散分布(discrete distribution)は整数型の乱数$i$, $0 \leq i < n$を返す分布だ。例えば$n = 10$ならば、$0,1,2,3,4,5,6,7,8,9$の10個のうちのいずれかの整数値を乱数として返す。この際、乱数値として取りうる整数値一つ一つに、確率を設定できる。確率は$p_0, \dotsc, p_{n-1}$で設定し、$p_0$が$0$の確率, $p_1$が$1$の確率...$p_{n-1}$が$n$の確率となる。それぞれの乱数$i$は確率$\frac{p_i}{S}$で出現する。このとき$S$とはすべての確率の合計、つまり$S = p_0 + \dotsc + p_{n-1}$となる。確率$p_i$は`double`型で与える。 + +たとえば、`{1.0, 1.0, 1.0}`という確率群を渡した場合、離散分布は$0, 1, 2$のいずれかの乱数をそれぞれ$\frac{1.0}{3.0}$の確率で返す。 + +もし、`{1.0, 2.0, 3.0}`という確率群を渡した場合、離散分布は$0, 1, 2$のいずれかの乱数を返す。その時の確率は、$1$が$\frac{1}{6}$、$2$が$\frac{1}{3}$、$3$が$\frac{1}{2}$だ。 + +例えば公平な6面ダイスを作りたい場合、`{1.0, 1.0, 1.0, 1.0, 1.0, 1.0}`を指定すると$0 \leq i \leq 5$までの6個の乱数$i$がそれぞれ$\frac{1}{6}$の確率で生成される。この結果に`+1`すると$1 \leq i \leq 6$の乱数を得ることができる。 + +6の目だけ2倍高い確率で出るイカサマ6面ダイスを作りたい場合、`{1.0, 1.0, 1.0, 1.0, 1.0, 2.0}`を指定すると、0から4までの5つの目は$\frac{1}{7}$の確率で出現し、5だけは$\frac{2}{7}$の確率で出る乱数を作ることができる。 + +$S$はすべての確率の合計で、それぞれの値は$\frac{p_i}{S}$の確率で出る。なので、以下はすべて分布だ。 + +~~~ +{1.0, 1.0, 1.0, 1.0, 1.0, 2.0} +{0.1, 0.1, 0.1, 0.1, 0.1, 0.2} +{2.0, 2.0, 2.0, 2.0, 2.0, 4.0} +~~~ + +#### 数学的な説明 + +`std::discrete_distribution`は整数型の乱数$i$, $0 \leq i < n$を以下の離散確率関数に従って分布する。 + +$$ +P(i \,|\, p_0, \dotsc, p_{n-1}) = p_i \text{ .} +$$ + +別に指定のない場合、分布パラメーターは$p_k = {w_k / S}$ for $k = 0, \dotsc, n - 1$として計算され、このとき値$w_k$は、一般に*ウエイト(weight)*と呼ばれていて、値は非負数、非NaN、非無限でなければならない。さらに、以下の関係が成り立たねばならない。$0 < S = w_0 + \dotsb + w_{n - 1}$ + +#### 変数の宣言 + +`std::discrete_distribution`の変数を宣言するには3つの方法がある。いずれも`double`型の値を`n`個渡すための方法だ。 + +##### イテレーターのペア + +変数の宣言。 + +~~~c++ +std::discrete_distribution d( firstW, lastW ) ; +~~~ + +`IntType`は整数型でデフォルトは`int`、`[firstW, lastW)`はイテレーターのペアで、`double`型に変換可能な値を参照している。 + +利用例。 + +~~~cpp +int main() +{ + std::array ps = {1.0, 2.0, 3.0} ; + std::discrete_distribution d( std::begin(ps), std::end(ps) ); + + std::mt19937 e ; + d(e) +} +~~~ + +##### 初期化リスト + +利用例。 + +~~~c++ +std::discrete_distribution d( {...} ) ; +std::discrete_distribution d = {...} ; +~~~ + +`...`には`double`型の浮動小数点数を指定する + +利用例。 + +~~~cpp +int main() +{ + std::discrete_distribution d( { 1.0, 2.0, 3.0 } ); + // もしくは + // ... d = { 1.0, 2.0, 3.0 } ; + + std::mt19937 e ; + d(e) +} +~~~ + +##### 個数、デルタ、関数 + +このコンストラクターは以下のように宣言されている。 + +~~~c++ +template + discrete_distribution( + size_t nw, + double xmin, double xmax, + UnaryOperation fw +); +~~~ + +`UnaryOperation`はひとつの実引数を取る関数オブジェクトで戻り値の型は`double`型に変換できること。さらに、`double`型は`UnaryOperation`の引数に変換可能なこと。もし$nw = 0$の場合は、$n = 1$とする。それ以外の場合、$n = \tcode{nw}$とする。このとき、$0 < \delta = (\tcode{xmax} - \tcode{xmin}) / n$となる関係が満たされなければならない。 + +もし$nw = 0$ならば$w_0 = 1$。それ以外の場合、$k = 0, \dotsc, n - 1$に対して、$w_k = \tcode{fw}(\tcode{xmin} + k \cdot \delta + \delta / 2)$とする。`fw`は`n`回を超えて呼ばれることはない。 + +~~~cpp +int main() +{ + std::discrete_distribution d( 5, 0.0, 1.0, [](auto x){ + std::cout << x << '\n' ; + if ( x < 0.3 ) + x = 0.3 ; + if ( x > 0.8 ) + x = 0.8 ; + return x ; + } ); +} +~~~ + +この`d`は、 + +~~~c++ +std::discrete_distribution d = {0.3, 0.3, 0.5, 0.7, 0.8 } ; +~~~ + +と初期化されたものと同じように初期化される。 + +#### 初期化パラメーターの確認 + +`std::discrete_distribution`の内部状態はメンバー関数`probabilities`で取得できる。戻り値の型は`std::vector`で、指定した確率群が要素になっている。 + +~~~cpp +int main() +{ + std::discrete_distribution d = { 1.0, 2.0, 3.0 } ; + auto v = d.probabilities() ; + // vは{1.0, 2.0, 3.0} +} +~~~ + +#### 応用例 + +以下は6の目が2倍の確率で出るイカサマ6面ダイスの実装だ。 + +~~~cpp +template < typename Engine > +int roll_dice( Engine & e ) +{ + std::discrete_distribution d = { 1.0, 1.0, 1.0, 1.0, 1.0, 2.0 } ; + return d(e) + 1 ; +} +~~~ + +### 区分定数分布(`std::piecewise_constant_distribution`) + +#### 簡単な説明 + +区分定数分布(piecewise constant distribution)とは、区分と、区分ごとの確率を指定し、いずれかの区分の範囲の値に一様分布させる分布だ。ここで言う確率は、密度、あるいはウエイトともいう。 + +ひとつの区分は`double`型の値2つ$b_i, b_{i+1}$で与える。このとき区分の乱数$x$の範囲は$[b_i, b_{i+1})$、もしくは$b_i \leq x < b_{i+1}$だ。$n$個の値を指定すると、$n-1$個の区分を指定したことになる。 + +例えば`{0.0, 1.0}`という2つの`double`型の値をつかって1つの区分を与えた場合、これは$0.0 \leq x < 1.0$という値の範囲の区分である。`{0.0, 1.0, 2.0}`という3つの`double`型の値は2つの区分になり、それぞれ$0.0 \leq x < 1.0$, $1.0 \leq x < 2.0$になる。 + +一般に、$n$個の`double`型の値$b_0, \dotsc, b_n$で$n-1$個の区分を表現する。このとき、$b_i < b_{i+1}$が$i = 0, \dotsc, n-1$までの$i$について成り立たなければならない。つまり区分を指定する`double`型の値は、後続の値より小さくなければならないということだ。 + +以下は正しい区分の指定だ。 + +~~~ +{1.0, 2.0, 100.0, 999.999} +{-1.0, 1.0, 2.0} +{-5.0, -4.0, -3.1} +~~~ + +以下は正しくない区分の指定だ。 + +~~~ +{1.0, 0.0} +~~~ + +これは$b_0 > b_1$なので正しくない。 + +それぞれの区分$[b_i, b_{i+1})$に対して確率$p_i$を`double`型で指定する。$n$個の$b_i$によって$n-1$個の区分を指定し、それぞれに対して1つずつ確率を設定するので、確率の数は$n-1$個だ。 + + +例えば`{0.0, 1.0}`という1つの区分と`{1.0}`という1つの確率を与えた場合、$0.0 \leq x < 1.0$の範囲の乱数$x$が生成される。 + +`{0.0, 1.0, 10.0}`という2つの区分と、`{1.0, 2.0}`という2つの確率を与えた場合、$\frac{1}{3}$の確率で$0.0 \leq x < 1.0$の範囲に一様分布した乱数になり、$\frac{2}{3}$の確率で$1.0 \leq x < 10.0$の範囲に一様分布した乱数になる。 + +#### 数学的な説明 + +`std::piecewise_constant_distribution`は浮動小数点数型の乱数$x$, $b_0 \leq x < b_n$を以下の確率密度関数に従って、それぞれの部分区間(subinterval)$[b_i, b_{i+1}$の間で一様に分布させる。 + +$$ +p(x \,|\, b_0, \dotsc, b_n, \; \rho_0, \dotsc, \rho_{n-1}) = \rho_i + \text{ , for $b_i \le x < b_{i+1}$.} +$$ + +この分布の区間境界(interval boundaries)ともいう$n+1$分布パラメーター$b_i$はすべての$i = 0, \dotsc, n - 1$に対して関係$b_i < b_{i + 1}$を満たさねばならない。別途指定なき場合、残りの$n$分布パラメーターは以下のように計算される。 + +$$ +\rho_k = \frac{w_k}{S \cdot (b_{k+1}-b_k)} \text{ for } k = 0, \dotsc, n - 1 \text{ ,} +$$ + +一般にウエイト(weight)と呼ばれている値$w_k$は、非負数、非NaN、非無限でなければならない。さらに、以下の関係を満たさなければならない。 + +$0 < S = w_0 + \dotsb + w_{n-1}$ + +#### 変数の宣言 + +`std::piecewise_constant_distribution`では、`double`型の値の集合を2つ渡す必要がある。ひとつは区間を指定するための$N$個の`double`型に変換可能な値で、もう一つは区間ごとの確率を指定するための$N-1$個の`double`型に変換可能な値だ。 + +##### イテレーターによる指定 + +イテレーターで区間と確率を指定するコンストラクターは以下の通り + +~~~c++ +template +piecewise_constant_distribution( + InputIteratorB firstB, InputIteratorB lastB, + InputIteratorW firstW +); +~~~ + +`[firstB, lastB)`は区間を指定するための$N$個の値を参照する入力イテレーターのペアだ。`firstW`はそれぞれの区間の確率を指定する$N-1$個の値を参照する入力イテレーターの先頭だ。`lastW`がないのは、確率の個数は$N-1$個であると分かっているからだ。 + +もし`[firstB, lastB)`のサイズが1以下の場合、区間は`[0.0, 1.0)`になり、確率は$\frac{1}{1}$になる。 + + +##### 利用例 + +~~~cpp +int main() +{ + std::array bs = {-1.0, 1.0, 2.0 } ; + std::array ps = { 1.0, 5.0 } ; + std::piecewise_constant_distribution d( std::begin(bs), std::end(bs), std::begin(ps) ) ; + + std::mt19937 e ; + d(e) ; +} +~~~ + +`bs`は区間を指定する値の集合、`ps`は区間ごとの確率だ。 + +区間は`[-1.0, 1.0)`と`[1.0, 2.0)`の2つ。確率はそれぞれ$\frac{1}{6}$、$\frac{5}{6}$だ。 + +区間を表現する値が足りない場合は以下の通り。 + +~~~cpp +int main() +{ + // 区間を指定すべき値が足りない + std::array bs = { 1.0 } ; + std::array ps = { 1.0, 5.0 } ; + // 引数は無視される。 + // 区間は[0.0, 1.0), 確率は100% + std::piecewise_constant_distribution d( std::begin(bs), std::end(bs), std::begin(ps) ) ; +} +~~~ + +##### 初期化リストと関数オブジェクトによる指定 + +初期化リストと関数を指定するコンストラクターは以下の通り。 + +~~~c++ +template +piecewise_constant_distribution( + initializer_list bl, + UnaryOperation fw +); +~~~ + +イテレーターのペアと同じく、区間は`[bl.begin(), bl.end())`で指定する。 + +確率は$k = 0, \dotsc, n - 1$について、$w_k = \tcode{fw}\bigl(\bigl(b_{k+1} + b_k\bigr) / 2\bigr)$とする。 + +`bl.size()`が1以下の場合、区間は`[0.0, 1.0)`になり、確率は$\frac{1}{1}$になる。 + + +##### 利用例 + +~~~cpp +int main() +{ + std::piecewise_constant_distribution d( + {1.0, 2.0, 3.0, 4.0, 5.0}, + []( auto x ) + { return x ; } + ) ; +} +~~~ + +この場合、区間は`[1.0, 2.0)`, `[2.0, 3.0)`, '[3.0, 4.0)', '[4.0, 5.0)'の4個になり、確率は`{1.5, 2.5, 3.5, 4.5}`となる。 + +##### 区間数、最小、最大、関数オブジェクトによる指定 + +コンストラクターの宣言 + +~~~c++ +template +piecewise_constant_distribution( + size_t nw, + RealType xmin, RealType xmax, + UnaryOperation fw +); +~~~ + +`nw`は区間数、`xmin`は最小値、`xmax`は最大値、`fw`は関数オブジェクトで、`double`型から変換できる型の実引数を取り、`double`型に変換可能な戻り値を返す。 + +$nw = 0$の場合、区間の個数$n$は$1$になる。それ以外の場合、$n = nw$となる。このとき関係、$0 < \delta = (\tcode{xmax} - \tcode{xmin}) / n$が成り立たなければならない。 + Let $b_k = \tcode{xmin} + k \cdot \delta $ for $ k = 0, \dotsc, n$, + and $w_k = \tcode{fw}(b_k + \delta / 2) $ for . + +$ k = 0, \dotsc, n - 1$において、区間は$b_k = \tcode{xmin} + k \cdot \delta $ for $ k = 0, \dotsc, n$とし、確率は$w_k = \tcode{fw}(b_k + \delta / 2) $とする。 + +##### 利用例 + +~~~cpp +int main() +{ + std::piecewise_constant_distribution d( 5, 1.0, 5.0, + []( auto x ) { return x ; } ) ; +} +~~~ + +この場合、区間の集合は`{1.0, 1.8, 2.6, 3.4, 4.2, 5.0}`となり、確率は`{1.4, 2.2, 3.0, 3.8, 4.6}`となる。 + +#### 内部状態の取得 + +`std::piecewise_constant_distribution`の内部状態は、メンバー関数`intervals`と`densities`で得ることができる。 + +~~~c++ +template +class piecewise_constant_distribution { +public : + vector intervals() const; + vector densities() const; +} ; +~~~ + +`intervals`は区間、densitiesは確率を返す。 + +~~~cpp +int main() +{ + auto bs = { 1.0, 2.0, 3.0 } ; + auto ps = { 1.0, 2.0 } ; + std::piecewise_constant_distribution d( std::begin(bs), std::end(bs), std::begin(ps) ) ; + + // {1.0, 2.0, 3.0} + auto intervals = d.intervals() ; + // {0.333333, 0.666667} + auto densities = d.densities() ; +} +~~~ + +`densities()`の結果が正規化されているのは、ユーザーが指定した確率は$w_k$だが、ここで返すのは$p_k$だからだ。 + +### 区分線形分布(`std::piecewise_linear_distribution`) + + +#### 簡単な説明 + +区分線形分布(piecewise linear distribution)は区分定数分布と同じく、区間と確率(又の名を密度、ウエイト)を指定する。 + +区間の指定は区分定数分布と同じだ。内部境界の集合で指定する。例えば`{1.0, 2.0, 3.0}`は2つの区間`[1.0, 2.0)`と`[2.0, 3.0)`を指定する。 + +区分線形分布における確率は、区間に対してではなく、内部境界に対して指定する。指定した全区間における値の出現確率は、内部境界から内部境界に向かって指定した確率の差の方向に線形に増加、もしくは減少する。 + +例えば区分`{0.0, 1.0}`と確率`{1.0, 2.0}`を指定した場合、これはひとつの区間`[0.0, 1.0)`について、内部境界`0.0`の確率は$\frac{1}{3}$、内部境界`1.0'の確率は`\frac{2}{3}`とし、$0.0 \leq x < 1.0$の範囲の乱数`x`を生成する。内部境界区間の範囲に注意。`1.0`未満なので。`1.0`は出ない。 + +そして、区間の間の値は、区間を区切る2つの内部境界の確率の差によって、線形に増加、もしくは減少する。例えば値`0.25`が出る確率は$\frac{1.25}{3}$、`0.5`が出る確率は$\frac{1.5}{3}$、値`1.75`がでる確率は$\frac{1.75}{3}$だ。 + +区分`{0.0, 1.0, 2.0}`と確率`{1.0, 2.0, 1.0}`の場合、2つの区間`[0.0, 1.0)`と`[1.0, 2.0)`の範囲について、`0.0`から`1.0`に向かう区間についての確率は$\frac{1}{4}$から$\frac{1}{2}$に増加し、`1.0`から`2.0`に向かう区間についての確率は$\frac{1}{2}$から$\frac{1}{4}$に減少する。 + +結果として、乱数値の分布をグラフに描画すると、`1.0`が最も出やすく、その前後±1.0の範囲で徐々に減少していく山のようなグラフになる。 + +TODO: グラフ、横軸が乱数値、縦軸が確率 + +~~~ + * + \frac{1}{2} + *** | + ***** | + ******* | + ********* | + *********** + \frac{1}{4} + *********** + *********** + *********** + *********** +-+----+----+ +0.0 1.0 2.0 +~~~ + +#### 数学的な説明 + +`std::piecewise_linear_distribution`は乱数$x$, $b_0 \leq x < b_n$を以下の確率密度関数に従って分布する。 + +$$ +p(x \,|\, b_0, \dotsc, b_n, \; \rho_0, \dotsc, \rho_n) + = \rho_{i} \cdot {\frac{b_{i+1} - x}{b_{i+1} - b_i}} + + \rho_{i+1} \cdot {\frac{x - b_i}{b_{i+1} - b_i}} + \text{ , for $b_i \le x < b_{i+1}$.} +$$ + +一般に*内部境界*とも呼ばれる$n + 1$分布パラメーター$b_i$は$i = 0, \dotsc, n - 1$において関係$b_i < b_{i+1}$ for $i = 0, \dotsc, n - 1$を満たさねばならない。別記する場合を除いて、残りの$n + 1$パラメーターは$k = 0, \dotsc, n$において$\rho_k = {w_k / S}$と計算される。このとき$w_k$は一般に境界におけるウエイト(weight at boundaries)と呼ばれ、非負数、非NaN、非無限でなければならない。さらに、以下の関係が成り立たねばならない。 + +$$ +0 < S = \frac{1}{2} \cdot \sum_{k=0}^{n-1} (w_k + w_{k+1}) \cdot (b_{k+1} - b_k) \text{ .} +$$ + +#### 変数の宣言 + +`piecewise_linear_distribution`は区間と確率を指定するために`n`個の`double`型に変換可能な値を指定する必要がある。 + +##### イテレーターによる指定 + +~~~c++ +template +piecewise_linear_distribution( + InputIteratorB firstB, InputIteratorB lastB, + InputIteratorW firstW ); +~~~ + +`[firstB, lastB)`は区間、`firstW`から区間数までのイテレーターが確立。 + +`firstB == lastB`もしくは`+firstB == lastB`の場合、つまり内部境界が1個以下で、空の場合、区間数はひとつで`[0.0, 1.0)`の範囲、確率は`{0.0, 1.0}`となる。 + +##### 使い方 + +~~~cpp +int main() +{ + auto bs = { 0.0, 1.0, 2.0 } ; + auto ps = { 1.0, 2.0, 1.0 } ; + std::piecewise_linear_distribution d( std::begin(bs), std::end(bs), std::begin(ps) ) ; + + std::mt19937 e ; + d(e) ; +} +~~~ + +空の場合。 + +~~~cpp +int main() +{ + auto bs = { 0.0 } ; + auto ps = { 0.0 } ; + std::piecewise_linear_distribution d( std::begin(bs), std::end(bs), std::begin(ps) ) ; +} +~~~ + +これは以下のコードと同じだ。 + +~~~cpp +int main() +{ + auto bs = { 0.0, 1.0 } ; + auto ps = { 0.0, 1.0 } ; + std::piecewise_linear_distribution d( std::begin(bs), std::end(bs), std::begin(ps) ) ; +} +~~~ + +##### 初期化リストと関数オブジェクトによる指定 + +~~~c++ +template +piecewise_linear_distribution( + initializer_list bl, + UnaryOperation fw +); +~~~ + +区間を指定する内部境界は`[bl.begin(), bl.end())`、内部境界$b_k$に対する確率$w_k$は$k = 0, \dotsc, n$について、$w_k = \tcode{fw}(b_k)$とする。 + +内部境界が1個以下の場合はイテレーターの場合と同じ。 + +##### 使い方 + +~~~cpp +int main() +{ + std::piecewise_linear_distribution d( + {0.0, 1.0, 2.0}, + [](auto x){ return x ; } + ) ; +} +~~~ + +これは以下のコード同じだ。 + +~~~cpp +int main() +{ + auto bs = { 0.0, 1.0, 2.0 } ; + auto ps = { 0.0, 1.0, 2.0 } ; + std::piecewise_linear_distribution d( std::begin(bs), std::end(bs), std::begin(ps) ) ; +} +~~~ + +##### 個数、最小値、最大値、関数オブジェクトによる指定 + +~~~c++ +template +piecewise_linear_distribution( + size_t nw, + RealType xmin, RealType xmax, + UnaryOperation fw +); +~~~ + +`nw`が個数、`xmin`が最小値、`xmax`が最大値、`fw`が関数オブジェクト。 + +関数オブジェクト`fw`は`double`型から変換できる実引数を1つだけとり、戻り値の型は`double`型に変換できること。 + +$\tcode{nw} = 0$ならば空であり、イテレーターの場合と同じ。 + +関係$0 < \delta = (\tcode{xmax} - \tcode{xmin}) / n$が成り立つこと。 + +内部境界$b_k$は$k = 0, \dotsc, n$について$b_k = \tcode{xmin} + k \cdot \delta$とする。確率$w_k$は$k = 0, \dotsc, n$について$w_k = \tcode{fw}(b_k)$とする。 + +##### 使い方 + +~~~cpp +int main() +{ + std::piecewise_linear_distribution d( + 5, + 1.0, 5.0, + [](auto x){ return x ;} + ) ; +} +~~~ + +上のコードは以下のコードと同じだ。 + +~~~cpp +int main() +{ + auto params = { 1.8, 2.6, 3.4, 4.2, 5.0, 5.8 } ; + std::piecewise_linear_distribution d( std::begin(params), std::end(params), std::begin(params) ) ; +} +~~~ diff --git a/docs/index.html b/docs/index.html index 3f550bf..31de250 100644 --- a/docs/index.html +++ b/docs/index.html @@ -458,6 +458,10 @@

江添亮のC++入門

  • shrink_to_fit
  • +
  • vectorのその他のコンストラクター
  • コピー
  • +
  • サンプリング分布(sampling distributions)
  • Cプリプロセッサー

    生文字列リテラル

    エスケープシーケンスは文法上の理由で直接ソースコード上に記述することができない文字を文字リテラルと文字列リテラルに記述できる機能だ。

    - +

    しかしエスケープシーケンスがあるために、バックスラッシュを普通に使うには、\\と書かなければならない。例えば上の文字列リテラルを改行文字に続いて「は改行文字」ではなく、本当に「」という文字列にしたい場合、以下のように書かなければならない。

    - +

    また、単一引用符'や二重引用符"もエスケープシーケンスが必要だ。

    - +

    また、以下のような内容の文字列をリテラルとして書きたい場合、

    foo
     bar
     baz

    以下のように書かなければならない。

    - +

    このようなわかりにくい記述ではなく、ソースコードに書いたままの文字列を文字列として扱いたい。そのための機能が生文字列リテラル(Raw String Literal)だ。

    生文字列リテラルは以下のような文法で書く

    - +

    例えば以下のように書くと、

    - +

    以下のような文字列リテラルと同じ意味になる。

    "foo\nbar\nbaz" ;

    エスケープシーケンスも書いたままに文字列となる。

    - +

    これは以下の文字列リテラルと同じ意味だ。

    - +

    文字列の表現方法

    文字列というのは文字型の配列で表現される。文字列を表現するには、配列の先頭へのポインターと配列のサイズが必要になる。

    null終端文字列

    C++の文字列リテラルは、末尾にnull文字が付与されたconstな文字型への配列だ。

    - +

    という文字列リテラルは型とその値としては

    - +

    になる。

    null終端文字列とはC言語から使われている文字列の表現方法だ。文字型の配列の末尾にnull文字を番兵として配置することで文字列の終端を表現している。C言語では文字列は文字型へのポインターとして表現される。ポインターが指す配列のサイズはわからないが、妥当な文字列はnull終端されているので、ポインターをインクリメントしていけばいずれnull文字が現れる。そこが文字列の終わりだ。これによって文字列のサイズもわかる。

    例えば、以下はC言語でよく書かれる典型的文字列を処理する関数だ。

    - +

    std::strlenはポインターが指し示すnull終端された配列のnull文字を除くサイズを返す。以下のような実装だ。

    - +

    ここで言う「文字列のサイズ」とは、ポインターが指し示す文字型の配列の要素数であって、文字数ではない。

    null終端文字列は文字型へのポインター1つだけなので取り回しがよい。ただし、文字列のサイズは実行時に文字列の先頭から末尾までイテレートして計算しなければならない。これは文字列の長さに比例したオーダーO(N)の処理量がかかる。

    std::basic_string

    今まで文字列の型として使ってきたstd::stringは、実はクラステンプレートで実装されている。

    - +

    テンプレートパラメーターのうち、charTが文字型、traitsは文字を処理するための補助的なライブラリ、Allocatorがアロケーターだ。

    これに対し、以下のようなエイリアスが存在する。

    - +

    それぞれの文字型に対応したbasic_stringのクラスだ。

    これに対して、ユーザー定義リテラルという機能を使い、文字列リテラルのサフィックスにsをつけることで、文字列リテラルを対応するbasic_stringのクラス型に変換できる。

    - +

    ユーザー定義リテラルの詳細については本書では詳しく説明しないが、演算子のオーバーロードと同じだ。演算子をオーバーロードするようにリテラル演算子をオーバーロードする。

    - +

    ユーザー定義リテラルを正しく実装するには複雑なルールがある。例えばユーザー定義のサフィックス名はアンダースコア1つから始まっていなければならないなどだ。

    - +

    これは将来の拡張のためにアンダースコアから始まらないサフィックス名をC++規格が予約しているためだ。

    basic_stringによる文字列の表現方法は、文字型配列の先頭要素へのポインター、文字型配列のサイズ、アロケーターだ。

    - +

    あるいは、配列のサイズを表現するために、配列の最後の要素の1つ次のポインターを使っているかもしれない。

    - +

    std::vectorと同じで、どちらのほうが効率がいいかはアーキテクチャにより異なる。

    basic_stringは文字列を表現するためのストレージを所有するクラスだ。コンストラクターでストレージを動的確保し、デストラクターで解放する。

    - +

    コピーはストレージの動的確保、ムーブはストレージの所有権の移動になる。

    - +

    std::basic_string_view

    basic_string_viewはストレージを所有しないクラスだ。以下のような宣言になる。

    - +

    その実装は文字型へのポインター2つか、文字型へのポインターひとつと配列のサイズを保持する整数型になる。

    - +

    もしくは、

    - +

    basic_string_viewにはbasic_stringと対になる各文字型に対する特殊化がある。

    - +

    さらに、各basic_stringに対するユーザー定義リテラルサフィックスsvがある。

    - +

    basic_string_viewは文字列がnull終端文字列とbasic_stringのどちらで表現されていても問題なく受け取るためのクラスだ。この2つの文字列の表現を別々に使う場合、文字列を受け取る関数は、

    - +

    のようにほとんど同じ関数を2つ書かなければならない。basic_string_viewを使えば、

    - +

    のように、どちらの文字列表現を使っても1つの関数を書くだけですむ。

    basic_string_viewはストレージを所有しないので関数の引数として使うときはリファレンスで取る必要はない。

    - +

    文字列の操作

    null終端文字列の操作

    null終端文字列は文字列の先頭となる文字型へのポインター型のオブジェクトひとつで表現されるので、文字型の配列のサイズを取得するにも、いちいちnull文字が見つかるまでポインターをインクリメントしていく必要がある。この処理をやってくれるのがstd::strlenだ。

    - +

    文字列リテラルの型はconstな文字型の配列なので、文字列を変更することができない。

    - +

    文字型への配列ならば変更できる。

    - +

    文字の長さを短くしたい場合は、終端をnull文字にする。

    - +

    この変数sの型はchar [4]だが、null終端文字列としてのサイズは1だ。

    文字列のサイズを長くするには、当然大きな配列が必要になる。

    - +

    このコードで、変数sは最終的に"abcdef"という文字列になる。最後のnull文字による終端を忘れてはならない。

    ここで、配列sの要素数は7以上でなければならない。最終的なnull終端文字列を表現するには最低でもchar [7]が必要だからだ。

    例えば2つのnull終端文字列を結合する場合で、どちらもconstであったり、十分なサイズがなかった場合、2つの文字列を保持できるサイズのメモリを確保して、コピーしなければならない。

    - +

    C言語の標準ライブラリにはnull終端文字列を扱うためのライブラリが多数ある。C言語の標準ライブラリを使えば、上のコードは以下のように書ける。

    - +

    basic_stringの操作

    basic_stringはストレージを所有するクラスだ。ストレージの解放と確保を自動でやってくれる上に、便利な操作がたくさんある。

    例えば上のconcat_strbasic_stringで実装すると以下のようになる。

    - +

    C++の作法に従って、引数s1, s2をnull終端文字列文字型ではなく、basic_string_viewにすると以下のようになる。

    - +

    初期化

    basic_stringはnull終端文字列、basic_string_viewbasic_stringで初期化、代入できる。

    - +

    結合

    basic_stringoperator +で文字列を結合できる。

    - +

    operator +=は第一オペランドを書き換える。

    - +

    basic_string::append(s)というメンバー関数もある。

    - +

    イテレーター

    basic_stringにはイテレーターがある。イテレーターの取得方法はstd::vectorと同じだ。

    - +

    これは以下のようにも書ける。

    - +

    部分文字列の検索

    イテレーターがあるので、basic_stringは汎用的なアルゴリズムに渡すことができる。例えばある文字列がその一部の別の文字列を含むかどうかを調べる場合、以下のように書ける。

    - +

    イテレーターを使うのは煩わしいが、C++20ではRangeライブラリが追加され、以下のように書ける予定だ

    - +

    名前空間を省くと、!empty( search( text, word) ) になるが、これでもまだ分かりづらい。そこでbasic_string::findがある。これは<algorithm>std::findとは別物で、文字列から部分文字列を探し、その部分文字列に一致する文字へのインデックスを返す。

    - +

    文字列"fox"に一致する部分文字列の先頭'f'の文字型の値へのインデックスは12で、"dog"'d'は36だ。この結果は、上のソースコードに使っている文字が1文字につき1文字型の値を使うためだ。通常は文字数と連続した文字型の要素へのインデックスは等しくならない。

    例えば以下のコードを実行すると、

    - +

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

    fox: 27
     dog: 45

    もし部分文字列が見つからない場合、basic_string::nposが返る。nposは“no position”という意味で、-1と等しい。

    - +

    この場合、変数textに文字列"abc"はないので、nposが返る。nposが返ったかどうかはnposと比較すればわかる。npos-1と等しいので、以下のようにも書ける。

    - +

    findの亜種として、rfindがある。

    findは最初の部分文字列を見つけるが、rfindは最後の部分文字列を見つける。

    - +

    findは最初に一致した部分文字列の先頭へのインデックスを返すので、この場合0が返る。rfindは最後に見つかった部分文字列の先頭へのインデックスを返すので、この場合10になる。

    C++20では、starts_with/ends_withという2つの便利なメンバー関数が追加される。

    starts_with(str)は文字列が部分文字列strで始まっている場合にtrueを返す。そうでない場合はfalseを返す。

    @@ -17663,142 +17744,142 @@

    部分文字列の検索

    bool b5 = text.starts_with("aaa"sv) ;

    } ~~

    ends_with(str)は文字列が部分文字列strで終わっている場合にtrueを返す。そうでない場合はfalseを返す。

    - +

    その他のメンバー関数

    size, empty, resize, capacity, reserve, shrink_to_fit, clearといったおなじみのメンバー関数もある。

    - +

    文字列の挿入

    文字列の挿入はinsert( pos, str ) で行える。

    posは挿入場所へのインデックスで、strは挿入する文字列だ。

    文字列の先頭や末尾への挿入は以下のようになる。

    - +

    末尾への挿入は文字列の結合と同じ効果だ。

    インデックスで中間に挿入するのは以下の通り。

    - +

    これはtext.find("cat"sv)でまず部分文字列"cat"の先頭へのインデックスを探し、そこに文字列"fat "を挿入している。結果として変数text"big fat cat"となる。

    部分文字列の削除

    文字列から部分文字列を削除するにはerase( pos, n )を使う。posは削除すべき先頭のインデックスで、nは削除すべきインデックス数だ。

    - +

    このプログラムは文字列"dirty cat"から"dirty "を削除し、"cat"にする。

    - +

    このプログラムは文字列"big fat cat"から部分文字列"fat"を検索し、その先頭から変数fatのサイズ文の部分文字列を削除する。結果として変数text"big cat"になる。

    先頭から末尾までを削除すると、clear()と同じ意味になる。

    - +

    部分文字列の置換

    replace( pos, n1, str )を使うと、文字列のインデックスposからn1個までの文字型の値を、文字列strで置き換える。

    - +

    このコードは、文字列textから部分文字列"ugly"を探し、その先頭へのインデックスと文字列"ugly"のサイズを指定することで、部分文字列"ugly"を、文字列prettyの値である"pretty"に置換する。結果としてtext"pretty cat"になる。

    その他の推奨できない操作

    basic_stringにはこの他に様々な、現代では推奨できない操作がある。

    例えばoperator []で文字列をインデックスでアクセスできる。これは基本実行文字セットに対しては動く。

    - +

    これは、basic_stringが設計された時代は、1文字型は1文字を表現できるという前提があったからだ。

    現代の文字列の表現方法であるUnicodeとUTFによるエンコードではこの前提が成り立たない。例えば、最もよく使われているUTF-8の場合、以下のようになる。

    - +

    textのインデックス0に当たる文字型の値はu8'い'ではない。UTF-8は文字「い」を文字型1つで表現できないからだ。u8"いろは"というUTF-8文字列リテラルはすでに学んだように、以下のように表現される。

    - +

    文字「い」をUTF-8で表現するためには、char8_t型の値が3つ必要で、0xe3, 0x81, 0x84というシーケンスでなければならない。そのため、個々の文字型の値をインデックスでアクセスしても意味がない。また、size()は文字数を返すのではなく、インデックス数を返す。

    basic_stringにはリバースイテレーターを返すrbegin/rendもあるが、Unicodeでエンコードされた文字列では、複数の値のシーケンスで1文字を表現しているため、単に値単位で逆順のイテレートすることは、技術的には可能だが、意味的には壊れてしまう。

    basic_stringには最初に発見したいずれかの文字へのインデックスを返すfind_first_ofがある。

    - +

    iは3になる。なぜならば、find_first_of("abc"sv)はa, b, cのうちいずれかの文字である最初のインデックスを返すからだ。

    この機能はUnicodeでは使えない。というのも1文字型で1文字を表現できないからだ。

    basic_string_viewの操作

    basic_string_viewbasic_stringとほぼ同じ操作が行える。ただし、basic_string_viewは書き換えることができないので、一部の操作が使えない。append, insert, erase, replaceは使えない。basic_string_view同士のoperator +もない。

    C++20では、文字列の先頭と末尾を指定したインデックス数分削ることはできる。

    先頭を削るにはremove_prefix(i)を使う。

    - +

    末尾を削るにはremove_suffix(i)を使う。

    - +

    乱数

    乱数はプログラミングにおいてよく使う。例えば6面ダイスをプログラムで実装するには、1,2,3,4,5,6までのいずれかの目を出す。

    $ ./dice
    @@ -17827,58 +17908,58 @@ 

    疑似乱数

    乱数エンジン

    乱数エンジンは生の乱数を生成するライブラリだ。クラスで実装されている。

    乱数エンジンはメンバー関数min()で最小値を、メンバー関数max()で最大値を、operator()で最小値から最大値の間の乱数を返す

    - +

    乱数エンジンのオブジェクトeoperator ()を呼び出すたび、つまりe()をするたびに変更される。これは疑似乱数のための内部状態を更新するためだ。そのため、乱数エンジンはconstでは新しい乱数を作るのに使えない。

    標準ライブラリはデフォルトの乱数エンジンとしてstd::default_random_engineを提供している。

    以下のプログラムはデフォルトの乱数エンジンから乱数を10個出力する。

    - +

    標準ライブラリの提供する乱数エンジンには様々なものがあるが、本書ではもう一つ、メルセンヌツイスターというアルゴリズムを実装した乱数エンジンを紹介する。std::mt19937だ。

    std::mt19937を使うには、st::default_random_engineを置き換えるだけでいい。

    - +

    メルセンヌツイスターはとても優秀な乱数エンジンだ。乱数が必要な多くの場面では、メルセンヌツイスターを使っておけばまず問題はない。

    では乱数エンジンを使って、生の乱数を標準入力で得た個数だけ出力するプログラムを書いてみよう。

    - +

    実行結果は以下のようになる。

    $ dice
     10
    @@ -17888,38 +17969,38 @@ 

    乱数分布

    乱数分布とは生の乱数を望みの範囲の乱数に加工するためのライブラリだ。クラスで実装されている。

    乱数分布ライブラリにも様々なものがあるが、6面ダイスのプログラムを実装するのに使うのはstd::uniform_int_distribution<T>だ。

    この乱数分布ライブラリは、Tにほしい乱数の整数型を指定する。コンストラクター引数を2つ取るので、1つ目の引数に最小値、2つ目の引数に最大値を指定する。

    - +

    この乱数分布クラスの変数dは、\(a \leq r \leq b\)までの範囲のint型の乱数rを作り出す。

    6面ダイスを作るには、d(a, b)d(1, 6)にすればよい。

    - +

    乱数分布クラスのオブジェクトdを作ったならば、operator()に乱数エンジンのオブジェクトを引数に渡すことで乱数が作れる。乱数エンジンのオブジェクトをeとすると、d(e)だ。

    - +

    以上の知識を利用して、プログラムdiceを作ってみよう。

    - +

    早速実行してみよう。

    $ ./dice
     5
    @@ -17931,23 +18012,23 @@ 

    乱数分布

    20 5 1 6 6 1 6 6 2 4 2 1 4 2 2 4 6 6 6 6 6

    この場合、乱数値は正の整数しか生成しないので、型をintではなくunsigned intにすることもできる。

    - +

    ただし、乱数の結果の型をunsigned intにすると、生成した乱数を使うときに負数が出てくるような計算で問題になる。例えば6面ダイスを2回振り、1回目の出目から2回目の出目を引き算するコードを書いてみよう。

    - +

    もし2回目の出目の方が1回目の出目より大きかった場合、結果は負数になってしまうが、unsigned int型は負数を表現できない。

    そのため、通常は符号付きの整数型を使ったほうが安全だ。

    また、分布クラスのテンプレートパラメーターにはデフォルトテンプレート実引数が指定されているので、デフォルトでよければ省略することもできる。

    - +

    ところで、上のコードは動くのだが、別のプログラムを実行しても毎回同じ出力になる。これでは実用的な6面ダイスプログラムとは言えない。プログラムの実行ごとに結果を買えたい場合、シードを設定する。

    シード

    線形合同法を思い出してみよう。線形合同法で次の乱数\(X_{n+1}\)を計算するには、今の乱数\(X_{n}\)に対して\(X_{n+1} = (a \times X_{n} + c) \bmod m\)という計算をする。

    @@ -17955,94 +18036,94 @@

    シード

    一般化すると、疑似乱数は内部状態\(S_n\)を持ち、計算を加える関数\(f(x)\)を適用することで、次の内部状態\(S_{n+1}=f(S_n)\)を作り出すのだ。単純な線形合同法の場合、内部状態がそのまま乱数の値になるが、複雑な疑似乱数アルゴリズムでは、内部状態から乱数を求めるのにさらに計算を加えるものもある。

    乱数エンジンをデフォルト初期化すると、この内部状態もデフォルト初期化される。そのため、今まで使っていた乱数は、プログラムの実行ごとに同じ乱数列を作り出すのだ。

    疑似乱数の内部状態の初期状態を設定するための値をシード(seed)という。シードを設定するには、std::seed_seqというクラスのオブジェクトを乱数エンジンのコンストラクターに渡す。

    - +

    std::seed_seq s({n})nの値を変更し、異なるシード値が異なる生の乱数を生成しているのを確かめよう。

    シード値は乱数エンジンのメンバー関数seed(s)でも渡すことができる。

    - +

    r1 == r2になるのは、同じシード値を渡して内部状態を設定しているからだ。

    std::seed_seqには複数の符号なし32bit整数を渡すことができる。= {n1, n2, n3,...}のように初期化することもできるし、イテレーターを使って(first, last)のように設定することもできる。

    - +

    乱数エンジンをコピーすると、その内部状態もコピーされる。これを利用して、乱数を保存しておくこともできる。

    - +

    乱数エンジンe1, e2は同じ状態を持っているので、同じ回数乱数生成をすると、同じ乱数列が生成される。

    予測不可能な乱数

    シード値を設定すれば乱数エンジンに異なった乱数列を生成させることができる。しかし、シード値はどうやって生成すればいいのだろうか。シード値をデフォルト初期化した乱数エンジンで生成しても意味がない。なぜならば初期状態はプログラムの実行ごとに同じだからだ。

    - +

    内部状態を更新するのではない、本当に予測不可能な乱数を生成するには、ハードウェアの支援が必要だ。例えば放射性同位体がいつ放射性崩壊を起こすかは予測不可能だ。したがって放射線量を計測するガイガーカウンターの値は予測不可能だ。コンピューターにガイガーカウンターが取り付けられていれば、その値を読むことによって予測不可能な値を得ることができる。他にもコンピューターには様々な予測不可能な値を得る方法がある。std::random_deviceはそのような実装依存のコンピューターの支援を受け、予測不可能な乱数を生成する乱数エンジンだ。

    - +

    std::random_deviceを使えば、std::seed_seqを予測不可能な値で初期化できる。

    - +

    十分なシード値の量

    std::seed_seqの初期化では32bit符号なし整数をいくつでも指定できる。

    - +

    ではいくつの値を渡せばいいのだろうかということは、初期化する乱数エンジンの内部状態のサイズによって異なってくる。現在、C++標準規格には乱数エンジンを適切に初期化する簡単な方法がない。

    1つの目安としては、乱数エンジンのオブジェクトサイズがある。

    - +

    これを筆者の環境で実行すると、5000と出力された。つまり筆者の使っているC++実装のstd::mt19937のオブジェクトサイズは5000バイトだということだ。

    std::random_deviceunsigned int型の乱数を返す。筆者の環境ではsizeof(unsigned int) == 4になる。すると\(5000 \div 4 = 1250\) となる。とすると安全のためには、std::seed_seqにはstd::random_deviceの乱数を1250個渡すべきだろう。

    - +

    筆者の環境ではsizeof(std::default_random_engine) == 8であった。すると2個で良いことになる。

    - +

    C++標準規格にはいずれ、乱数エンジンを予測不可能なシード値で適切に初期化する簡単な方法が追加されるはずだ。

    乱数分布ライブラリ

    生の乱数は使いづらい。生の乱数というのはnビットの整数値だ。それに対して、我々が使いたい実用的な乱数というのは以下のようなものだ。

    @@ -18059,13 +18140,13 @@

    乱数分布ライブラリ

    n = r \bmod |b-a| + a \]

    この間違った計算式を使うと、6面ダイスの乱数値nは生の乱数r(\(0 \leq r\))から以下のようにC++で計算できる。

    - +

    剰余を使って値を\(0 \leq n \leq 5\)までの範囲にし、そこに1を加えることで\(1 \leq n \leq 6\)にしようというものだ。残念ながら、この方法は偏った6面ダイスを作ってしまう。なぜか。

    生の乱数rには3ビット以上の情報が必要だ。コンピューターは整数をビット列で表現するのですべてのビットが等しく乱数の場合、nビットの乱数値は\(2^n\)個の状態を持つ。これを\(0 \leq r \leq 2^n-1\)に割り振った符号なし整数にしたとする。rが3ビットの場合、その値の範囲は\(o \leq r \leq 7\)だ。

    上のコードでは、0から5まではそのまま1から6になる。剰余のため、6と7はそれぞれ1と2になる。すべての取りうる乱数を書き出してみよう。

    @@ -18120,90 +18201,90 @@

    乱数分布ライブラリ

  • それ以外の場合、1に戻る
  • これを実装すると以下のようになる。

    - +

    この関数の実行時間は確率的に決まる。この実装はとても非効率的に見えるが、これ以外に公平に2で割り切れない素因数を含む状態数の乱数を生成する方法はない。

    このコードは1回の乱数生成をキャッシュして複数回の3bitの乱数を切り出すなどの処理をすれば、乱数生成にコストがかかる場合、その分高速化できる。

    分布クラス

    分布クラスには共通の機能がある。本書ではすべてを解説しないが、重要な機能を解説する。

    まず標準ライブラリの分布クラスに共通する機能を説明する。

    分布クラスはオブジェクトを作り、そのオブジェクトを乱数エンジンと組み合わせて使う。その際、コンストラクターの引数で細かい設定を指定する。

    - +

    分布クラスはoperator ()を呼び出して乱数を分布させる。その際、引数には乱数エンジンへの非constなリファレンスを指定する

    - +

    rが分布された乱数。乱数エンジンeと乱数分布dは乱数を生成したので内部状態が変更される。

    分布クラスはメンバー関数minmaxで分布する乱数の最小値、最大値が得られる。

    - +

    分布クラスは構築時の実引数を同名のメンバー関数で取得することができる。

    例えば、std::uniform_int_distribution( a, b )の場合、構築時に渡したa, bを引数の名前でメンバー関数として取得できる。

    - +

    分布クラスは内部状態のリセットができる。

    分布クラスは内部的に乱数値をキャッシュしている可能性がある。例えば乱数値が0か1である場合、1 bitの乱数しか必要ない。ここで渡した乱数エンジンが2 bit以上の乱数を生成できるのであれば、乱数値をキャッシュしておいて、1 bitずつ切り出して使うという最適化が考えられる。

    しかしこの場合、同じ乱数エンジンを渡したのに、結果が違うということが起こり得る。

    - +

    このような場合に、内部状態をリセットするメンバー関数resetを呼び出せば、同じ内部状態になることが保証される。

    - +

    また、この内部状態を取り出すこともできる。内部状態はネストされた型名param_typeで保持できる。内部状態を取り出すにはメンバー関数param()を呼び出す。分布クラスのコンストラクターこのparam_typeの値を渡すと、同じ内部状態の分布クラスを作り出すことができる。またメンバー関数param(parm)param_typeの値を渡して内部状態を設定することも可能だ。

    - +

    一様分布(Uniform Distribution)

    一様分布とは乱数の取りうる状態がすべて等しい確率で出現する乱数のことだ。

    整数の一様分布(std::uniform_int_distribution<IntType>)

    @@ -18212,21 +18293,21 @@

    整数の一様 P(i\,|\,a,b) = 1 / (b - a + 1) \text{ .} \]

    以下のように変数を宣言する。

    - +

    IntTypeは整数型でデフォルトはintaは最小値、bは最大値。ただし\(a \leq b\)

    エンジンも含めた使い方は以下の通り

    - +

    値の範囲には負数も使える。

    - +

    この分布は、-3,-2,-1,0,1,2,3のいずれかをそれぞれ\(\frac{1}{7}\)の等しい確率で返す。

    浮動小数点数の一様分布(uniform_real_distribution<RealType>)

    uniform_real_distribution<RealType>は浮動小数点数型の乱数\(x\), \(a \leq x < b\)を以下の定数確率密度関数に従って分布させる。

    @@ -18235,19 +18316,19 @@

    浮 \]

    a == bのときは未定義となる。

    以下のように変数を宣言する。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoubleaは最小値、bは最大値。値の範囲は\(a \leq b\) かつ \(b - a \leq \text{RealType型の最大値}\)

    エンジンも含めた使い方は以下の通り。

    - +

    浮動小数点数の難しいところは、整数と違って値の範囲の状態が多いことだ。例えば0.01.0の間には0.5もあるし、0.01もあるし0.001もある。浮動小数点数の実装が表現できる状態はとても多い。。uniform_real_distributionは指定された値の範囲で浮動小数点数が表現できるすべての値のうちから乱数を生成してくれる。そのため読者は難しいことを考える必要はない。

    ベルヌーイ分布(Bernoulli distributions)

    ベルヌーイ分布(bernoulli distribution)とは、ベルヌーイ試行(bernoulli trial)に関する分布だ。

    @@ -18273,16 +18354,16 @@

    ベルヌーイ試行

    このような結果を2種類に分けることができ、そのうちのどちらか一方だけが結果として出る、かつ1回1回が独立した試行をベルヌーイ試行と呼ぶ。

    ベルヌーイ分布を使うと、一様分布ではない2値(true/false)の確率的な結果について乱数で得ることができる。例えば、ビデオゲームで宝箱を開けると32%の確率でアイテムが入っており、68%の確率で空っぽであるとする。これを一様分布で実装すると、以下のようになる。

    - +

    このようなコードを書くのは間違いの元だ。確率32%というのは32.0/100.0というdouble型の値で表現できる。この値だけ指定して、残りはライブラリに任せたい。そのようなときに使うのがベルヌーイ分布だ。

    ベルヌーイ分布(std::bernoulli_distribution)

    ベルヌーイ分布(bernoulli distribution)は一回のベルヌーイ試行の結果を乱数として返す。

    @@ -18295,46 +18376,46 @@

    ベルヌーイ分布(st \]

    確率\(p\)trueが、確率\(1-p\)falseが返る。

    以下のように変数を宣言する。

    - +

    pdouble型で、値の範囲は\(0 \leq p \leq 1\)

    使い方。

    - +

    bernoulli_distributionはテンプレートクラスではない。生成する乱数の型はboolだ。pdouble型で確率\(p\)のことだ。値の範囲は\(0 \leq p \leq 1\)

    例えば前述の32%の確率でアイテムが入っている宝箱を実装するには以下のようになる。

    - +

    この関数open_chestは確率32%でtrueを、確率68%でfalseを返す。

    本当にそうだろうか。確かめてみよう。

    32%の確率でtrueになり、68%の確率でfalseになっているかどうかを確かめるには、大量の乱数を生成してtrue/falseをカウントし、それぞれ乱数を生成した数で割って割合を見ればよい。

    - +

    これを実行してみると、筆者の環境では、

    false: 72%
     true : 28%
    @@ -18359,55 +18440,55 @@

    二項分布(std::bin P(i\,|\,t,p) = \binom{t}{i} \cdot p^i \cdot (1-p)^{t-i} \text{ .} \]

    以下のように変数を宣言する。

    - +

    IntTypeは整数型でデフォルトはintだ。tIntType型の整数値で、値の範囲は\(0 \leq t\)だ。pdouble型の値で確率を指定する。pの値の範囲は\(0 \leq p \leq 1\)だ。

    使い方。

    - +

    100回コイントスとした結果、表が出た回数を乱数で得る関数coinflips100は以下のように書ける。

    - +

    100回のベルヌーイ試行をするので\(t=100\)で、ベルヌーイ試行の成功確率は\(p=\frac{1}{2}=0.5\)になる。

    これを10回ぐらい呼んでみよう。

    - +

    筆者の環境では結果は以下のようになった。

    53, 54, 43, 56, 51, 50, 45, 48, 49, 47, 

    期待値は50なので、50前後の乱数が出やすい。

    6面ダイスを60回振った結果出た1の目の合計を乱数で返す関数’roll_for_one’は以下のようになる。

    - +

    60回のベルヌーイ試行をするので\(t=60\)で、ベルヌーイ試行の確率は6面ダイスの1の目が出る確率なので、$\(p=\frac{1}{6}\)になる。

    確率1%で当たるくじを100回引いた結果アタリの回数を返す関数lootboxは以下のように実装できる。

    - +

    この関数を10回呼び出してみると結果は以下のようになる。

    1, 0, 2, 1, 0, 0, 0, 0, 1, 3,

    確率1%で当たるくじを100回引くと、複数回当たることもあれば、1回も当たらないこともある。期待値は1だが、期待値というのは平均的に期待できる結果でしかない。読者諸君もくじ引きをするときは確率に気をつけよう。たとえくじが毎回公平であったとしても、確率は無記憶性なのだ。「もう90回くじを引いたから後10回引けば当たるはず」という考え方は通用しない。

    @@ -18425,47 +18506,47 @@

    幾何分布(std::geometric_distr P(i\,|\,p) = p \cdot (1-p)^{i} \text{ .} \]

    変数の宣言。

    - +

    IntTypeは整数型でデフォルトはintpは確率で値の範囲は\(0 < p < 1\)だ。pの値の範囲に注意すること。0と1であってはならない。幾何分布は成功するまでベルヌーイ試行した回数をかえすので、\(p=0\)の場合、必ず失敗するベルヌーイ試行になり意味がない。\(p=1\)のときは必ず成功するベルヌーイ試行であり、やはり意味がない。

    geometric_distributionの生成する乱数の範囲にも注意が必要だ。生成される乱数\(i\)の範囲は\(i \geq 0\)だ。0もあり得る。0ということは、最初のベルヌーイ試行が成功したということだ。1は2回めのベルヌーイ試行が成功したということだ。幾何分布はベルヌーイ試行が初めて成功するまでのベルヌーイ試行の回数を返すので、成功したベルヌーイ試行は回数に含めない。

    使い方。

    - +

    コイントスを表が出るまで繰り返し、その合計回数を乱数で返す関数try_coinflipsを書いてみよう。

    - +

    最後に+1しているのは、この文脈では表を出したときのコイントスも数えるからだ。つまり成功したベルヌーイ試行も回数に数えるので、幾何分布の生成する乱数より1多い数になる。

    10回呼び出してみたところ、以下のような戻り値を返した。

    1, 3, 6, 1, 1, 2, 1, 8, 9, 5, 

    運がよければ1回で表が出るが、運が悪ければ9回かかる。もちろんもっとかかる可能性もある。

    6面ダイスを1の目が出るまで振り、その合計回数を返す関数try_rollsを書いてみよう。

    - +

    これも10回呼び出してみると筆者の環境では以下のようになった。

    1, 10, 20, 2, 3, 5, 2, 28, 31, 19, 

    6面ダイスを振ると、運がよければ1回で1の目が出るが、運が悪いと何十回も振る必要がある。

    確率1%のくじをはじめて当たるまで引き続け、くじを引いた回数を返す関数try_lootboxesも書いてみよう。

    - +

    10回呼び出してみよう。

    15, 180, 346, 25, 37, 79, 21, 493, 562, 342,

    確率1%のくじを当てるには、運が悪いと何百回も引かなければならない。

    @@ -18489,60 +18570,60 @@

    負の二項分布( \]

    \(p = 1\)のときの\(P(i\,|\,k,p)\)は未定義だ。

    変数の宣言は以下の通り。

    - +

    IntTypeは整数型でデフォルトはintkIntType型の値\(0 < k\)で成功させるベルヌーイ試行の回数、pdouble`型の確率\(- < p \leq 1\)だ。

    使い方。

    - +

    幾何分布と同じく、負の二項分布が生成する乱数ik回のベルヌーイ試行を成功させるまでに失敗したベルヌーイ試行の数を返す。

    例えば、コイントスで10回表が出るまでに失敗したコイントスの数を返す。コイントスがベルヌーイ試行で、表が成功だ。成功したベルヌーイ試行の数は返さない。そのため、結果の乱数は10以下、時には0であることもあり得る。0というのは10回コイントスをしたらすべて表になったので1回もベルヌーイ試行が失敗しなかった場合だ。

    コイントスを10回表が出るまでに行ったコイントスの回数を返す関数count_10_coinflipsは以下のように書く。

    - +

    最後に+10しているのは、この関数は成功も含めたコイントスの回数を返すからだ。10回表が出るまでに失敗した、つまり裏になったコイントス回数がほしければ、そのままの値を使えばいい。

    - +

    参考までに、n回表が出るまでに行ったコイントスの回数を乱数で返す関数count_n_coinflipsは以下の通り。

    - +

    6面ダイスを10回、1の目が出るまで振った回数を乱数で返す関数“count_10_rolls”は以下の通り。

    - +

    確率1%のくじを10回当てるまでくじを引いた回数を返す関数count_10_lootboxは以下の通り。

    - +

    ポアソン分布

    ポアソン分布(Poisson distribution)とは、シメオン・ドニ・ポアソン(Siméon Denis Poisson 1781-1840)が1837年に発表した論文、「刑事民事の判決における確率の調査」で初めて公開されたものだ。この論文でポアソンは、ある国における冤罪の数について、ある時間間隔における冤罪の発生数を乱数とし、そのような乱数の分布について考察した。その結果がポアソン分布だ。

    ある時間間隔に発生する離散的な事象の多くがポアソン分布に従う。例えば以下は具体的な例だ。

    @@ -18562,25 +18643,25 @@

    ポアソン分布(poisson_distr \]

    ここで\(\mu\)meanとする。\(\mu > 0\)ではない場合未定義だ。

    変数の宣言は以下の通り。

    - +

    Tは整数型でデフォルトはintmeanRealType型。\(\mu\)と同じで浮動小数点数型の値で所定の時間に平均して発生する事象の回数だ。値の範囲は\(0 < \text{mean}\)

    使い方。

    - +

    ポアソン分布が生成する乱数は0以上の事象が発生した回数となる。

    例えば、1ヶ月に交通事故が平均して10件発生するとする。1ヶ月に発生した交通事故の件数は平均が10件になるように増減するはずだ。1ヶ月の交通事故の発生件数を乱数で返す関数traffic_accidentsは以下のようになる。

    - +

    これを10回呼び出すと以下のような乱数列が生成された。

    14, 6, 11, 8, 8, 14, 7, 16, 12, 17, 

    だいぶばらつきがある。ポアソン分布とはこういうものだ。離散的に起こる事象を乱数として取ると、このようにばらつく。現実でも、1ヶ月に交通事故が平均して10件起きている場合、20件起きる月や無事故の月が存在する可能性があるのだ。

    @@ -18609,25 +18690,25 @@

    指数分布(std: p(x\,|\,\lambda) = \lambda e^{-\lambda x} \text{ .} \]

    変数の宣言方法は以下の通り。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublelambdaRealType型。ポアソン分布のmeanと同じで、ある時間間隔における事象の発生回数だ。値の範囲は\(0 < \text{lambda}\)

    std::exponential_distributionの生成する乱数は1.0のとき、ある時間間隔に等しくなる。0.5なら半分の時間間隔、2.0なら2倍の時間間隔だ。

    使い方。

    - +

    一ヶ月に10件の交通事故がポアソン分布に従って発生する場合に、ある交通事故から次の交通事故までの時間間隔の乱数を日数で得る関数until_next_traffic_accidentは以下のように書く。

    - +

    ある時間間隔に10回起こるので、lambdaには10.0を指定する。ここでは簡単のために一ヶ月を30日とする。結果の乱数は1.0がある時間間隔に等しいので、つまり1.0は30日に等しい。結果に30.0をかけることで日数を計算する。

    この関数を10回呼び出すと以下のようになった。

    0.436732, 5.40559, 10.4085, 0.749364, 1.10523, 2.37705, 0.626176, 14.8351, 16.932, 10.2976,
    @@ -18641,18 +18722,18 @@

    ガンマ分布(std: \]

    \(\alpha\)alpha\(\beta\)betaとする。

    変数の宣言は以下の通り。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublealpha, betaRealType型。値の範囲は\(0 < alpha\), \(0 < beta\)

    使い方。

    - +

    ウェイブル分布(std::weibull_distribution<RealType>)

    std::weibull_distribution<RealType>は浮動小数点数型の乱数\(x > 0\)を以下の確率密度関数に従って分布する。

    \[ @@ -18662,18 +18743,18 @@

    ウェイブル分 \text{ .} \]

    変数の宣言は以下の通り。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublea, bRealType型。値の範囲は\(0 < a\), \(0 < b\)

    使い方。

    - +

    極値分布(std::extreme_value_distribution<RealType>)

    std::extreme_value_distribution<RealType>は浮動小数点数型の乱数\(x\)お以下の確率密度関数に従って分布する。

    \[ @@ -18683,18 +18764,18 @@

    極値分布(st \]

    極値分布(extreme value distribution)は、ガンベルI型(Gumbel Type I)、対数ウェイブル(log-Weibull)、フィッシャー=ティペットI型(Fisher-Tippett Type I)という名前の分布と呼ばれることもある。

    変数の宣言は以下の通り。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublea, bRealType型。値の範囲は\(0 < b\)

    使い方。

    - +

    正規分布

    正規分布(std::normal_distribution<RealType>)

    std::normal_distribution<RealType>は浮動小数点数型の乱数\(x\) 以下の確率密度関数に従って分布する。

    @@ -18711,21 +18792,21 @@

    正規分布(std::norm \]

    分布パラメーターのうちの\(\mu\)\(\sigma\)は、それぞれ分布の平均(mean)、標準偏差(standard deviation)とも呼ばれている。

    変数の宣言。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublemean, stddevは浮動小数点数型。meanは平均。stddevは標準偏差で値の範囲は\(0 < \text{stddev}\)

    使い方。

    - +

    対数正規分布(std::lognormal_distribution<RealType>)

    std::lognormal_distribution<RealType>は浮動小数点数の乱数\(x > 0\)を以下の確率密度関数に従って分布する。

    \[ @@ -18734,62 +18815,62 @@

    対数正規分布( \text{ .} \]

    変数の宣言。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublem, sRealType型。値の範囲は\(0 < s\)

    使い方。

    - +

    カイ二乗分布(std::chi_squared_distribution<RealType>)

    std::chi_squared_distribution<RealType>は浮動小数点数型の乱数\(x > 0\)を以下の確率密度関数に従って分布する。

    \[ p(x\,|\,n) = \frac{x^{(n/2)-1} \cdot e^{-x/2}}{\Gamma(n/2) \cdot 2^{n/2}} \text{ .} \]

    変数の宣言。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublenRealType型。値の範囲は\(0 < n\)

    使い方。

    - +

    コーシー分布(std::cauchy_distribution<RealType>)

    std::cauchy_distribution<RealType>は浮動小数点数型の乱数\(x\)を以下の確率密度関数に従って分布する。

    \[ p(x\,|\,a,b) = \left(\pi b \left(1 + \left(\frac{x-a}{b} \right)^2 \, \right)\right)^{-1} \text{ .} \]

    変数の宣言。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublea, bRealType型。値の範囲は\(0 < b\)

    使い方。

    - +

    フィッシャーのF分布(std::fisher_f_distribution<RealType>)

    フィッシャーのF分布(Fisher’s F-distribution)の名前は数学者サー・ロナルド・エイルマー・フィッシャー(Sir Ronald Aylmer Fisher)に由来する。

    std::fisher_f_distribution<RealType>は浮動小数点数の乱数\(x > 0\)を以下の関数密度関数に従って分布する。

    @@ -18801,17 +18882,17 @@

    フィッ \text{ .} \]

    変数の宣言。

    - +

    RealTypeは浮動小数点数型でデフォルトはdobulem, nRealType型。値の範囲は\(0 < m\) かつ \(0 < n\)

    使い方。

    - +

    スチューデントのt分布(std::student_t_distribution<RealType>)

    スチューデントのt分布(Student’s t-distribution)はウィリアム・シーリー・ゴセットによって考案された。当時、ウィリアムはギネス醸造所で働いていたが、ギネスは従業員に科学論文を発表することを禁じていたために、ウィリアムはスチューデントという偽名で発表した。

    std::student_t_distribution<RealType>は浮動小数点数型の乱数\(x\)お以下の確率密度関数に従って分布する。

    @@ -18822,134 +18903,460 @@

    スチ \text{ .} \]

    変数の宣言。

    - +

    RealTypeは浮動小数点数型でデフォルトはdoublenRealType型で、値の範囲は\(0 < n\)

    使い方。

    - + +

    サンプリング分布(sampling distributions)

    +

    サンプリング分布(sampling distributions)とは、標本から分布の特徴が分かっている場合に、その特徴を指定することにより、望みの分布を作り出す分布のことだ。

    +

    離散分布(std::discrete_distribution<IntType>)

    +

    簡単な説明

    +

    離散分布(discrete distribution)は整数型の乱数\(i\), \(0 \leq i < n\)を返す分布だ。例えば\(n = 10\)ならば、\(0,1,2,3,4,5,6,7,8,9\)の10個のうちのいずれかの整数値を乱数として返す。この際、乱数値として取りうる整数値一つ一つに、確率を設定できる。確率は\(p_0, \dotsc, p_{n-1}\)で設定し、\(p_0\)\(0\)の確率, \(p_1\)\(1\)の確率…\(p_{n-1}\)\(n\)の確率となる。それぞれの乱数\(i\)は確率\(\frac{p_i}{S}\)で出現する。このとき\(S\)とはすべての確率の合計、つまり\(S = p_0 + \dotsc + p_{n-1}\)となる。確率\(p_i\)double型で与える。

    +

    たとえば、{1.0, 1.0, 1.0}という確率群を渡した場合、離散分布は\(0, 1, 2\)のいずれかの乱数をそれぞれ\(\frac{1.0}{3.0}\)の確率で返す。

    +

    もし、{1.0, 2.0, 3.0}という確率群を渡した場合、離散分布は\(0, 1, 2\)のいずれかの乱数を返す。その時の確率は、\(1\)\(\frac{1}{6}\)\(2\)\(\frac{1}{3}\)\(3\)\(\frac{1}{2}\)だ。

    +

    例えば公平な6面ダイスを作りたい場合、{1.0, 1.0, 1.0, 1.0, 1.0, 1.0}を指定すると\(0 \leq i \leq 5\)までの6個の乱数\(i\)がそれぞれ\(\frac{1}{6}\)の確率で生成される。この結果に+1すると\(1 \leq i \leq 6\)の乱数を得ることができる。

    +

    6の目だけ2倍高い確率で出るイカサマ6面ダイスを作りたい場合、{1.0, 1.0, 1.0, 1.0, 1.0, 2.0}を指定すると、0から4までの5つの目は\(\frac{1}{7}\)の確率で出現し、5だけは\(\frac{2}{7}\)の確率で出る乱数を作ることができる。

    +

    \(S\)はすべての確率の合計で、それぞれの値は\(\frac{p_i}{S}\)の確率で出る。なので、以下はすべて分布だ。

    +
    {1.0, 1.0, 1.0, 1.0, 1.0, 2.0}
    +{0.1, 0.1, 0.1, 0.1, 0.1, 0.2}
    +{2.0, 2.0, 2.0, 2.0, 2.0, 4.0}
    +

    数学的な説明

    +

    std::discrete_distribution<IntType>は整数型の乱数\(i\), \(0 \leq i < n\)を以下の離散確率関数に従って分布する。

    +

    \[ +P(i \,|\, p_0, \dotsc, p_{n-1}) = p_i \text{ .} +\]

    +

    別に指定のない場合、分布パラメーターは\(p_k = {w_k / S}\) for \(k = 0, \dotsc, n - 1\)として計算され、このとき値\(w_k\)は、一般にウエイト(wieght)と呼ばれていて、値は非負数、非NaN、非無限でなければならない。さらに、以下の関係が成り立たねばならない。\(0 < S = w_0 + \dotsb + w_{n - 1}\)

    +

    変数の宣言

    +

    std::discrete_distributionの変数を宣言するには3つの方法がある。いずれもdouble型の値をn個渡すための方法だ。

    +
    イテレーターのペア
    +

    変数の宣言。

    + +

    IntTypeは整数型でデフォルトはint[firstW, lastW)はイテレーターのペアで、double型に変換可能な値を参照している。

    +

    利用例。

    + +
    初期化リスト
    +

    利用例。

    + +

    ...にはdouble型の浮動小数点数を指定する

    +

    利用例。

    + +
    個数、デルタ、関数
    +

    このコンストラクターは以下のように宣言されている。

    + +

    UnaryOperationはひとつの実引数を取る関数オブジェクトで戻り値の型はdouble型に変換できること。さらに、double型はUnaryOperationの引数に変換可能なこと。もし\(nw = 0\)の場合は、\(n = 1\)とする。それ以外の場合、\(n = \tcode{nw}\)とする。このとき、\(0 < \delta = (\tcode{xmax} - \tcode{xmin}) / n\)となる関係が満たされなければならない。

    +

    もし\(nw = 0\)ならば\(w_0 = 1\)。それ以外の場合、\(k = 0, \dotsc, n - 1\)に対して、\(w_k = \tcode{fw}(\tcode{xmin} + k \cdot \delta + \delta / 2)\)とする。fwn回を超えて呼ばれることはない。

    + +

    このdは、

    + +

    と初期化されたものと同じように初期化される。

    +

    初期化パラメーターの確認

    +

    std::discrete_distributionの内部状態はメンバー関数probabilitiesで取得できる。戻り値の型はstd::vector<double>で、指定した確率群が要素になっている。

    + +

    応用例

    +

    以下は6の目が2倍の確率で出るイカサマ6面ダイスの実装だ。

    + +

    区分定数分布(std::piecewise_constant_distribution<RealType>)

    +

    簡単な説明

    +

    区分定数分布(piecewise constant distribution)とは、区分と、区分ごとの確率を指定し、いずれかの区分の範囲の値に一様分布させる分布だ。ここで言う確率は、密度、あるいはウエイトともいう。

    +

    ひとつの区分はdouble型の値2つ\(b_i, b_{i+1}\)で与える。このとき区分の乱数\(x\)の範囲は\([b_i, b_{i+1})\)、もしくは\(b_i \leq x < b_{i+1}\)だ。\(n\)個の値を指定すると、\(n-1\)個の区分を指定したことになる。

    +

    例えば{0.0, 1.0}という2つのdouble型の値をつかって1つの区分を与えた場合、これは\(0.0 \leq x < 1.0\)という値の範囲の区分である。{0.0, 1.0, 2.0}という3つのdouble型の値は2つの区分になり、それぞれ\(0.0 \leq x < 1.0\), \(1.0 \leq x < 2.0\)になる。

    +

    一般に、\(n\)個のdouble型の値\(b_0, \dotsc, b_n\)\(n-1\)個の区分を表現する。このとき、\(b_i < b_{i+1}\)\(i = 0, \dotsc, n-1\)までの\(i\)について成り立たなければならない。つまり区分を指定するdouble型の値は、後続の値より小さくなければならないということだ。

    +

    以下は正しい区分の指定だ。

    +
    {1.0, 2.0, 100.0, 999.999}
    +{-1.0, 1.0, 2.0}
    +{-5.0, -4.0, -3.1}
    +

    以下は正しくない区分の指定だ。

    +
    {1.0, 0.0}
    +

    これは\(b_0 > b_1\)なので正しくない。

    +

    それぞれの区分\([b_i, b_{i+1})\)に対して確率\(p_i\)double型で指定する。\(n\)個の\(b_i\)によって\(n-1\)個の区分を指定し、それぞれに対して1つづつ確率を設定するので、確率の数は\(n-1\)個だ。

    +

    例えば{0.0, 1.0}という1つの区分と{1.0}という1つの確率を与えた場合、\(0.0 \leq x < 1.0\)の範囲の乱数\(x\)が生成される。

    +

    {0.0, 1.0, 10.0}という2つの区分と、{1.0, 2.0}という2つの確率を与えた場合、\(\frac{1}{3}\)の確率で\(0.0 \leq x < 1.0\)の範囲に一様分布した乱数になり、\(\frac{2}{3}\)の確率で\(1.0 \leq x < 10.0\)の範囲に一様分布した乱数になる。

    +

    数学的な説明

    +

    std::piecewise_constant_distribution<RealType>は浮動小数点数型の乱数\(x\), \(b_0 \leq x < b_n\)を以下の確率密度関数に従って、それぞれの部分区間(subinterval)\([b_i, b_{i+1}\)の間で一様に分布させる。

    +

    \[ +p(x \,|\, b_0, \dotsc, b_n, \; \rho_0, \dotsc, \rho_{n-1}) = \rho_i + \text{ , for $b_i \le x < b_{i+1}$.} +\]

    +

    この分布の区間境界(interval boundaries)ともいう\(n+1\)分布パラメーター\(b_i\)はすべての\(i = 0, \dotsc, n - 1\)に対して関係\(b_i < b_{i + 1}\)を満たさねばならない。別途指定なき場合、残りの\(n\)分布パラメーターは以下のように計算される。

    +

    \[ +\rho_k = \frac{w_k}{S \cdot (b_{k+1}-b_k)} \text{ for } k = 0, \dotsc, n - 1 \text{ ,} +\]

    +

    一般にウエイト(weight)と呼ばれている値\(w_k\)は、非負数、非NaN、非無限でなければならない。さらに、以下の関係を満たさなければならない。

    +

    \(0 < S = w_0 + \dotsb + w_{n-1}\)

    +

    変数の宣言

    +

    std::piecewise_constant_distributionでは、double型の値の集合を2つ渡す必要がある。ひとつは区間を指定するための\(N\)個のdouble型に変換可能な値で、もう一つは区間ごとの確率を指定するための\(N-1\)個のdouble型に変換可能な値だ。

    +
    イテレーターによる指定
    +

    イテレーターで区間と確率を指定するコンストラクターは以下の通り

    + +

    [firstB, lastB)は区間を指定するための\(N\)個の値を参照する入力イテレーターのペアだ。firstWはそれぞれの区間の確率を指定する\(N-1\)個の値を参照する入力イテレーターの先頭だ。lastWがないのは、確率の個数は\(N-1\)個であると分かっているからだ。

    +

    もし[firstB, lastB)のサイズが1以下の場合、区間は[0.0, 1.0)になり、確率は\(\frac{1}{1}\)になる。

    +
    利用例
    + +

    bsは区間を指定する値の集合、psは区間ごとの確率だ。

    +

    区間は[-1.0, 1.0)[1.0, 2.0)の2つ。確率はそれぞれ\(\frac{1}{6}\)\(\frac{5}{6}\)だ。

    +

    区間を表現する値が足りない場合は以下の通り。

    + +
    初期化リストと関数オブジェクトによる指定
    +

    初期化リストと関数を指定するコンストラクターは以下の通り。

    + +

    イテレーターのペアと同じく、区間は[bl.begin(), bl.end())で指定する。

    +

    確率は\(k = 0, \dotsc, n - 1\)について、\(w_k = \tcode{fw}\bigl(\bigl(b_{k+1} + b_k\bigr) / 2\bigr)\)とする。

    +

    bl.size()が1以下の場合、区間は[0.0, 1.0)になり、確率は\(\frac{1}{1}\)になる。

    +
    利用例
    + +

    この場合、区間は[1.0, 2.0), [2.0, 3.0), ‘[3.0, 4.0)’, ’[4.0, 5.0)’の4個になり、確率は{1.5, 2.5, 3.5, 4.5}となる。

    +
    区間数、最小、最大、関数オブジェクトによる指定
    +

    コンストラクターの宣言

    + +

    nwは区間数、xminは最小値、xmaxは最大値、fwは関数オブジェクトで、double型から変換できる型の実引数を取り、double型に変換可能な戻り値を返す。

    +

    \(nw = 0\)の場合、区間の個数\(n\)\(1\)になる。それ以外の場合、\(n = nw\)となる。このとき関係、\(0 < \delta = (\tcode{xmax} - \tcode{xmin}) / n\)が成り立たなければならない。 Let $b_k = + k $ for $ k = 0, , n$, and $w_k = (b_k + / 2) $ for .

    +

    $ k = 0, , n - 1\(において、区間は\)b_k = + k $ for $ k = 0, , n\(とし、確率は\)w_k = (b_k + / 2) $とする。

    +
    利用例
    + +

    この場合、区間の集合は{1.0, 1.8, 2.6, 3.4, 4.2, 5.0}となり、確率は{1.4, 2.2, 3.0, 3.8, 4.6}となる。

    +

    内部状態の取得

    +

    std::piecewise_constant_distributionの内部状態は、メンバー関数intervalsdensitiesで得ることができる。

    + +

    intervalsは区間、densitiesは確率を返す。

    + +

    densities()の結果が正規化されているのは、ユーザーが指定した確率は\(w_k\)だが、ここで返すのは\(p_k\)だからだ。

    +

    区分線形分布(std::piecewise_linear_distribution<RealType>)

    +

    簡単な説明

    +

    区分線形分布(piecewise linear distribution)は区分定数分布と同じく、区間と確率(又の名を密度、ウエイト)を指定する。

    +

    区間の指定は区分定数分布と同じだ。内部境界の集合で指定する。例えば{1.0, 2.0, 3.0}は2つの区間[1.0, 2.0)[2.0, 3.0)を指定する。

    +

    区分線形分布における確率は、区間に対してではなく、内部境界に対して指定する。指定した全区間における値の出現確率は、内部境界から内部境界に向かって指定した確率の差の方向に線形に増加、もしくは減少する。

    +

    例えば区分{0.0, 1.0}と確率{1.0, 2.0}を指定した場合、これはひとつの区間[0.0, 1.0)について、内部境界0.0の確率は\(\frac{1}{3}\)、内部境界1.0'の確率はとし、$0.0 \leq x < 1.0$の範囲の乱数xを生成する。内部境界区間の範囲に注意。1.0未満なので。1.0`は出ない。

    +

    そして、区間の間の値は、区間を区切る2つの内部境界の確率の差によって、線形に増加、もしくは減少する。例えば値0.25が出る確率は\(\frac{1.25}{3}\)0.5が出る確率は\(\frac{1.5}{3}\)、値1.75がでる確率は\(\frac{1.75}{3}\)だ。

    +

    区分{0.0, 1.0, 2.0}と確率{1.0, 2.0, 1.0}の場合、2つの区間[0.0, 1.0)[1.0, 2.0)の範囲について、0.0から1.0に向かう区間についての確率は\(\frac{1}{4}\)から\(\frac{1}{2}\)に増加し、1.0から2.0に向かう区間についての確率は\(\frac{1}{2}\)から\(\frac{1}{4}\)に減少する。

    +

    結果として、乱数値の分布をグラフに描画すると、1.0が最も出やすく、その前後±1.0の範囲で徐々に減少していく山のようなグラフになる。

    +

    TODO: グラフ、横軸が乱数値、縦軸が確率

    +
          *       + \frac{1}{2}
    +     ***      |
    +    *****     |
    +   *******    |
    +  *********   |
    + ***********  + \frac{1}{4}
    + ***********
    + ***********
    + ***********
    + ***********
    +-+----+----+
    +0.0  1.0  2.0
    +

    数学的な説明

    +

    std::piecewise_linear_distribution<RealType>は乱数\(x\), \(b_0 \leq x < b_n\)を以下の確率密度関数に従って分布する。

    +

    \[ +p(x \,|\, b_0, \dotsc, b_n, \; \rho_0, \dotsc, \rho_n) + = \rho_{i} \cdot {\frac{b_{i+1} - x}{b_{i+1} - b_i}} + + \rho_{i+1} \cdot {\frac{x - b_i}{b_{i+1} - b_i}} + \text{ , for $b_i \le x < b_{i+1}$.} +\]

    +

    一般に内部境界とも呼ばれる\(n + 1\)分布パラメーター\(b_i\)\(i = 0, \dotsc, n - 1\)において関係\(b_i < b_{i+1}\) for \(i = 0, \dotsc, n - 1\)を満たさねばならない。別記する場合を除いて、残りの\(n + 1\)パラメーターは\(k = 0, \dotsc, n\)において\(\rho_k = {w_k / S}\)と計算される。このとき\(w_k\)は一般に境界におけるウエイト(weight at boundaries)と呼ばれ、非負数、非NaN、非無弁でなければならない。さらに、以下の関係が成り立たねばならない。

    +

    \[ +0 < S = \frac{1}{2} \cdot \sum_{k=0}^{n-1} (w_k + w_{k+1}) \cdot (b_{k+1} - b_k) \text{ .} +\]

    +

    変数の宣言

    +

    piecewise_linear_distributionは区間と確率を指定するためにn個のdouble型に変換可能な値を指定する必要がある。

    +
    イテレーターによる指定
    + +

    [firstB, lastB)は区間、firstWから区間数までのイテレーターが確立。

    +

    firstB == lastBもしくは+firstB == lastBの場合、つまり内部境界が1個以下で、空の場合、区間数はひとつで[0.0, 1.0)の範囲、確率は{0.0, 1.0}となる。

    +
    使い方
    + +

    空の場合。

    + +

    これは以下のコードと同じだ。

    + +
    初期化リストと関数オブジェクトによる指定
    + +

    区間を指定する内部境界は[bl.begin(), bl.end())、内部境界\(b_k\)に対する確率\(w_k\)\(k = 0, \dotsc, n\)について、\(w_k = \tcode{fw}(b_k)\)とする。

    +

    内部境界が1個以下の場合はイテレーターの場合と同じ。

    +
    使い方
    + +

    これは以下のコード同じだ。

    + +
    個数、最小値、最大値、関数オブジェクトによる指定
    + +

    nwが個数、xminが最小値、xmaxが最大値、fwが関数オブジェクト。

    +

    関数オブジェクトfwdouble型から変換できる実引数を1つだけとり、戻り値の型はdouble型に変換できること。

    +

    \(\tcode{nw} = 0\)ならば空であり、イテレーターの場合と同じ。

    +

    関係\(0 < \delta = (\tcode{xmax} - \tcode{xmin}) / n\)が成り立つこと。

    +

    内部境界\(b_k\)\(k = 0, \dotsc, n\)について\(b_k = \tcode{xmin} + k \cdot \delta\)とする。確率\(w_k\)\(k = 0, \dotsc, n\)について\(w_k = \tcode{fw}(b_k)\)とする。

    +
    使い方
    + +

    上のコードは以下のコードと同じだ。

    +

    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しても、最初のifndefのみがコンパイル対象になるため、問題は起こらない。

    インクルードガードは以下の様式を持つ。

    - +

    十分にユニークなマクロ名は全ソースファイル中で衝突しないそのヘッダーに固有のマクロ名を使う。慣習的に推奨される方法としてはすべて大文字を使い、十分に長いマクロ名にすることだ。

    #define

    #defineはマクロ置換を行う。マクロにはオブジェクト風マクロ(object-like macro)と関数風マクロ(function-like macro)がある。風というのは、マクロはオブジェクトでも関数でもないからだ。ただ、文法上オブジェクトや関数の似ているだけで、実態はトークン列の愚直な置換だ。

    @@ -18957,76 +19364,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 )
    @@ -19036,71 +19443,71 @@

    __VA_OPT__

    #演算子

    #はマクロ実引数を文字列リテラルにする。

    #は関数風マクロの置換リストの中のみで使うことができる。#は関数風マクロの仮引数の識別子の直前に書くことができる。#が直前に書かれた識別子は、マクロ実引数のトークン列の文字列リテラルになる。

    - +

    これは以下のように置換される。

    "hello"
     "hello world"

    また、可変長マクロと組み合わせた場合、

    - +

    以下のように置換される。

    - +

    ##演算子

    ##はマクロ実引数の結合を行う。

    ##は関数風マクロの置換リストの中にしか書けない。##は両端にマクロの仮引数の識別子を書かなければならない。##は両端の識別子の参照するマクロ実引数のトークン列を結合した置換を行う。

    - +

    これは以下のように置換される。

    foobar
     aaa bbbccc ddd

    結合した結果のトークンは更にマクロ置換の対象となる。

    - +

    これは以下のように置換される。

    hello

    CONCAT(FOO,BAR)FOOBARに置換され、FOOBARという名前のマクロ名があるためにさらにhelloに置換される。

    複数行の置換リスト

    #defineディレクティブの置換リストは複数行に渡って書くことができない。これは文法上の制約によるものだ。#defineディレクティブは改行文字で終端される。

    しかし、関数やクラスを生成するような複雑なマクロは、複数行に分けて書きたい。

    - +

    この場合、行末にバックスラッシュに続けて改行を書くと、バックスラッシュと改行がプリプロセッサーによって削除される。

    上の例は以下のように、プリプロセッサーとしては比較的わかりやすく書くことができる。

    - +

    C++ではテンプレートがあるために、このようなマクロを書く必要はない。

    #undefディレクティブ

    #undefはそれ以前に定義されたマクロを削除する。

    - +

    これは以下のように置換される。

    BAR
     FOO
    @@ -19109,120 +19516,120 @@

    条件付きソースファイ

    プリプロセッサーの定数式

    プリプロセッサーで使える条件式は、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
    @@ -19232,25 +19639,25 @@ 

    #elseディレクティブ

    #elseディレクティブはC++でいうelseに相当する。

    #elseディレクティブは#ifディレクティブと#endifディレクティブの間に書くことができる。もし#if#elifディレクティブが処理されない場合で#elseディレクティブがある場合、#elseから#endifまでのトークン列が処理される。

    以下の例は、YESのトークンがある行のみ処理される。

    - +

    #ifdef, #ifndefディレクティブ

    #ifdef 識別子
     #ifndef 識別子
    @@ -19258,59 +19665,59 @@

    #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も処理されないので、コンパイルエラーにはならない。

    #pragma

    #pragmaディレクティブは実装依存の処理を行う。#pragmaはコンパイラー独自の拡張機能を追加する文法として使われている。

    @@ -19387,30 +19794,30 @@

    単一のソースフ

    ヘッダーファイルはコピペ

    すでに、ソースファイルの他にヘッダーファイルというファイルも使っている。ヘッダーファイルはソースファイルではない。コンパイル前にソースファイルにコピペされるだけのものだ。

    例えば以下のような内容のheader.hというヘッダーファイルがあるとして、

    - +

    source.cppが以下のようであるとき、

    - +

    source.cppをコンパイルすると、まずヘッダーファイルが以下のように展開される。

    - +

    ヘッダーファイルとはこれだけのものだ。コンパイラーが#includeされた場所に、ヘッダーファイルの中身を愚直にコピペするだけだ。

    複数のソースファイルのコンパイル

    2つのソースファイル、foo.cppbar.cppからなるプログラムをコンパイルするには、

    @@ -19462,190 +19869,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. 同じトークン列である
    @@ -19656,50 +20063,50 @@

    インライン関数/イン
  • 意味が同じである。
  • 同じトークン列でも意味が異なることがある。

    - +

    foo.cppのインライン関数gf(int)を呼び出すが、bar.cppのインライン関数gf(double)を呼び出す。インライン関数gのトークン列はどちらも同じだが、意味が異なる。

    ODRの例外的な回避の怖いところは、間違えてしまってもコンパイラーがエラーメッセージを出してくれる保証がないところだ。上の同じトークン列で違う意味のような関数は、そのままコンパイルが通ってリンクされ、実行可能なプログラムが生成されてしまうかも知れない。そのようなプログラムの挙動がどうなるかはわからない。この理由は、ODR違反を完全に発見するコンパイラーの実装が技術的に困難だからだ。ODR違反をしないのはユーザーの責任だ。

    インライン変数とインライン関数はわざわざ翻訳単位を分けて分割コンパイルするまでもないライブラリに使うとよい。

    クラス

    クラスにも宣言と定義がある。

    - +

    クラスを複数の翻訳単位で使うには、関数と同じように宣言と定義に分ければよいと考えるかも知れないが、残念ながらクラスの宣言だけでできることは少ない。

    クラスの宣言だけでできることは、クラス名を型名として使うとか、クラスのポインター型を作るぐらいのものだ。

    - +

    宣言だけされたクラスのオブジェクトを作ることはできないし、ポインターの演算もできない。

    - +

    この理由は、宣言だけされたクラスは不完全型(Incomplete type)という特別な扱いの型になるからだ。クラスのオブジェクトを作ったりポインター演算をするには、クラスのオブジェクトのサイズを決定する必要があるが、そのための情報はまだコンパイラーが得ていないために起こる制約だ。

    クラスの定義では、インライン変数やインライン関数と同じく、ODRの例外的な回避が認められている。条件も同じで、1. 異なる翻訳単位で、2. 同じトークン列で、3. 意味も同じ場合だ。

    ODR違反を起こさないために、クラス定義はインクルードファイルに書いて#includeするのがお作法だ。

    @@ -19730,162 +20137,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行のコードで、問題の箇所も一箇所だ。これが数百行、数千行になり、関数やクラスを複雑に使い、問題の原因は複数の箇所のコードの実行が組み合わさった結果で、しかも自分で書いたコードなので正しく書いたはずだという先入観がある場合、たとえコードとしてはささいな間違いであったとしても、発見は難しい。

    こういうとき、実際にコードを一行ずつ実行したり、ある時点でプログラムの実行を停止させて変数の値を見たりしたいものだ。

    @@ -19898,15 +20305,15 @@

    デバッガー

    $ make clean

    GDBのチュートリアル

    では具体的にデバッガーを使ってみよう。以下のようなソースファイルを用意する。

    - +

    このプログラムをコンパイルする。

    $ g++ -g program.cpp -o program

    GDBを使ってプログラムのデバッグを始めるには、GDBのオプションとして“-g”オプション付きでコンパイルしたプログラムのファイル名を指定する。

    @@ -20071,16 +20478,16 @@

    関数名へのブレイクポ
    (gdb) ファイル名:関数名

    と書く。

    C++では異なる引数で同じ名前の関数が使える。

    - +

    このようなプログラムで関数fにブレイクポイントを設定すると、fという名前の関数すべてにブレイクポイントが設定される。

    ブレイクポイントの一覧を表示する“info breakpoints”コマンドで確かめてみよう。

    (gdb) break f
    @@ -20114,32 +20521,32 @@ 

    条件付きブレイクポイン
    (gdb) break ... if 条件

    と入力すると、条件trueとなるときのみブレイクポイントが発動する。

    例えば以下のようなコードで、

    - +

    以下のように7行目に変数i500に等しい条件を設定すると

    (gdb) break 7 if i == 500

    変数i500でありかつ7行目が実行される直前でブレイクポイントが発動する。

    プログラムの実行再開とステップ実行

    以下のようなプログラムがあるとする。

    - +

    このプログラムをmain関数から1行づつ実行してその挙動を確かめたい。その場合に、すべての行にブレイクポイントを設定するのは面倒だ。GDBではこのような場合に、現在中断している場所から一行だけ実行する方法がある。

    実行再開(continue)

    continueコマンドは実行を再開する。省略してcでもよい

    @@ -20155,29 +20562,29 @@

    ステップ実行(step)

    (gdb) break main
     (gdb) run

    main関数に入った直後で実行が中断する。

    - +

    この状態でstepコマンドを使うと

    (gdb) step

    1行分にあたる実行が行われ、また中断される。

    - +

    もう一度stepコマンドを使うと、今度は関数fの中で実行が中断する。

    - +

    このままstepコマンドを続けていくと、またmain関数に戻り、また次の行が実行され、また関数fが実行される。

    1行づつ実行するのではなくn行実行したい場合は、stepコマンドにnを指定する

    (gdb) step n
    @@ -20187,21 +20594,21 @@

    ステップ実行(step)

    ネクスト実行(next)

    stepコマンドはソースファイルの1行分を実行してくれるが、途中に関数呼び出しが入る場合、その関数のソースファイルがある場合はその関数の中も1行とカウントする。nextコマンドは現在実行が中断しているソースファイルの次の行を1行として扱い、次の行まで実行して中断する。

    例えばプログラムが以下の状態で中断しているとする。

    - +

    このままstepコマンドを実行すると、関数fの中の1行で実行が中断する。一方nextコマンドを使うと、

    (gdb) next

    現在止まっているソースファイルの次の1行の手前まで実行して中断する。途中の関数呼び出しはすべて実行される。

    - +

    stepコマンドと同じく、nextコマンドもn行分一度に実行することができる。

    (gdb) next n

    関数から抜けるまで実行(finish)

    @@ -20212,19 +20619,19 @@

    バックトレース

    (gdb) bt

    バックトレースを表示するにはbacktraceもしくはbtというコマンドを使う。

    例えば以下のようなソースファイルがある。

    - +

    ここで関数fに注目してみよう。関数fは様々な関数から呼ばれる。関数mainから呼ばれるし、関数applebananaからも呼ばれる。特に、関数cherryは関数appleを呼び出すので、間接的に関数fを呼ぶ。

    関数fにブレイクポイントを仕掛けて実行してみよう。

    (gdb) break f
    @@ -20269,12 +20676,12 @@ 

    変数の値を確認

    (gdb) print 式

    printコマンドは式を評価した結果を出力する。

    例えば以下のようなソースファイルがある。

    - +

    この変数xの値を見ていこう。

    まず変数xが初期化されるところまで実行する。

    (gdb) break main
    @@ -20293,12 +20700,12 @@ 

    変数の値を確認

    (gdb) print ++x
     (gdb) print x = 0

    式では関数まで呼べてしまう。

    - +

    このプログラムは関数helloを呼ばないし標準出力には何も出力しない。しかしこのプログラムをGDBでロードし、main関数にブレイクポイントを設定してから実行し、ブレイクポイントが発動したらprint hello()コマンドを使ってみると、

    (gdb) break main
     (gdb) run
    @@ -20308,18 +20715,18 @@ 

    変数の値を確認

    シグナルによるプログラムの中断

    プログラムは様々な理由によりシグナルを出して実行を強制的に終了する。このシグナルはGDBによってトラップされ、ブレイクポイントと同じくプログラムの中断として扱われる。

    プログラムが実行を終了するようなシグナルは、プログラムの不具合によって生じる。具体的な不具合は実行環境に依存するが、大抵の環境で動く不具合は、nullポインターを経由した間接アクセスと、ゼロ除算だ。

    - +

    実際にそのようなプログラムを作ってGDBで実行し、プログラムが中断されることを確認してみよう。

    - +

    このプログラムはユーザーが標準入力から0を入力するとゼロ除算となり強制的に終了する。GDBで実行してみよう。

    $ gdb program
     (gdb) run