From 6a7d61629e53da7bf65f7cfee395a70fe6dd3f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E9=87=8E=E3=80=80=E6=B5=A9=E7=AB=A0=E3=80=80?= Date: Fri, 12 Jul 2019 17:00:25 +0900 Subject: [PATCH] =?UTF-8?q?=E8=A1=A8=E8=A8=98=E3=82=92=E7=B5=B1=E4=B8=80?= =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 000-preface.md | 2 +- 001-intro.md | 18 +- 002-build.md | 86 ++++----- 003-guide-to-c++.md | 61 +++--- 004-debug-compile-error.md | 39 ++-- ...the-restaurant-at-the-end-of-the-branch.md | 39 ++-- 006-debug-compile-warning.md | 20 +- 007-standard-input.md | 24 +-- 008-loop.md | 182 +++++++++--------- 009-vector.md | 48 ++--- 010-debug-printf.md | 39 ++-- 011-integer.md | 75 ++++---- 012-floating-point.md | 36 ++-- 013-names.md | 100 +++++----- 014-iterator.md | 42 ++-- 015-reference.md | 36 ++-- 016-algorithm.md | 68 +++---- 017-lambda.md | 16 +- 018-class.md | 16 +- 019-operator-overloading.md | 83 ++++---- 020-array.md | 4 +- 021-three-virtues-of-a-programmer.md | 8 +- 022-implement-array.md | 25 ++- 023-template.md | 22 +-- 024-more-array.md | 32 ++- 025-array-iterator.md | 118 ++++++------ 026-exception.md | 28 +-- 027-pointer.md | 2 +- 028-pointer-semantics.md | 28 +-- 029-pointer-syntax.md | 162 ++++++++-------- 030-pointer-details.md | 73 +++---- 031-iterator-operations.md | 138 ++++++------- 032-memory-allocation.md | 118 ++++++------ 033-vector-implementation.md | 177 +++++++++-------- 034-vector-memory-allocation.md | 148 +++++++------- 035-copy.md | 85 ++++---- 036-move.md | 40 ++-- 037-rvalue-reference.md | 82 ++++---- 038-move-semantics.md | 52 ++--- 039-disable-copy.md | 8 +- 040-smart-pointer.md | 28 +-- 041-move-support.md | 104 +++++----- 042-string-intro.md | 111 ++++++----- 043-random.md | 106 +++++----- 044-random-part2.md | 32 +-- 045-random-part3.md | 21 +- 200-cpp.md | 169 ++++++++-------- 300-multiple-source-files.md | 87 ++++----- 400-gdb.md | 60 +++--- 49 files changed, 1542 insertions(+), 1556 deletions(-) diff --git a/000-preface.md b/000-preface.md index b45af70..44457aa 100644 --- a/000-preface.md +++ b/000-preface.md @@ -2,6 +2,6 @@ 本書はプログラミング経験者向けのC++入門書である。 -本書の対象読者は、すでに何らかの実用的なプログラミング言語を習得していることを想定し、プログラミングの初歩的な概念はすべて理解しているものとして説明しない。そのため、本書には、「変数は箱のようなものである」といったような説明は出てこない。ただし、主要な他の言語とC++として特別に注意が必要な差は解説している。 +本書の対象読者は、すでに何らかの実用的なプログラミング言語を習得していることを想定し、プログラミングの初歩的な概念はすべて理解しているものとして説明しない。そのため、本書には、「変数は箱のようなものである」といったような説明は出てこない。ただし、主要なほかの言語とC++として特別に注意が必要な差は解説している。 diff --git a/001-intro.md b/001-intro.md index fe3b1df..e7ebfbb 100644 --- a/001-intro.md +++ b/001-intro.md @@ -6,31 +6,31 @@ C++とは何か。C++の原作者にして最初の実装者であるBjarne Stro > > Bjarne Stroustrup, A History of C++: 1979-1991, HOPL2 -プログラミング言語史に詳しくない読者は、Simulaというプログラミング言語について知らないことだろう。Simulaというのは、初めてオブジェクト指向プログラミングを取り入れたプログラミング言語だ。当時と言えばまだ高級なプログラミング言語はほとんどなく、`if else`, `while`などのIBMの提唱した構造化プログラミングを可能にする文法を提供しているプログラミング言語すら、多くは研究段階であった。いわんやオブジェクト指向など、当時はまだアカデミックにおいて可能性の一つとして研究されている程度の地に足のついていない夢の機能であった。そのような粗野な時代において、Simulaは先進的なオブジェクト指向プログラミングを実現していた。 +プログラミング言語史に詳しくない読者は、Simulaというプログラミング言語について知らないことだろう。Simulaというのは、初めてオブジェクト指向プログラミングを取り入れたプログラミング言語だ。当時と言えばまだ高級なプログラミング言語はほとんどなく、`if else`, `while`などのIBMの提唱した構造化プログラミングを可能にする文法を提供しているプログラミング言語すら、多くは研究段階であった。いわんやオブジェクト指向など、当時はまだアカデミックにおいて可能性の1つとして研究されている程度の地に足のついていない夢の機能であった。そのような粗野な時代において、Simulaは先進的なオブジェクト指向プログラミングを実現していた。 オブジェクト指向は現代のプログラミング言語ではすっかり普通になった。データの集合とそのデータに適用する関数を関連付けることができる便利なシンタックスシュガー、つまりプログラミング言語の文法上の機能として定着した。しかし、当時のオブジェクト指向というのはもっと抽象度の高い概念であった。本来のオブジェクト指向をプログラミング言語に落とし込んだ最初の言語として、SimulaとSmalltalkがある。 -Simulaではクラスのオブジェクトひとつひとつが、あたかも並列実行しているかのように振る舞った。Smalltalkでは同一プログラム内のオブジェクトごとのデータのやり取りですらあたかもネットワーク越しに通信をするかのようなメッセージパッシングで行われた。 +Simulaではクラスのオブジェクト1つ1つが、あたかも並列実行しているかのように振る舞った。Smalltalkでは同一プログラム内のオブジェクトごとのデータのやり取りですらあたかもネットワーク越しに通信をするかのようなメッセージパッシングで行われた。 問題は、そのような抽象度の高すぎるSimulaやSmalltalkのようなプログラミング言語の設計と実装では実行速度が遅く、大規模なプログラムを開発するには適さなかった。 -Cの効率性と柔軟性というのは、要するに実行速度が速いとかメモリ消費量が少ないということだ。ではなぜCは他の言語に比べて効率と柔軟に優れているのか。これには2つの理由がある。 +Cの効率性と柔軟性というのは、要するに実行速度が速いとかメモリー消費量が少ないということだ。ではなぜCはほかの言語に比べて効率と柔軟に優れているのか。これには2つの理由がある。 -ひとつ、Cのコードは直接ハードウェアがサポートする命令にまでマッピング可能であるということ。現実のハードウェアにはストレージがあり、メモリがあり、キャッシュがあり、レジスターがあり、命令は投機的に並列実行される泥臭い計算機能を提供している。 +1つ、Cのコードは直接ハードウェアがサポートする命令にまでマッピング可能であるということ。現実のハードウェアにはストレージがあり、メモリーがあり、キャッシュがあり、レジスターがあり、命令は投機的に並列実行される泥臭い計算機能を提供している。 -ひとつ、使わない機能のコストを支払う必要がないというゼロオーバーヘッドの原則。例えばあらゆるメモリ利用がGC(ガベージコレクション)によって管理されている言語では、たとえメモリをすべて明示的に管理していたとしても、GCのコストを支払わなければならない。GCではプログラマーは確保したメモリの解放処理を明示的に書く必要はない。定期的に全メモリを調べて、どこからも使われていないメモリを解放する。この処理には余計なコストがかかる。しかし、いつメモリを解放すべきかがコンパイル時に決定できる場合では、GCは必要ない。GCが存在する言語では、たとえGCが必要なかったとしても、そのコストを支払う必要がある。また実行時にメモリレイアウトを判定して実行時に分岐処理ができる言語では、たとえコンパイル時にメモリレイアウトが決定されていたとしても、実行時にメモリレイアウトを判定して条件分岐するコストを支払わなければならない。 +1つ、使わない機能のコストを支払う必要がないというゼロオーバーヘッドの原則。例えばあらゆるメモリー利用がGC(ガベージコレクション)によって管理されている言語では、たとえメモリーをすべて明示的に管理していたとしても、GCのコストを支払わなければならない。GCではプログラマーは確保したメモリーの解放処理を明示的に書く必要はない。定期的に全メモリーを調べて、どこからも使われていないメモリーを解放する。この処理には余計なコストがかかる。しかし、いつメモリーを解放すべきかがコンパイル時に決定できる場合では、GCは必要ない。GCが存在する言語では、たとえGCが必要なかったとしても、そのコストを支払う必要がある。また実行時にメモリーレイアウトを判定して実行時に分岐処理ができる言語では、たとえコンパイル時にメモリーレイアウトが決定されていたとしても、実行時にメモリーレイアウトを判定して条件分岐するコストを支払わなければならない。 C++は、「アセンブリ言語をおいて、C++より下に言語を置かない」と宣言するほど、ハードウェア機能への直接マッピングとゼロオーバーヘッドの原則を重視している。 -C++の他の特徴としては、委員会方式による国際標準規格を定めていることがある。特定の一個人や一法人が所有する言語は、個人や法人の意思で簡単に仕様が変わってしまう。短期的な利益を追求するために長期的に問題となる変更をしたり、単一の実装が仕様だと言わんばかりの振る舞いをする。特定の個人や法人に所有されていないこと、実装が従うべき標準規格があること、独立した実装が複数あること、言語に利害関係を持つ関係者が議論して投票で変更を可決すること、これがC++が長期に渡って使われてきた理由でもある。 +C++のほかの特徴としては、委員会方式による国際標準規格を定めていることがある。特定の一個人や一法人が所有する言語は、個人や法人の意思で簡単に仕様が変わってしまう。短期的な利益を追求するために長期的に問題となる変更をしたり、単一の実装が仕様だと言わんばかりの振る舞いをする。特定の個人や法人に所有されていないこと、実装が従うべき標準規格があること、独立した実装が複数あること、言語に利害関係を持つ関係者が議論して投票で変更を可決すること、これがC++が長期に渡って使われてきた理由でもある。 委員会方式の規格制定では、下位互換性の破壊は忌避される。なぜならば、既存の動いているコードを壊すということは、それまで存在していた資産の価値を毀損することであり、利害関係を持つ委員が反対するからだ。 -下位互換性を壊した結果何が起こるかと言うと、単に言語が新旧2つに分断される。Python 2とPython 3がその最たる例だ。 +下位互換性を壊した結果何が起こるかというと、単に言語が新旧2つに分断される。Python 2とPython 3がその最たる例だ。 -C++には今日の最新で高級な言語からみれば古風な制約が数多く残っているが、いずれも理由がある。下位互換性を壊すことができないという理由。効率的な実装方法が存在しないという理由。仮に効率的な実装が存在するにしても、様々な環境で実装可能でなければ規格化はできないという理由。 +C++には今日の最新で高級な言語からみれば古風な制約が数多く残っているが、いずれも理由がある。下位互換性を壊すことができないという理由。効率的な実装方法が存在しないという理由。仮に効率的な実装が存在するにしても、さまざまな環境で実装可能でなければ規格化はできないという理由。 -C++には良し悪しがある。Bjarne StroustrupはC++への批判にこう答えている。 +C++には善しあしがある。Bjarne StroustrupはC++への批判にこう答えている。 > 言語には2種類ある。文句を言われる言語と、誰も使わない言語。 diff --git a/002-build.md b/002-build.md index 992e5db..b5645eb 100644 --- a/002-build.md +++ b/002-build.md @@ -1,16 +1,16 @@ # C++の実行 -プログラミング言語を学ぶには、まず書いたソースコードをプログラムとして実行できるようになることが重要だ。自分が正しく理解しているかどうかを確認するために書いたコードが期待通りに動くことを確かめてこそ、正しい理解が確認できる。 +プログラミング言語を学ぶには、まず書いたソースコードをプログラムとして実行できるようになることが重要だ。自分が正しく理解しているかどうかを確認するために書いたコードが期待どおりに動くことを確かめてこそ、正しい理解が確認できる。 ## C++の実行の仕組み C++は慣習的に、ソースファイルをコンパイルしてオブジェクトファイルを生成し、オブジェクトファイルをリンクして実行可能ファイルを生成し、実行可能ファイルを直接実行することで実行する言語だ。 -他の言語では、ソースファイルをそのままパースし、解釈して実行するインタープリター形式の言語が多い。もっとも、今となってはソースファイルから中間言語に変換して、VM(Virtual Machine)と呼ばれる中間言語を解釈して実行するソフトウェア上で実行するとか、JIT(Just-In-Time)コンパイルしてネイティブコードを生成して実行するといった実装もあるため、昔のように単純にインタープリター型の言語ということはできなくなっている事情はある。ただし、最終的にJITコンパイルされてネイティブコードが実行される言語でも、コンパイルやコード生成はプログラマーが意識しない形で行われるため、プログラマーはコンパイラーを直接使う必要のない言語も多い。 +ほかの言語では、ソースファイルをそのままパースし、解釈して実行するインタープリター形式の言語が多い。もっとも、いまとなってはソースファイルから中間言語に変換して、VM(Virtual Machine)と呼ばれる中間言語を解釈して実行するソフトウェア上で実行するとか、JIT(Just-In-Time)コンパイルしてネイティブコードを生成して実行するといった実装もあるため、昔のように単純にインタープリター型の言語ということはできなくなっている事情はある。ただし、最終的にJITコンパイルされてネイティブコードが実行される言語でも、コンパイルやコード生成はプログラマーが意識しない形で行われるため、プログラマーはコンパイラーを直接使う必要のない言語も多い。 C++はプログラマーが直接コンパイラーを使い、ソースファイルをプログラムに変換する言語だ。 -## 簡単な一つのソースファイルからなるプログラムの実行 +## 簡単な1つのソースファイルからなるプログラムの実行 ここでは、典型的なC++のソースファイルをどのようにコンパイルし実行するか、一連の流れを学ぶ。 @@ -64,7 +64,7 @@ int main() C++のソースファイルから、実行可能ファイルを生成するソフトウェアをC++コンパイラーという。C++コンパイラーとしては、GCC(GNU Compiler Collection)とClang(クラン)がある。使い方はどちらもほぼ同じだ。 -GCCを使って先程の`hello.cpp`をコンパイルするには以下のようにする。 +GCCを使って先ほどの`hello.cpp`をコンパイルするには以下のようにする。 ~~~ $ g++ -o hello hello.cpp @@ -72,7 +72,7 @@ $ g++ -o hello hello.cpp GCCという名前のC++コンパイラーなのに`g++`なのは、`gcc`はC言語コンパイラーの名前としてすでに使われているからだ。この慣習はClangも引き継いでいて、ClangのC++コンパイラーは`clang++`だ。 -サンプルコードを間違いなくタイプしていれば、カレントディレクトリに`hello`とぃう実行可能ファイルが作成されるはずだ。確認してみよう。 +サンプルコードを間違いなくタイプしていれば、カレントディレクトリーに`hello`とぃう実行可能ファイルが作成されるはずだ。確認してみよう。 ~~~ $ ls @@ -81,7 +81,7 @@ hello hello.cpp ### 実行 -さて、いよいよ実行だ。通常のOSではカレントディレクトリが`PATH`に含まれていないため、実行するにはカレントディレクトリからパスを指定する必要がある。 +さて、いよいよ実行だ。通常のOSではカレントディレクトリーが`PATH`に含まれていないため、実行するにはカレントディレクトリーからパスを指定する必要がある。 ~~~ $ ./hello @@ -94,7 +94,7 @@ hello GCCはC++のソースファイルからプログラムを生成するC++コンパイラーだ。 -GCCの基本的な使い方は以下の通り。 +GCCの基本的な使い方は以下のとおり。 ~~~ g++ その他のオプション -o 出力するファイル名 ソースファイル名 @@ -103,7 +103,7 @@ g++ その他のオプション -o 出力するファイル名 ソースファ ソースファイル名は複数指定することができる。 ~~~ -g++ -o abc a.cpp b.cpp c.cpp +$ g++ -o abc a.cpp b.cpp c.cpp ~~~ これについては分割コンパイルの章で詳しく解説する。 @@ -118,7 +118,7 @@ g++ -o abc a.cpp b.cpp c.cpp GCCのコンパイラーオプションをいくつか学んでいこう。 -`-std=`はC++の規格を選択するオプションだ。C++17に準拠したいのであれば`-std=c++17`を指定する。読者が本書を読む頃には、C++20や、あるいはもっと未来の規格が発行されているかもしれない。常に最新のC++規格を選択するオプションを指定するべきだ。 +`-std=`はC++の規格を選択するオプションだ。C++17に準拠したいのであれば`-std=c++17`を指定する。読者が本書を読むころには、C++20や、あるいはもっと未来の規格が発行されているかもしれない。常に最新のC++規格を選択するオプションを指定するべきだ。 `-Wall`はコンパイラーの便利な警告メッセージのほとんどすべてを有効にするオプションだ。コンパイラーによる警告メッセージはプログラムの不具合を未然に発見できるので、このオプションは指定すべきだ。 @@ -144,7 +144,7 @@ $ man gcc ### ヘッダーファイルの省略 -先程のソースコードをもう一度見てみよう。冒頭に以下のような行がある。 +先ほどのソースコードをもう一度見てみよう。冒頭に以下のような行がある。 ~~~cpp #include @@ -152,11 +152,11 @@ $ man gcc これは`#includeディレクティブ`(#include directive)といい、プリプロセッサー(preprocessor)の一部だ。プリプロセッサーについて詳しくは煩雑になるので巻末資料を参照してもらうとして、このコードは`iostream`ライブラリを使うために必要で、その意味としてはヘッダーファイル`iostream`の取り込みだ。 -C++の標準ライブラリを使うには、ライブラリごとに対応した`#includeディレクティブ`を書かなければならない。それはあまりにも煩雑なので、本書では標準ライブラリのヘッダーファイルをすべて`#include`した`ヘッダーファイル`(header file)を作成し、それを`#include`することで、`#include`を書かなくてすむようにする。 +C++の標準ライブラリを使うには、ライブラリごとに対応した`#includeディレクティブ`を書かなければならない。それはあまりにも煩雑なので、本書では標準ライブラリのヘッダーファイルをすべて`#include`した`ヘッダーファイル`(header file)を作成し、それを`#include`することで、`#include`を書かなくて済むようにする。 そのためにはまず標準ライブラリのヘッダーファイルのほとんどすべてを`#include`したヘッダーファイル、`all.h`を作成する。 -~~~c++ +~~~cpp #include #include #include @@ -224,7 +224,7 @@ C++の標準ライブラリを使うには、ライブラリごとに対応し using namespace std::literals ; ~~~ -このようなヘッダーファイル`all.h`を作成した後に、ソースファイルで以下のように書けば、他のヘッダーファイルを`#include`する必要がなくなる。 +このようなヘッダーファイル`all.h`を作成したあとに、ソースファイルで以下のように書けば、ほかのヘッダーファイルを`#include`する必要がなくなる。 ~~~cpp #include "all.h" @@ -234,7 +234,7 @@ using namespace std::literals ; `//`から行末まではコメントで、好きなテキストを書くことができる。 -しかし、この最初の一行の`#include`も面倒だ。そこでGCCのオプション`-include`を使い、`all.h`を常に`#include`した扱いにする。 +しかし、この最初の1行の`#include`も面倒だ。そこでGCCのオプション`-include`を使い、`all.h`を常に`#include`した扱いにする。 ~~~ $ g++ -include all.h -o program main.cpp @@ -264,7 +264,7 @@ $ time g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp どうだろうか。読者の環境にもよるが、知覚できるぐらいの時間がかかっているのではないだろうか。プログラミングの習得にはコードを書いてから実行までの時間が短い方がよい。そこで本格的にC++を学ぶ前に、コンパイル時間を短縮する方法を学ぶ。 -プログラムで変更しないファイルを事前にコンパイルしておくと、変更した部分だけコンパイルすれば良いので、コンパイル時間の短縮になる。GCCでは、ヘッダーファイルを事前にコンパイルする特別な機能がある。標準ライブラリのヘッダーファイルは変更しないので、事前にコンパイルしておけばコンパイル時間の短縮になる。 +プログラムで変更しないファイルを事前にコンパイルしておくと、変更した部分だけコンパイルすればよいので、コンパイル時間の短縮になる。GCCでは、ヘッダーファイルを事前にコンパイルする特別な機能がある。標準ライブラリのヘッダーファイルは変更しないので、事前にコンパイルしておけばコンパイル時間の短縮になる。 事前にコンパイルしたヘッダーファイルのことをコンパイル済みヘッダー(precompiled header)という。 @@ -272,10 +272,10 @@ $ time g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp コンパイル済みヘッダーファイルを作成するには、ヘッダーファイル単体をGCCに与え、出力するファイルを`ヘッダーファイル名.gch`とする。ヘッダーファイル名が`all.h`の場合、`all.h.gch`となる。 -GCCのオプションには他のソースファイルをコンパイルするときと同じオプションを与えるほか、ヘッダーファイルがC++で書かれていることを示すオプション`-x c++-header`を与える。 +GCCのオプションにはほかのソースファイルをコンパイルするときと同じオプションを与えるほか、ヘッダーファイルがC++で書かれていることを示すオプション`-x c++-header`を与える。 ~~~ -g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h +$ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h ~~~ こうすると、コンパイル済みヘッダーファイル`all.h.gch`が生成できる。 @@ -283,10 +283,10 @@ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h GCCはヘッダーファイルを使うときに、同名の`.gch`ファイルが存在する場合は、そちらをコンパイル済みヘッダーファイルとして使うことで、ヘッダーファイルの処理を省略する。 ~~~ -g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp +$ g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp ~~~ -コンパイル済みヘッダーは一回のコンパイルにつき一つしか使うことができない。そのため、コンパイル済みヘッダーとするヘッダーファイルを定め、そのヘッダーファイル内に他のヘッダーをすべて記述する。本書ではコンパイル済みヘッダーファイルとする元のヘッダーファイルの名前を`all.h`とする。 +コンパイル済みヘッダーは1回のコンパイルにつき1つしか使うことができない。そのため、コンパイル済みヘッダーとするヘッダーファイルを定め、そのヘッダーファイル内にほかのヘッダーをすべて記述する。本書ではコンパイル済みヘッダーファイルとする元のヘッダーファイルの名前を`all.h`とする。 さっそくコンパイル時間の短縮効果を確かめてみよう。 @@ -314,7 +314,7 @@ $ cat main.cpp int main() { std::cout << "hello"s ; } ~~~ -まず、カレントディレクトリには`all.h`と`main.cpp`がある。この2つのファイルは実行可能ファイルを生成するために必要なファイルだ。今回、その中身は最小限にしてある。本当の`all.h`は、実際には前回書いたように長い内容になる。 +まず、カレントディレクトリーには`all.h`と`main.cpp`がある。この2つのファイルは実行可能ファイルを生成するために必要なファイルだ。今回、その中身は最小限にしてある。本当の`all.h`は、実際には前回書いたように長い内容になる。 ~~~ $ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h @@ -339,7 +339,7 @@ hello 実行可能ファイル`program`を実行する。 -これで読者はC++のプログラミングを学び始めるに当たって必要なことは全て学んだ。さっそくC++を学んでいきたいところだが、その前にもう一つ、ビルドシステムを学ぶ必要がある。 +これで読者はC++のプログラミングを学び始めるにあたって必要なことはすべて学んだ。さっそくC++を学んでいきたいところだが、その前にもう1つ、ビルドシステムを学ぶ必要がある。 ### 依存関係を解決するビルドシステム @@ -349,7 +349,7 @@ hello 一度コンパイルしたプログラムのソースファイルを書き換えて再びコンパイルする場合はどうすればいいだろう。`main.cpp`だけを書き換えた場合、`all.h`は何も変更されていないので、コンパイル済みヘッダーファイル`all.h.gch`の再生成は必要ない。`all.h`だけを書き換えた場合は、`all.h.gch`を生成するだけでなく、`program`も再生成しなければならない。 -プログラムのコンパイルには、このような複雑な依存関係の解決が必要になる。依存関係の解決を人間の手で行うのは大変だ。例えば読者が他人によって書かれた何千ものソースファイルと、プログラムをコンパイルする手順書だけを渡されたとしよう。手順書に従ってコンパイルをしたとして、ソースファイルの一部だけを変更した場合、一体どの手順は省略できるのか、手順書から導き出すのは難しい。するとコンパイルを最初からやり直すべきだろうか。しかし、一つのソースファイルのコンパイルに1秒かかるとして、何千ものソースファイルがある場合、何千秒もかかってしまう。たった一つのソースファイルを変更しただけですべてをコンパイルし直すのは時間と計算資源の無駄だ。 +プログラムのコンパイルには、このような複雑な依存関係の解決が必要になる。依存関係の解決を人間の手で行うのはたいへんだ。例えば読者が他人によって書かれた何千ものソースファイルと、プログラムをコンパイルする手順書だけを渡されたとしよう。手順書に従ってコンパイルをしたとして、ソースファイルの一部だけを変更した場合、いったいどの手順は省略できるのか、手順書から導き出すのは難しい。するとコンパイルを最初からやり直すべきだろうか。しかし、1つのソースファイルのコンパイルに1秒かかるとして、何千ものソースファイルがある場合、何千秒もかかってしまう。たった1つのソースファイルを変更しただけですべてをコンパイルし直すのは時間と計算資源の無駄だ。 この依存関係の問題は、ビルドシステムによって解決できる。本書ではGNU Makeというビルドシステムを学ぶ。読者がこれから学ぶビルドシステムによって、以下のような簡単なコマンドだけで、他人の書いた何千ものソースファイルからなるプログラムがコンパイル可能になる。 @@ -359,17 +359,17 @@ hello $ make ~~~ -これだけだ。`make`というコマンド一つでプログラムのコンパイルは自動的に行われる。 +これだけだ。`make`というコマンド1つでプログラムのコンパイルは自動的に行われる。 -何千ものソースファイルのうち、一つのソースファイルだけを変更し、必要な部分だけを効率よく再コンパイルしたい。 +何千ものソースファイルのうち、1つのソースファイルだけを変更し、必要な部分だけを効率よく再コンパイルしたい。 ~~~ $ make ~~~ -これだけだ。`make`というコマンド一つでプログラムの再コンパイルは自動的に行われる。 +これだけだ。`make`というコマンド1つでプログラムの再コンパイルは自動的に行われる。 -ところで、生成される実行可能ファイルの名前はプログラムごとに様々だ。プログラムの開発中は、共通の方法でプログラムを実行したい。 +ところで、生成される実行可能ファイルの名前はプログラムごとにさまざまだ。プログラムの開発中は、共通の方法でプログラムを実行したい。 ~~~ $ make run @@ -395,14 +395,14 @@ VimはノーマルモードからMakeを呼び出すことができる。もち ### 依存関係を記述するルール -依存関係はどのように表現したらいいのだろうか。GNU makeでは`Makefile`という名前のファイルの中に、`ターゲット`(targets)、`事前要件`(prerequisites)、`レシピ`(recipes)という3つの概念で依存関係を`ルール`(rules)として記述する。`ルール`は以下の文法だ。 +依存関係はどのように表現したらいいのだろうか。GNU Makeでは`Makefile`という名前のファイルの中に、`ターゲット`(targets)、`事前要件`(prerequisites)、`レシピ`(recipes)という3つの概念で依存関係を`ルール`(rules)として記述する。`ルール`は以下の文法だ。 ~~~ ターゲット : 事前要件 [TAB文字]レシピ ~~~ -レシピは必ず`TAB文字`を直前に書かなければならない。スペース文字ではだめだ。これは`make`の初心者を混乱させる落とし穴の一つとなっている。忘れずに`TAB文字`を打とう。 +レシピは必ず`TAB文字`を直前に書かなければならない。スペース文字ではだめだ。これは`make`の初心者を混乱させる落とし穴の1つとなっている。忘れずに`TAB文字`を打とう。 問題を簡単に理解するために、以下のような状況を考えよう。 @@ -451,7 +451,7 @@ Makefile program source これがMakeの仕組みだ。`ターゲット`の生成に必要な`事前要件`と、`ターゲット`を生成する`レシピ`を組み合わせた`ルール`で依存関係を記述する。`make`を実行すると、実行した`レシピ`が表示される。 -もうすこしMakeの`ルール`を追加してみよう。例えばファイル`source`は予め存在するのではなく、ファイル`source01`, `source02`, `source03`の中身をこの順番で連結して生成するとしよう。以下のように書ける。 +もう少しMakeの`ルール`を追加してみよう。例えばファイル`source`はあらかじめ存在するのではなく、ファイル`source01`, `source02`, `source03`の中身をこの順番で連結して生成するとしよう。以下のように書ける。 ~~~makefile program : source @@ -461,7 +461,7 @@ source : source01 source02 source03 cat source01 source02 source03 > source ~~~ -GNU Makeはカレントディレクトリにあるファイル`Makefile`の一番上に書かれたルールを実行しようとする。`program`を生成するには`source`が必要だが、`source`の生成には別のルールの実行が必要だ。`Makefile`はこの依存関係を自動で解決してくれる。 +GNU Makeはカレントディレクトリーにあるファイル`Makefile`の一番上に書かれたルールを実行しようとする。`program`を生成するには`source`が必要だが、`source`の生成には別のルールの実行が必要だ。`Makefile`はこの依存関係を自動で解決してくれる。 ~~~ $ touch source01 source02 source03 @@ -474,16 +474,16 @@ $ ls Makefile program source source01 source02 source03 ~~~ -すでに`make`を実行した後で、もう一度`make`を実行するとどうなるだろうか。 +すでに`make`を実行したあとで、もう一度`make`を実行するとどうなるだろうか。 ~~~ $ make make: 'program' is up to date. ~~~ -このメッセージの意味は「`program`は最新だ」という意味だ。`make`はファイルのタイムスタンプを調べ、もしファイル`program`より`source`のタイムスタンプの方が若い場合、つまり`program`が変更されたよりも後に`source`が変更された場合、`ルール`を実行する。 +このメッセージの意味は「`program`は最新だ」という意味だ。`make`はファイルのタイムスタンプを調べ、もしファイル`program`より`source`のタイムスタンプの方が若い場合、つまり`program`が変更されたよりもあとに`source`が変更された場合、`ルール`を実行する。 -ためしにファイル`source02`のタイムスタンプを更新してみよう。 +試しにファイル`source02`のタイムスタンプを更新してみよう。 ~~~ $ touch source02 @@ -494,7 +494,7 @@ cat source > program ファイル`source`は`事前要件`に`source02`を含む。`source02`のタイムスタンプが`source`より若いので、`source`が再び生成される。すると、`source`のタイムスタンプが`program`のタイムスタンプよりも若くなったので、`program`も生成される。 -もう一つ例を見てみよう。 +もう1つ例を見てみよう。 ~~~ $ touch a b c @@ -502,11 +502,11 @@ $ ls a b c Makefile ~~~ -あるディレクトリにファイル`a`, `b`, `c`がある。 +あるディレクトリーにファイル`a`, `b`, `c`がある。 `Makefile`は以下の内容になっている。 -~~~ +~~~makefile D : A B C cat A B C > D @@ -567,7 +567,7 @@ source : source01 source02 source03 `Makefile`には`変数`を書くことができる。 -変数の文法は以下の通り +変数の文法は以下のとおり。 ~~~ variable = foobar @@ -695,7 +695,7 @@ $ make hello make: 'hello' is up to date. ~~~ -GNU makeはこの問題に対処するため、`.PHONY`ターゲットという特殊な機能がある。これはPHONYターゲットを`.PHONY`ターゲットの事前要件とすることで、ターゲットと同じファイル名の存在の有無にかかわらずルールを実行させられる。 +GNU Makeはこの問題に対処するため、`.PHONY`ターゲットという特殊な機能がある。これはPHONYターゲットを`.PHONY`ターゲットの事前要件とすることで、ターゲットと同じファイル名の存在の有無にかかわらずルールを実行させられる。 ~~~makefile hello : @@ -723,7 +723,7 @@ clean : 以上を踏まえて、C++入門用の環境構築をしてこの章のまとめとする。 -今回構築する環境のファイル名とその意味は以下の通り。 +今回構築する環境のファイル名とその意味は以下のとおり。 `main.cpp` : C++のコードを書く @@ -734,9 +734,9 @@ clean : `program` : 実行可能ファイル `Makefile` -: GNU makeのルールを書く +: GNU Makeのルールを書く -使い方は以下の通り。 +使い方は以下のとおり。 `make` : コンパイルする @@ -754,7 +754,7 @@ gcc_options = -std=c++17 -Wall --pedantic-error 言語はC++17、すべての警告を有効にし、規格準拠ではないコードはエラーとする。 -プログラムをコンパイルする部分は以下の通り。 +プログラムをコンパイルする部分は以下のとおり。 ~~~makefile program : main.cpp all.h all.h.gch @@ -766,7 +766,7 @@ all.h.gch : all.h 実行可能ファイル`program`と、コンパイル済みヘッダー`all.h.gch`をコンパイルするルールだ。 -PHONYターゲットは以下の通り。 +PHONYターゲットは以下のとおり。 ~~~makefile run : program diff --git a/003-guide-to-c++.md b/003-guide-to-c++.md index ab4a8e2..d1514ef 100644 --- a/003-guide-to-c++.md +++ b/003-guide-to-c++.md @@ -1,6 +1,6 @@ # C++ヒッチハイクガイド -プログラミング言語の個々の機能の解説を理解するためには、まず言語の全体像を掴まなければならない。この章ではC++の様々なコードを一通り観光していく。ここではコードの詳細な解説はしない。 +プログラミング言語の個々の機能の解説を理解するためには、まず言語の全体像を掴まなければならない。この章ではC++のさまざまなコードをひと通り観光していく。ここではコードの詳細な解説はしない。 ## 最小のコード @@ -14,7 +14,7 @@ int main(){} ソースコードにコメントを記述して、もう少しわかりやすく書いてみよう。 -~~~ +~~~cpp int // 関数の戻り値の型 main // 関数名 () // 関数の引数 @@ -25,7 +25,7 @@ main // 関数名 `//`から行末まではコメントだ。コメントには好きなことを書くことができる。 -このコードと一つ前のコードは、コメントの有無を別にすれば何の違いもない。このコードで使っている、`int`とか`main`とか記号文字の一つ一つをトークン(token)と呼ぶ。C++ではトークンの間に空白文字や改行文字をいくら使ってもよい。 +このコードと1つ前のコードは、コメントの有無を別にすれば何の違いもない。このコードで使っている、`int`とか`main`とか記号文字の1つ1つをトークン(token)と呼ぶ。C++ではトークンの間に空白文字や改行文字をいくら使ってもよい。 なので、 @@ -54,7 +54,7 @@ main ただし、トークンの途中で空白文字や改行文字を使うことはできない。以下のコードは間違っている。 -~~~c++ +~~~cpp i nt ma in(){} ~~~ @@ -76,15 +76,15 @@ int main() `std::cout`は標準出力を使うためのライブラリだ。 -`<<`は`operator <<`という演算子だ。C++では演算子にも名前がついていて、例えば`+`は`operator +`となる。`<<`も演算子の一種だ。 +`<<`は`operator <<`という演算子だ。C++では演算子にも名前が付いていて、例えば`+`は`operator +`となる。`<<`も演算子の一種だ。 `"hello"s`というのは文字列で、二重引用符で囲まれた中の文字列が標準出力に出力される。 -セミコロン`;`は文の区切り文字だ。C++では文の区切りは明示的にセミコロンを書く必要がある。他の言語では改行文字を文脈から判断して文の区切りとみなすこともあるが、C++では明示的に文の区切り文字としてセミコロンを書かなければならない。 +セミコロン`;`は文の区切り文字だ。C++では文の区切りは明示的にセミコロンを書く必要がある。ほかの言語では改行文字を文脈から判断して文の区切りとみなすこともあるが、C++では明示的に文の区切り文字としてセミコロンを書かなければならない。 セミコロンを書き忘れるとエラーとなる。 -~~~c++ +~~~cpp int main() { // エラー! セミコロンがない @@ -103,7 +103,7 @@ int main() } ~~~ -C++は他の多くの言語と同じように、逐次実行される。つまり、コードは書いた順番に実行される。そして標準出力のような外部への副作用は、実行された順番で出力される。このコードを実行した結果は以下の通り。 +C++はほかの多くの言語と同じように、逐次実行される。つまり、コードは書いた順番に実行される。そして標準出力のような外部への副作用は、実行された順番で出力される。このコードを実行した結果は以下のとおり。 ~~~ one two three @@ -124,7 +124,7 @@ int main() ## 文字列 -二重引用符で囲まれた文字列を、文字通り`文字列`という。文字列には末尾に`s`がつくものとつかないものがある。これには違いがあるのだが、わからないうちは`s`を付けておいたほうが便利だ。 +二重引用符で囲まれた文字列を、文字どおり`文字列`という。文字列には末尾に`s`が付くものと付かないものがある。これには違いがあるのだが、わからないうちは`s`を付けておいた方が便利だ。 ~~~cpp int main() @@ -174,7 +174,7 @@ int main() ## 整数と浮動小数点数 -`iostream`は文字列の他にも、整数や浮動小数点数を出力できる。早速試してみよう。 +`iostream`は文字列のほかにも、整数や浮動小数点数を出力できる。さっそく試してみよう。 ~~~cpp int main() @@ -247,7 +247,7 @@ int main() そういえばC++には文字列もあるのだった。文字列と文字列は足すことができる。数値と数値も足すことができる。では数値と文字列を足すとどうなるのだろう。 -~~~c++ +~~~cpp int main() { std::cout << 1 + "234"s ; @@ -256,7 +256,7 @@ int main() この結果はエラーになる。 -いやまて、C++には末尾に`s`をつけない文字列もあるのだった。これも試してみよう。 +いや待て、C++には末尾に`s`を付けない文字列もあるのだった。これも試してみよう。 ~~~cpp int main() @@ -265,7 +265,7 @@ int main() } ~~~ -結果はなんと`34`になるではないか。C++では謎の数学により`1 + "234" = "34"`であることが判明した。この謎はいずれ解き明かすとして、今は文字列には必ず末尾に`s`をつけることにしよう。そのほうが安全だ。 +結果はなんと`34`になるではないか。C++では謎の数学により`1 + "234" = "34"`であることが判明した。この謎はいずれ解き明かすとして、いまは文字列には必ず末尾に`s`を付けることにしよう。その方が安全だ。 ## 変数(variable) @@ -289,13 +289,13 @@ int main() 変数はキーワード`auto`に続いて変数名を書き、`=`に続いて値を書くことで宣言できる。変数の宣言は文なので、文末にはセミコロンが必要だ。 -~~~ +~~~c++ auto 変数名 = 値 ; ~~~ `変数名`はキーワード、アンダースコア(`_`)で始まる名前、アンダースコア2つ(`__`)を含む名前以外は自由に名付けることができる。 -変数の最初の値は、`= 値`のかわりに`(値)`や`{値}`と書いてもよい。 +変数の最初の値は、`= 値`の代わりに`(値)`や`{値}`と書いてもよい。 ~~~cpp @@ -311,7 +311,7 @@ int main() 変数は使う前に宣言しなければならない。 -~~~c++ +~~~cpp int main() { // エラー、名前xは宣言されていない @@ -320,7 +320,7 @@ int main() } ~~~ -変数の値は初期化した後にも演算子`=`で変更できる。これを`代入`という。 +変数の値は初期化したあとにも演算子`=`で変更できる。これを`代入`という。 ~~~cpp int main() @@ -359,9 +359,9 @@ int main() } ~~~ -`operator =`は「代入」という意味で、「等号」という意味ではないからだ。`x=x+5`は、「`x`と`x+5`は等しい」という独創的な数学上の定義ではなく、「変数xに代入前の変数xの値に5を加えた数を代入する」という意味だ。 +`operator =`は「代入」という意味で、「等号」という意味ではないからだ。`x=x+5`は、「`x`と`x+5`は等しい」という独創的な数学上の定義ではなく、「変数`x`に代入前の変数`x`の値に5を加えた数を代入する」という意味だ。 -変数の今の値に対して演算した結果を変数に代入するという処理はとても良く使うので、C++には`x = x + a`と同じ意味で使える演算子、`operator +=`もある。 +変数のいまの値に対して演算した結果を変数に代入するという処理はとてもよく使うので、C++には`x = x + a`と同じ意味で使える演算子、`operator +=`もある。 ~~~cpp int main() @@ -392,7 +392,7 @@ JavaScriptではこのコードは正しい。変数`x`は数値型であり、 C++ではこのようなコードは書けない。 -~~~c++ +~~~cpp int main() { auto x = 1 ; @@ -404,7 +404,7 @@ int main() C++では、変数`x`は整数型であり、文字列型に変わることはない。整数型の変数に文字列型を代入しようとするとエラーとなる。 -C++では型に名前がついている。整数型は`int`、浮動小数点数型は`double`、文字列型は`std::string`だ。 +C++では型に名前が付いている。整数型は`int`、浮動小数点数型は`double`、文字列型は`std::string`だ。 ~~~cpp int main() @@ -418,10 +418,9 @@ int main() } ~~~ -実は変数の宣言で`auto`と書くかわりに、具体的な型を書いてもよい。 +実は変数の宣言で`auto`と書く代わりに、具体的な型を書いてもよい。 ~~~cpp - int main() { int i = 123 ; @@ -469,11 +468,11 @@ int main() } ~~~ -整数型と浮動小数点数型の挙動については後の章で詳しく解説する。また、これ以外にも型はいくらでもあるし、読者が新しい型を作り出すこともできる。これも後の章で詳しく解説する。 +整数型と浮動小数点数型の挙動についてはあとの章で詳しく解説する。また、これ以外にも型はいくらでもあるし、読者が新しい型を作り出すこともできる。これもあとの章で詳しく解説する。 ## 関数(function) -「変数ぐらい知っている。さっさと教えてもらいたい。どうせC++の関数は書きづらいのだろう」と考える読者の皆さん、おまたせしました。こちらがC++の関数でございます。 +「変数ぐらい知っている。さっさと教えてもらいたい。どうせC++の関数は書きづらいのだろう」と考える読者の皆さん、お待たせしました。こちらがC++の関数でございます。 ~~~cpp int main() @@ -617,19 +616,19 @@ $ make f is called. ~~~ -return文以降の文が実行されていないことがわかる。 +`return`文以降の文が実行されていないことがわかる。 ## 本当の関数 実はラムダ式は本当のC++の`関数`ではない。本当の`関数`はとても書きづらいので心して読むべきだ。 -読者は本書の冒頭で使った`main関数`という言葉を覚えているだろうか。覚えていないとしても、サンプルコードに必ずと行っていいほど出てくる`main`という名前は気になっていたことだろう。 +読者は本書の冒頭で使った`main関数`という言葉を覚えているだろうか。覚えていないとしても、サンプルコードに必ずと言って いいほど出てくる`main`という名前は気になっていたことだろう。 ~~~cpp int main(){} ~~~ -これをみると、聡明な読者はラムダ式と似通ったところがあることに気づくだろう。 +これを見ると、聡明な読者はラムダ式と似通ったところがあることに気付くだろう。 ~~~c++ [](){} @@ -648,7 +647,7 @@ main // 関数名 {} // 関数の本体 ~~~ -ためしに、`int`型の引数を2つ取り足して返す関数`plus`を書いてみよう。 +試しに、`int`型の引数を2つ取り足して返す関数`plus`を書いてみよう。 ~~~cpp int plus( int x, int y ) @@ -682,7 +681,7 @@ int main() しかも、C++の関数は、戻り値の型を正しく返さなければならない。 -~~~c++ +~~~cpp int f() { // エラー、return文がない @@ -700,7 +699,7 @@ void f() ただし、戻り値の型については、具体的な型の代わりに`auto`を書くこともできる。その場合、`return`文で同じ型さえ返していれば、気にする必要はない。 -~~~c++ +~~~cpp // void auto a() { } // int diff --git a/004-debug-compile-error.md b/004-debug-compile-error.md index e7b6943..54a53a1 100644 --- a/004-debug-compile-error.md +++ b/004-debug-compile-error.md @@ -1,8 +1,8 @@ # デバッグ:コンパイルエラーメッセージの読み方 -やれやれ疲れた。この辺で一休みして、デバッグについて考えよう。まずはコンパイルエラーについてだ。 +やれやれ疲れた。この辺でひと休みして、デバッグについて考えよう。まずはコンパイルエラーについてだ。 -プログラムには様々なバグがあるが、コンパイルエラーは最も簡単なバグだ。というのも、プログラムのバグの存在が実行前に発覚したわけだから、手間が省ける。もしコンパイルエラーにならない場合、実行した結果から、バグがあるかどうかを判断しなければならない。 +プログラムにはさまざまなバグがあるが、コンパイルエラーは最も簡単なバグだ。というのも、プログラムのバグの存在が実行前に発覚したわけだから、手間が省ける。もしコンパイルエラーにならない場合、実行した結果から、バグがあるかどうかを判断しなければならない。 読者の中には、せっかく書いたソースコードをコンパイルしたらコンパイルエラーが出たので、運が悪かったとか、失敗したとか、怒られてつらい気持ちになったなどと感じることがあるかもしれない。しかしそれは大違いだ。コンパイラーによって読者はプログラムを実行することなくバグが発見できたのだから、読者は運が良かった、大成功した、褒められて最高の気持ちになったと感じるべきなのだ。 @@ -15,7 +15,7 @@ 熟練のプログラマーは自分の書いたコードがコンパイルエラーを出さずに一発でコンパイルが通った場合、逆に不安になるくらいだ。 -もしバグがあるのにコンパイルエラーが出なければ、バグの存在に気が付かないまま、読者の書いたソフトウェアは広く世の中に使われ、10年後、20年後に最もバグが発見されてほしくない方法で発見されてしまうかもしれない。すなわち、セキュリティ上問題となる脆弱性という形での発覚だ。しかし安心してほしい。今読者が出したコンパイルエラーによって、そのような悲しい未来の可能性は永久に排除されたのだ。コンパイルエラーはどんどん出すとよい。 +もしバグがあるのにコンパイルエラーが出なければ、バグの存在に気が付かないまま、読者の書いたソフトウェアは広く世の中に使われ、10年後、20年後に最もバグが発見されてほしくない方法で発見されてしまうかもしれない。すなわち、セキュリティ上問題となる脆弱性という形での発覚だ。しかし安心してほしい。いま読者が出したコンパイルエラーによって、そのような悲しい未来の可能性は永久に排除されたのだ。コンパイルエラーはどんどん出すとよい。 コンパイルエラーの原因は2つ。 @@ -75,7 +75,7 @@ $ make g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program ~~~ -1行目はシェルに`make`を実行させるためのコマンド、二行目は`make`が実行したレシピの中身だ。これはコンパイラーによるメッセージではない。 +1行目はシェルに`make`を実行させるためのコマンド、2行目は`make`が実行したレシピの中身だ。これはコンパイラーによるメッセージではない。 3行目からはコンパイラーによる出力だ。 @@ -123,7 +123,7 @@ GCCというコンパイラーのエラーメッセージは、以下のフォ auto y = x + 1 ; ~~~ -何の問題もないように見える。更にエラーメッセージを読んでみよう。 +何の問題もないように見える。さらにエラーメッセージを読んでみよう。 列番号が`5`となっている。列番号というのは、行頭からの文字数だ。最初の文字を1文字目とし、文字ごとにインクリメントされていく。 @@ -178,14 +178,14 @@ main.cpp:4:6: error: expected unqualified-id before ‘auto’ これはコンパイルが通るようだ。 -しかしなぜこれでコンパイルが通るのだろう。そのためには、コンパイラーが問題だとした行の一つ上の行を見る必要がある。 +しかしなぜこれでコンパイルが通るのだろう。そのためには、コンパイラーが問題だとした行の1つ上の行を見る必要がある。 ~~~ auto x = 1 + 1 auto y = x + 1 ; ~~~ -コンパイラーにとって、改行は空白文字と同じくソースファイル中の意味のあるトークン(キーワードや名前や記号)を区切る文字でしかない。コンパイラーにとって、このコードは実質以下のように見えてる。 +コンパイラーにとって、改行は空白文字と同じくソースファイル中の意味のあるトークン(キーワードや名前や記号)を区切る文字でしかない。コンパイラーにとって、このコードは実質以下のように見えている。 ~~~ auto x=1+1 auto y=x+1; @@ -193,7 +193,7 @@ auto x=1+1 auto y=x+1; `"1 auto"`というのは文法エラーだ。なのでコンパイラーは文法エラーが発覚する最初の文字である`'auto'`の`'a'`を指摘したのだ。 -人間にとって自然になるように修正すると、コンパイラーが指摘した行の一つ上の行の行末に`';'`を追加すべきだ。 +人間にとって自然になるように修正すると、コンパイラーが指摘した行の1つ上の行の行末に`';'`を追加すべきだ。 ~~~ auto x = 1 + 1 ; @@ -214,7 +214,7 @@ Makefile:4: recipe for target 'program' failed make: *** [program] Error 1 ~~~ -これはGNU makeによるメッセージだ。GCCがソースファイルを正しくコンパイルできず、実行が失敗したとエラーを返したので、レシピの実行が失敗したことを伝えるメッセージだ。 +これはGNU Makeによるメッセージだ。GCCがソースファイルを正しくコンパイルできず、実行が失敗したとエラーを返したので、レシピの実行が失敗したことを伝えるメッセージだ。 プログラムはどうやってエラーを通知するのか。`main`関数の戻り値によってだ。`main`関数は関数であるので、戻り値がある。`main`関数の戻り値は`int`型だ。 @@ -265,7 +265,7 @@ int main() 文法エラーというのは厄介なバグだ。というのも、コンパイラーというのは正しい文法のソースファイルを処理するように作られている。文法を間違えた場合、ソースファイル全体が正しくないということになる。コンパイラーは文法違反に遭遇した場合、なるべく人間がよく間違えそうなパターンをヒューリスティックに指摘することもしている。そのため、エラーメッセージに指摘された行番号と列番号は、必ずしも人間にとっての問題の箇所と一致しない。 -もうひとつ例を見てみよう。 +もう1つ例を見てみよう。 ~~~c++ int main() @@ -278,7 +278,7 @@ int main() } ~~~ -GCCによるコンパイルエラーメッセージだけ抜粋すると以下の通り、 +GCCによるコンパイルエラーメッセージだけ抜粋すると以下のとおり、 ~~~ main.cpp: In function ‘int main()’: @@ -287,7 +287,7 @@ main.cpp:7:40: error: expected ‘;’ before ‘)’ token ^ ~~~ -さて早速読んでみよう。すでに学んだように、GCCのメッセージのフォーマットは以下の通りだ。 +さてさっそく読んでみよう。すでに学んだように、GCCのメッセージのフォーマットは以下のとおりだ。 ~~~ ソースファイル名:行番号:列番号: メッセージの種類: メッセージの内容 @@ -315,13 +315,13 @@ f // 関数名 ; // 終端文字 ~~~ -これを見ると、閉じ括弧が一つ多いことがわかる。 +これを見ると、閉じ括弧が1つ多いことがわかる。 ## 意味エラー 意味エラーとは、ソースファイルは文法的に正しいが、意味的に間違っているコンパイルエラーのことだ。 -早速例を見ていこう。 +さっそく例を見ていこう。 ~~~c++ int main() @@ -330,7 +330,7 @@ int main() } ~~~ -このコードをコンパイルすると出力されるエラーメッセージは以下の通り。 +このコードをコンパイルすると出力されるエラーメッセージは以下のとおり。 ~~~ main.cpp: In function ‘int main()’: @@ -348,7 +348,7 @@ main.cpp:3:18: error: invalid operands of types ‘double’ and ‘double’ to このコードはどうだろう。 ~~~c++ -// 引数を一つ取る関数 +// 引数を1つ取る関数 void f( int x ) { } int main() @@ -361,7 +361,6 @@ int main() このようなエラーメッセージになる。 ~~~ - main.cpp: In function ‘int main()’: main.cpp:7:13: error: too many arguments to function ‘void f(int)’ f( 1, 2 ) ; @@ -371,11 +370,11 @@ main.cpp:2:6: note: declared here ^ ~~~ -問題の箇所は7行目。「関数`'void f(int)'`に対して実引数が多すぎる」とある。`関数f`は引数を一つしか取らないのに、2つの引数を渡しているのがエラーの原因だ。 +問題の箇所は7行目。「関数`'void f(int)'`に対して実引数が多すぎる」とある。`関数f`は引数を1つしか取らないのに、2つの引数を渡しているのがエラーの原因だ。 -2つめのメッセージはエラーではなくて、エラーを補足説明するための注記(note)メッセージだ。ここで言及している`関数f`とは、2行目に宣言されていることを説明してくれている。 +2つ目のメッセージはエラーではなくて、エラーを補足説明するための注記(note)メッセージだ。ここで言及している`関数f`とは、2行目に宣言されていることを説明してくれている。 -意味エラーは時としておぞましいほどのエラーメッセージを生成することがある。例えば以下の一件無害そうなコードだ。 +意味エラーはときとしておぞましいほどのエラーメッセージを生成することがある。例えば以下の一見無害そうなコードだ。 ~~~c++ int main() diff --git a/005-the-restaurant-at-the-end-of-the-branch.md b/005-the-restaurant-at-the-end-of-the-branch.md index 1619aa4..56dde43 100644 --- a/005-the-restaurant-at-the-end-of-the-branch.md +++ b/005-the-restaurant-at-the-end-of-the-branch.md @@ -22,7 +22,7 @@ int main() } ~~~ -複数の`文`を`{}`で囲むことで、一つの文として扱うことができる。これを`複合文`という +複数の`文`を`{}`で囲むことで、1つの文として扱うことができる。これを`複合文`という ~~~cpp int main() @@ -66,7 +66,7 @@ int main() `関数の本体`としての一番外側`'{}'`はこの`複合文`とは別のものだが、読者はまだ気にする必要はない。 -`複合文`は複数の`文`をひとまとめにして、ひとつの`文`として扱えるようにするぐらいの意味しか持っていない。ただし、変数の見え方に影響する。変数は宣言された最も内側の複合文の中でしか使えない。 +`複合文`は複数の`文`をひとまとめにして、1つの`文`として扱えるようにするぐらいの意味しか持っていない。ただし、変数の見え方に影響する。変数は宣言された最も内側の複合文の中でしか使えない。 ~~~cpp @@ -112,12 +112,12 @@ int main() } ~~~ -なれないうちは驚くかもしれないが、多くのプログラミング言語はこのような挙動になっているものだ。 +慣れないうちは驚くかもしれないが、多くのプログラミング言語はこのような挙動になっているものだ。 ## 条件分岐 -すでに読者は様々な数値計算を学んだ。読者は`12345 + 6789`の答えや、`8073 * 132 / 5`の答えを計算できる上、この2つの答えをさらにかけ合わせた結果だって計算できる。 +すでに読者はさまざまな数値計算を学んだ。読者は`12345 + 6789`の答えや、`8073 * 132 / 5`の答えを計算できる上、この2つの答えをさらに掛け合わせた結果だって計算できる。 ~~~cpp int main() @@ -159,7 +159,7 @@ int main() `if文`は以下のように書く。 -~~~ +~~~c++ if ( 条件 ) 文1 else @@ -170,13 +170,13 @@ else elseの部分は書かなくてもよい。 -~~~ +~~~c++ if ( 条件 ) 文1 文2 ~~~ -その場合、`条件`が真の時だけ`文1`が実行される。条件の真偽にかかわらず`文2`は実行される。 +その場合、`条件`が真のときだけ`文1`が実行される。条件の真偽にかかわらず`文2`は実行される。 ~~~cpp int main() @@ -202,7 +202,7 @@ int main() } ~~~ -`条件`とか`真偽`についてはとてもとても深い話があるのだが、その解説は後の章に回すとして、まずは以下の比較演算子を覚えよう。 +`条件`とか`真偽`についてはとてもとても深い話があるのだが、その解説はあとの章に回すとして、まずは以下の比較演算子を覚えよう。 演算子 意味 -------- --------- @@ -337,7 +337,7 @@ int main() } ~~~ -これを実行すると、aaと出力される。すると`"aa"s`は`"ab"s`より小さいことになる。 +これを実行すると、`aa`と出力される。すると`"aa"s`は`"ab"s`より小さいことになる。 文字列の大小比較は文字単位で行われる。まず最初の文字が大小比較される。もし等しい場合は、次の文字が大小比較される。等しくない最初の文字の結果が、文字列の大小比較の結果となる。 @@ -626,7 +626,7 @@ int main() } ~~~ -比較演算子の結果はbool値になるということを覚えてるだろうか。`"1 \< 2"`は`true`になり、`"1 \> 2"`は`false`になる。 +比較演算子の結果は`bool`値になるということを覚えているだろうか。`"1 \< 2"`は`true`になり、`"1 \> 2"`は`false`になる。 `bool`値同士も同値比較ができるということは、`"(1 \< 2) == true"`のように書くことも可能だということだ。 @@ -653,7 +653,7 @@ int main() `true && false` `false` `true && true` `true` -早速確かめてみよう。 +さっそく確かめてみよう。 ~~~cpp int main() @@ -717,7 +717,7 @@ int main() `true || false` `true` `true || true` `true` -早速確かめてみよう。 +さっそく確かめてみよう。 ~~~cpp int main() @@ -769,7 +769,7 @@ int main() 論理積と論理和は短絡評価と呼ばれる特殊な評価が行われる。これは、左から右に最小限の評価をするという意味だ。 -論理積では、"a && b"とある場合、`a`と`b`が共に`true`である場合のみ、結果は`true`になる。もし、`a`が`false`であった場合、`b`の結果如何にかかわらず結果は`false`となるので、`b`は評価されない。 +論理積では、"`a` && `b`"とある場合、`a`と`b`がともに`true`である場合のみ、結果は`true`になる。もし、`a`が`false`であった場合、`b`の結果如何にかかわらず結果は`false`となるので、`b`は評価されない。 ~~~cpp int main() @@ -834,9 +834,9 @@ true `bool`型の値と演算はこれで全部だ。値は`true`/`false`の2つのみ。演算は`==`, `!=`, `!`と`&&`と`||`の5つだけだ。 -読者の中には納得の行かないものもいるだろう。ちょっとまってもらいたい。`bool`の大小比較できないのだろうか。`bool`の四則演算はできないのか。`"if(123)"`などと書けてしまうのはなんなのか。 +読者の中には納得のいかないものもいるだろう。ちょっと待ってもらいたい。`bool`の大小比較できないのだろうか。`bool`の四則演算はできないのか。`"if(123)"`などと書けてしまうのはなんなのか。 -好奇心旺盛な読者は本書の解説を待たずしてすでに自分で色々とコードを書いて試してしまっていることだろう。 +好奇心旺盛な読者は本書の解説を待たずしてすでに自分でいろいろとコードを書いて試してしまっていることだろう。 `bool`の大小比較はどうなるのだろうか。 @@ -855,7 +855,6 @@ int main() 四則演算はどうか? ~~~cpp - int main() { auto print = [](auto x) @@ -877,9 +876,9 @@ int main() 0 ~~~ -不思議な結果だ。`"true+true"`は`"2"`、`"true+false"`は`"1"`、`"false+false"`は`"0"`。これは`true`が`1`で`false`が`0`ならば納得の行く結果だ。大小比較の結果としても矛盾していない。 +不思議な結果だ。`"true+true"`は`"2"`、`"true+false"`は`"1"`、`"false+false"`は`"0"`。これは`true`が`1`で`false`が`0`ならば納得のいく結果だ。大小比較の結果としても矛盾していない。 -すでに見たように、`std::boolalpha`を出力していない状態でboolを出力すると`true`が`1`、`false`が`0`となる。 +すでに見たように、`std::boolalpha`を出力していない状態で`bool`を出力すると`true`が`1`、`false`が`0`となる。 ~~~cpp int main() @@ -896,7 +895,7 @@ int main() これは`bool型`と`整数型`が変換されているのだ。 -ことなる型の値が変換されるというのは、すでに例がある。`整数型`と`浮動小数点数型`だ。 +異なる型の値が変換されるというのは、すでに例がある。`整数型`と`浮動小数点数型`だ。 ~~~cpp int main() @@ -917,7 +916,7 @@ int main() `bool`型の`true`を`整数型`に変換すると`1`になる。`false`は`0`になる。 -~~~ +~~~cpp int main() { // 1 diff --git a/006-debug-compile-warning.md b/006-debug-compile-warning.md index 32cccc3..918cb88 100644 --- a/006-debug-compile-warning.md +++ b/006-debug-compile-warning.md @@ -1,12 +1,12 @@ # デバッグ: コンパイル警告メッセージ -やれやれ、条件分岐は難しかった。この辺でもう一度一休みして、息抜きとしてデバッグの話をしよう。今回はコンパイラーの警告メッセージ(warning messages)についてだ。 +やれやれ、条件分岐は難しかった。この辺でもう一度ひと休みして、息抜きとしてデバッグの話をしよう。今回はコンパイラーの警告メッセージ(warning messages)についてだ。 コンパイラーはソースコードに文法エラーや意味エラーがあると、エラーメッセージを出すことはすでに学んだ。 -コンパイラーがエラーメッセージを出さなかったとき、コンパイラーはソースコードには文法エラーや意味エラーを発見できず、コンパイラーは意味のあるプログラムを生成することができたということを意味する。しかし、コンパイルが通って実行可能なプログラムが生成できたからと行って、プログラムにバグがないことは保証できない。 +コンパイラーがエラーメッセージを出さなかったとき、コンパイラーはソースコードには文法エラーや意味エラーを発見できず、コンパイラーは意味のあるプログラムを生成することができたということを意味する。しかし、コンパイルが通って実行可能なプログラムが生成できたからといって、プログラムにバグがないことは保証できない。 -たとえば、変数xとyを足して出力するプログラムを考える。 +たとえば、変数`x`と`y`を足して出力するプログラムを考える。 ~~~cpp int main() @@ -22,7 +22,7 @@ int main() コンパイラーはこのソースコードをコンパイルエラーにはしない。なぜならば上のコードは文法的に正しく、意味的にも正しいコードだからだ。 -警告メッセージはこのような疑わしいコードについて、エラーとまでは行かないまでも、文字通り警告を出す機能だ。例えば上のコードをGCCでコンパイルすると以下のような警告メッセージを出す +警告メッセージはこのような疑わしいコードについて、エラーとまではいかないまでも、文字どおり警告を出す機能だ。例えば上のコードをGCCでコンパイルすると以下のような警告メッセージを出す。 ~~~ $ make @@ -54,7 +54,7 @@ $ g++ -Wunused-variable その他のオプション 今回は`-Wall`というすべての警告を有効にするオプションを使っているので、このオプションを使う必要はない。 -もう一つ例を出そう。以下のソースコードは変数`x`の値が`123`と等しいかどうかを調べるものだ。 +もう1つ例を出そう。以下のソースコードは変数`x`の値が`123`と等しいかどうかを調べるものだ。 ~~~cpp int main() @@ -70,7 +70,7 @@ int main() } ~~~ -これを実行すると、`"x is 123.\n"`と出力される。しかし、変数`x`の値は`0`のはずだ。なぜか`0`と`123`は等しいと判断されてしまった。一体どういうことだろう。 +これを実行すると、`"x is 123.\n"`と出力される。しかし、変数`x`の値は`0`のはずだ。なぜか`0`と`123`は等しいと判断されてしまった。いったいどういうことだろう。 この謎は警告メッセージを読むと解ける。 @@ -82,7 +82,7 @@ main.cpp:5:12: warning: suggest parentheses around assignment used as truth valu ~~^~~~~ ~~~ -`main.cpp`の5行目の12列目、「真偽値として使われている代入は括弧で囲むべき」とある。これは一体どういうことか。よく見てみると、演算子が同値比較につかう`==`ではなく、`=`だ。`=`は代入演算子だ。 +`main.cpp`の5行目の12列目、「真偽値として使われている代入は括弧で囲むべき」とある。これはいったいどういうことか。よく見てみると、演算子が同値比較に使う`==`ではなく、`=`だ。`=`は代入演算子だ。 ~~~cpp int main() @@ -118,7 +118,7 @@ int main() つまり、`"if(x=1)"`というのは、`"if(1)"`と書くのと同じで、これは最終的に、`"if(true)"`と同じ意味になる。 -警告メッセージの「括弧で囲むべき」というのは、括弧で囲んだ場合、この警告メッセージはでなくなるからだ。 +警告メッセージの「括弧で囲むべき」というのは、括弧で囲んだ場合、この警告メッセージは出なくなるからだ。 ~~~cpp int main() @@ -132,11 +132,11 @@ int main() } ~~~ -このコードをコンパイルしても警告メッセージはでない。 +このコードをコンパイルしても警告メッセージは出ない。 わざわざ括弧で囲むということは、ちゃんと代入を意図して使っていることがわかっていると意思表示したことになり、結果として警告メッセージはなくなる。 この警告メッセージ単体を有効にするオプションは`-Wparentheses`だ。 -警告メッセージは万能ではない。時には全く問題ないコードに対して警告メッセージがでたりする。これは仕方がないことだ。というのもコンパイラーはソースコード中に表現されていない、人間の脳内にある意図を読むことはできないからだ。ただし、警告メッセージには一通り目を通して、それが問題ない誤検知であるかどうかを確認することは重要だ。 +警告メッセージは万能ではない。ときにはまったく問題ないコードに対して警告メッセージが出たりする。これは仕方がないことだ。というのもコンパイラーはソースコード中に表現されていない、人間の脳内にある意図を読むことはできないからだ。ただし、警告メッセージにはひと通り目を通して、それが問題ない誤検知であるかどうかを確認することは重要だ。 diff --git a/007-standard-input.md b/007-standard-input.md index 505ec39..1a84319 100644 --- a/007-standard-input.md +++ b/007-standard-input.md @@ -6,7 +6,7 @@ 最近肥満が気になる読者は、肥満度を把握するためにBMI(Body Mass Index)を計算して出力するプログラムを書くことにした。 -BMIの計算は以下の通り。 +BMIの計算は以下のとおり。 $$ BMI = \frac{体重_{kg}}{身長^2_{m}} @@ -39,7 +39,7 @@ BMI 状態 25以上、30未満 太り気味(Overweight) 30以上 肥満(Obese) -では早速、この表のようにBMIから肥満状態も出力してくれるように、プログラムを書き換えよう。 +ではさっそく、この表のようにBMIから肥満状態も出力してくれるように、プログラムを書き換えよう。 ~~~cpp int main() @@ -73,11 +73,11 @@ int main() } ~~~ -ここまで問題なく読むことができただろうか。ここまでのコードはすべて、本書をはじめからから読めば理解できる機能しか使っていない。わからない場合、この先に進む前に本書をもう一度はじめから読みなすべきだろう。 +ここまで問題なく読むことができただろうか。ここまでのコードはすべて、本書を始めから読めば理解できる機能しか使っていない。わからない場合、この先に進む前に本書をもう一度始めから読みなすべきだろう。 ## 標準入力 -上のプログラムには実用にする上で一つ問題がある。身長と体重の値を変えたい場合、ソースコードを書き換えてコンパイルしなければならないのだ。 +上のプログラムには実用にする上で1つ問題がある。身長と体重の値を変えたい場合、ソースコードを書き換えてコンパイルしなければならないのだ。 例えば読者の身長が1.8mで体重が80kgの場合、以下のように書き換えなければならない。 @@ -155,7 +155,7 @@ helloworld 空白文字は文字列の区切り文字として認識されるので変数`x`, `y`には入らない。 -`std::cin`では文字列の他にも整数や浮動小数点数、`bool`を入力として得ることができる。 +`std::cin`では文字列のほかにも整数や浮動小数点数、`bool`を入力として得ることができる。 ~~~cpp @@ -260,7 +260,7 @@ int main() ## リダイレクト -標準入出力が扱えるようになれば、もう自分の好きなプログラムを書くことができる。プログラムというのは結局、入力を得て、処理して、出力するだけのものだからだ。入力はテキストだったりグラフィックだったり何らかの特殊なデバイスだったりするが、基本は変わらない。 +標準入出力が扱えるようになれば、もう自分の好きなプログラムを書くことができる。プログラムというのはけっきょく、入力を得て、処理して、出力するだけのものだからだ。入力はテキストだったりグラフィックだったり何らかの特殊なデバイスだったりするが、基本は変わらない。 たとえば読者はまだC++でファイルを読み書きする方法を知らないが、標準入出力さえ使えれば、ファイルの読み書きはリダイレクトを使うだけでできるのだ。 @@ -287,9 +287,9 @@ $ cat hello.txt hello ~~~ -ファイルへの簡単な書き込みは、リダイレクトを使うことで後から簡単に実現可能だ。 +ファイルへの簡単な書き込みは、リダイレクトを使うことであとから簡単に実現可能だ。 -リダイレクトはファイルの読み込みにも使える。例えば先程のBMIを計算するプログラムを用意しよう。 +リダイレクトはファイルの読み込みにも使える。例えば先ほどのBMIを計算するプログラムを用意しよう。 ~~~cpp @@ -345,7 +345,7 @@ $ cat index.text プログラムが出力した結果をさらに入力にすることだってできる。 -例えば、先程のプログラム`bmi`に入力するファイル`bodymass.txt`の身長の単位がメートルではなくセンチメートルだったとしよう。 +例えば、先ほどのプログラム`bmi`に入力するファイル`bodymass.txt`の身長の単位がメートルではなくセンチメートルだったとしよう。 ~~~ 163 @@ -401,7 +401,7 @@ $ ./convert < bodymass.txt | ./bmi 27.4756 ~~~ -ところで、すでに何度か説明無しで使っているが、POSIX規格を満たすOSには`cat`というプログラムが標準で入っている。`cat ファイル名`は指定したファイル名の内容を標準出力する。標準出力はパイプで標準入力にできる。 +ところで、すでに何度か説明なしで使っているが、POSIX規格を満たすOSには`cat`というプログラムが標準で入っている。`cat ファイル名`は指定したファイル名の内容を標準出力する。標準出力はパイプで標準入力にできる。 ~~~ $ cat bodymass.txt | ./convert | ./bmi @@ -412,7 +412,7 @@ $ cat bodymass.txt | ./convert | ./bmi 現代のプログラミングというのは、すでに存在するプログラムを組み合わせて作るものだ。もし、自分の必要とする処理がすでに実装されているのであれば、自分で書く必要はない。 -例えば、読者はまだカレントディレクトリー下のファイルの一覧を列挙する方法を知らない。しかしPOSIX規格を満たすOSには`ls`というカレントディレクトリー下のファイルの一覧を列挙するプログラムが存在する。これを先程までBMIの計算などの作業をしていたディレクトリ下で実行してみよう。 +例えば、読者はまだカレントディレクトリー下のファイルの一覧を列挙する方法を知らない。しかしPOSIX規格を満たすOSには`ls`というカレントディレクトリー下のファイルの一覧を列挙するプログラムが存在する。これを先ほどまでBMIの計算などの作業をしていたディレクトリー下で実行してみよう。 ~~~ $ ls @@ -440,4 +440,4 @@ $ curl https://example.com | ./program 読者はC++でネットワークアクセスする方法を知らないが、すでにネットワークアクセスは可能になった。 -他にも便利なプログラムはたくさんある。プログラミングの学び始めはできることが少なくて退屈になりがちだが、読者はもうファイルの読み書きやネットワークアクセスまでできるようになったのだから、退屈はしないはずだ。 +ほかにも便利なプログラムはたくさんある。プログラミングの学び始めはできることが少なくて退屈になりがちだが、読者はもうファイルの読み書きやネットワークアクセスまでできるようになったのだから、退屈はしないはずだ。 diff --git a/008-loop.md b/008-loop.md index 4fa8b0f..f3349cc 100644 --- a/008-loop.md +++ b/008-loop.md @@ -1,6 +1,6 @@ # ループ -さて、ここまでで変数や関数、標準入出力といったプログラミングの基礎的な概念を教えてきた。あと一つでプログラミングに必要な基礎的な概念はすべて説明し終わる。ループだ。 +さて、ここまでで変数や関数、標準入出力といったプログラミングの基礎的な概念を教えてきた。あと1つでプログラミングに必要な基礎的な概念はすべて説明し終わる。ループだ。 ## これまでのおさらい @@ -53,13 +53,13 @@ int main() ## goto文 -ここでは繰り返し(ループ)の基礎的な仕組みを理解するために、最も原始的で最も使いづらい繰り返しの機能である`goto文`を学ぶ。`goto文`で実用的な繰り返し処理をするのは面倒だが、恐れることはない。より簡単な方法もすぐに説明するからだ。なぜ本書で`goto文`を先に教えるかと言うと、あらゆる繰り返しは、結局のところ`if文`と`goto文`へのシンタックスシュガーにすぎないからだ。`goto文`を学ぶことにより、繰り返しを恐れることなく使う本物のプログラマーになれる。 +ここでは繰り返し(ループ)の基礎的な仕組みを理解するために、最も原始的で最も使いづらい繰り返しの機能である`goto文`を学ぶ。`goto文`で実用的な繰り返し処理をするのは面倒だが、恐れることはない。より簡単な方法もすぐに説明するからだ。なぜ本書で`goto文`を先に教えるかというと、あらゆる繰り返しは、けっきょくのところ`if文`と`goto文`へのシンタックスシュガーにすぎないからだ。`goto文`を学ぶことにより、繰り返しを恐れることなく使う本物のプログラマーになれる。 ### 無限ループ `"hello\n"`と3回出力するプログラムはどうやって書くのだろうか。`"hello\n"`を1回出力するプログラムの書き方はすでにわかっているので、同じ文を3回書けばよい。 -~~~ +~~~cpp // 1回"hello\n"を出力する関数 void hello() { @@ -76,7 +76,7 @@ int main() 10回出力する場合はどうするのだろう。10回書けばよい。コードは省略する。 -では100回出力する場合はどうするのだろう。100回書くのだろうか。100回も同じコードを書くのはとても面倒だ。読者がVimのような優秀なテキストエディターを使っていない限り100回も同じコードを間違えずに書くことは不可能だろう。Vimならば1回書いた後にノーマルモードで`"100."`するだけで100回書ける。 +では100回出力する場合はどうするのだろう。100回書くのだろうか。100回も同じコードを書くのはとても面倒だ。読者がVimのような優秀なテキストエディターを使っていない限り100回も同じコードを間違えずに書くことは不可能だろう。Vimならば1回書いたあとにノーマルモードで`"100."`するだけで100回書ける。 実際のところ、100回だろうが、1000回だろうが、あらかじめ回数がコンパイル時に決まっているのであれば、その回数だけ同じ処理を書くことで実現可能だ。 @@ -96,7 +96,7 @@ hello `goto文`は指定したラベルに実行を移す機能だ。 -~~~ +~~~c++ ラベル名 : 文 goto ラベル名 ; @@ -155,7 +155,7 @@ loop : ひたすら同じ文字列を出力し続けるだけのプログラムというのも味気ない。もっと面白くてためになるプログラムを作ろう。例えば、ユーザーから入力された数値を合計し続けるプログラムはどうだろう。 -今から作るプログラムを実行すると以下のようになる。 +いまから作るプログラムを実行すると以下のようになる。 ~~~ $ make run @@ -175,7 +175,7 @@ $ make run 2. これまでの入力との合計値を出力 3. 1.に戻る -という動作を繰り返す。先程学んだ無限ループと同じだ。 +という動作を繰り返す。先ほど学んだ無限ループと同じだ。 さっそく作っていこう。 @@ -202,7 +202,7 @@ loop : `"sum = sum + input()"`は、変数`sum`に新しい値を代入するもので、その代入する値というのは、代入する前の変数`sum`の値と関数`input`の戻り値を足した値だ。 -このような変数xに何らかの値nを足した結果を元の変数xに代入するという処理はとても多く使われるので、C++では`"x = x + n"`を意味する省略記法`"x += n"`がある。 +このような変数`x`に何らかの値`n`を足した結果を元の変数`x`に代入するという処理はとても多く使われるので、C++では`"x = x + n"`を意味する省略記法`"x += n"`がある。 ~~~cpp int main() @@ -271,7 +271,7 @@ int main() 関数というのは宣言と定義に分かれている。 -~~~cpp +~~~c++ // 関数の宣言 void f( ) ; @@ -283,7 +283,7 @@ void f( ) 関数の宣言というのは何度書いても大丈夫だ。 -~~~cpp +~~~c++ // 宣言 int f( int x ) ; @@ -296,7 +296,7 @@ int f( int x ) ; 関数の宣言というのは戻り値の型や関数名や引数リストだけで、`";"`で終わる。 -関数の定義とは、関数の宣言の後の`"{}"`だ。この場合、宣言のあとに`";"`は書かない。 +関数の定義とは、関数の宣言のあとの`"{}"`だ。この場合、宣言のあとに`";"`は書かない。 ~~~cpp int f( int x ) { return x ; } @@ -311,9 +311,9 @@ void f() {} void f() {} ~~~ -なぜ関数は宣言と定義とに別れているかというと、C++では名前は宣言しないと使えないためだ。 +なぜ関数は宣言と定義とに分かれているかというと、C++では名前は宣言しないと使えないためだ。 -~~~c++ +~~~cpp int main() { // エラー @@ -341,7 +341,7 @@ int main() void f() { } ~~~ -さて、話をもとに戻そう。これから学ぶのは$n$回`"hello\n"s`と出力するプログラムの書き方だ。ただし$n$はユーザーが入力するので実行時にしかわからない。すでに我々はユーザーから$n$の入力を受け取る部分のプログラムは書いた。 +さて、話を元に戻そう。これから学ぶのは$n$回`"hello\n"s`と出力するプログラムの書き方だ。ただし$n$はユーザーが入力するので実行時にしかわからない。すでに我々はユーザーから$n$の入力を受け取る部分のプログラムは書いた。 ~~~cpp // n回出力する関数の宣言 @@ -372,7 +372,7 @@ loop : 終了条件付きループで学んだように、このループを$n$回繰り返した場合に終了させるには、`if文`を使って、終了条件に達したかどうかで実行を分岐させればよい。 -~~~c++ +~~~cpp void hello_n( int n ) { loop : @@ -387,7 +387,7 @@ loop : このコードを完成させるにはどうすればいいのか。まず、現在何回繰り返しを行ったのか記録する必要がある。このために変数を作る。 -~~~c++ +~~~cpp int i = 0 ; ~~~ @@ -395,19 +395,19 @@ int i = 0 ; 1回繰り返し実行をするたびに、変数`i`の値を1増やす。 -~~~c++ +~~~cpp i = i + 1 ; ~~~ これはすでに学んだように、もっと簡単に書ける。 -~~~c++ +~~~cpp i += 1 ; ~~~ -実は、更に簡単に書くこともできる。変数の代入前の値に1を足した値を代入する、つまり変数の値を1増やすというのはとてもよく書くコードなので、とても簡単な演算子が用意されている。`operator ++`だ。 +実は、さらに簡単に書くこともできる。変数の代入前の値に1を足した値を代入する、つまり変数の値を1増やすというのはとてもよく書くコードなので、とても簡単な演算子が用意されている。`operator ++`だ。 -~~~c++ +~~~cpp int main() { int i = 0 ; @@ -421,7 +421,7 @@ int main() インクリメントと対になるのがデクリメント(decrement)だ。これは変数の値を1減らす。演算子は`operator --`だ。 -~~~c++ +~~~cpp int main() { int i = 0 ; @@ -431,7 +431,7 @@ int main() } ~~~ -さて、必要な知識は学び終えたので本題に戻ろう。$n$回の繰り返しをした後にループを終了するには、まず今何回繰り返し実行しているのかを記録する必要がある。その方法を学ぶために、0, 1, 2, 3, 4...と無限に出力されるプログラムを書いてみよう。 +さて、必要な知識は学び終えたので本題に戻ろう。$n$回の繰り返しをしたあとにループを終了するには、まずいま何回繰り返し実行しているのかを記録する必要がある。その方法を学ぶために、0, 1, 2, 3, 4...と無限に出力されるプログラムを書いてみよう。 このプログラムを実行すると以下のように表示される。 @@ -470,7 +470,7 @@ loop : ここまでくればしめたもの。あとは`goto文`を実行するかどうかを`if文`で条件分岐すればよい。しかし、`if文`の中にどんな条件を書けばいいのだろうか。 -~~~c++ +~~~cpp void hello_n( int n ) { int i = 0 ; @@ -487,10 +487,10 @@ loop : 具体的に考えてみよう。`n == 3`のとき、つまり3回繰り返すときを考えよう。 -1. 1回目の`if`文実行の時、`i == 0` -2. 2回目の`if`文実行の時、`i == 1` -3. 3回目の`if`文実行の時、`i == 2` -4. 4回目の`if`文実行の時、`i == 3` +1. 1回目の`if`文実行のとき、`i == 0` +2. 2回目の`if`文実行のとき、`i == 1` +3. 3回目の`if`文実行のとき、`i == 2` +4. 4回目の`if`文実行のとき、`i == 3` ここでは`n == 3`なので、3回まで実行してほしい。つまり3回目までは`true`になり、4回目の`if`文実行のときには`false`になるような式を書く。そのような式とは、ズバリ`"i != n"`だ。 @@ -509,7 +509,7 @@ loop : } ~~~ -早速実行してみよう。 +さっそく実行してみよう。 ~~~ $ make run @@ -523,7 +523,7 @@ hello hello ~~~ -なるほど、動くようだ。しかしこのプログラムにはバグがある。`-1`を入力すると、何故か大量の`hello`が出力されてしまうのだ。 +なるほど、動くようだ。しかしこのプログラムにはバグがある。`-1`を入力すると、なぜか大量の`hello`が出力されてしまうのだ。 ~~~ $ make run @@ -535,7 +535,7 @@ hello [Ctrl-C] ~~~ -この原因はまだ現時点の読者には難しい。この謎はいずれ明らかにするとして、今は`n`が負数の場合にプログラムを0回の繰り返し分の実行で終了するように書き換えよう。 +この原因はまだ現時点の読者には難しい。この謎はいずれ明らかにするとして、いまは`n`が負数の場合にプログラムを0回の繰り返し分の実行で終了するように書き換えよう。 ~~~cpp @@ -559,7 +559,7 @@ loop : ## while文 -`goto文`は極めて原始的で使いづらい機能だ。現実のC++プログラムでは`goto文`はめったに使われない。もっと簡単な機能を使う。ではなぜ`goto文`が存在するかというと、`goto文`は最も原始的で基礎的で、他の繰り返し機能は`if文`と`goto文`に変換することで実現できるからだ。 +`goto文`は極めて原始的で使いづらい機能だ。現実のC++プログラムでは`goto文`はめったに使われない。もっと簡単な機能を使う。ではなぜ`goto文`が存在するかというと、`goto文`は最も原始的で基礎的で、ほかの繰り返し機能は`if文`と`goto文`に変換することで実現できるからだ。 `goto文`より簡単な繰り返し文に、`while文`がある。ここでは`goto文`と`while文`を比較することで、`while文`を学んでいこう。 @@ -584,7 +584,7 @@ loop : そこで`while(true)`だ。`while(true)`は`goto文`と`ラベル文`よりも簡単に無限ループを実現できる。 -~~~ +~~~c++ while (true) 文 ~~~ @@ -603,14 +603,14 @@ int main() このコードの重要な部分は以下の2行。 -~~~c++ +~~~cpp while (true) hello() ; ~~~ これを`goto文`と`ラベル文`を使った無限ループと比べてみよう。 -~~~ +~~~cpp loop: hello() ; goto loop ; @@ -643,7 +643,7 @@ int main() 重要なのは以下の4行だ。 -~~~c++ +~~~cpp while( true ) { sum += input() ; @@ -653,23 +653,23 @@ while( true ) これを`goto文`で書いた場合と比べてみよう。 -~~~c++ +~~~cpp loop : sum += input() ; std::cout << sum << "\n"s ; goto loop ; ~~~ -本当に重要で本質的な、繰り返し実行をする部分の2行のコードは全く変わっていない。それでいて`while(true)`の方が圧倒的に簡単に書ける。 +本当に重要で本質的な、繰り返し実行をする部分の2行のコードはまったく変わっていない。それでいて`while(true)`の方が圧倒的に簡単に書ける。 ## 終了条件付きループ -なるほど、無限ループを書くのに、`goto文`を使うより`while(true)`を使ったほうがいいことがわかった。では他のループの場合でも、`while文`の方が使いやすいだろうか。 +なるほど、無限ループを書くのに、`goto文`を使うより`while(true)`を使った方がいいことがわかった。ではほかのループの場合でも、`while文`の方が使いやすいだろうか。 -本書を先頭から読んでいる優秀な読者は`while(true)`の`true`はbool型の値であることに気がついているだろう。実は`while(E)`の括弧の中Eは、`if(E)`と書くのと全く同じ`条件`なのだ。`条件`が`true`であれば繰り返し実行される。`false`なら繰り返し実行されない。 +本書を先頭から読んでいる優秀な読者は`while(true)`の`true`は`bool`型の値であることに気が付いているだろう。実は`while(E)`の括弧の中`E`は、`if(E)`と書くのとまったく同じ`条件`なのだ。`条件`が`true`であれば繰り返し実行される。`false`なら繰り返し実行されない。 -~~~ +~~~c++ while ( 条件 ) 文 ~~~ @@ -717,7 +717,7 @@ int main() 重要なのはこの5行。 -~~~c++ +~~~cpp while( ( x = input() ) != 0 ) { sum += x ; @@ -725,7 +725,7 @@ while( ( x = input() ) != 0 ) } ~~~ -ここではちょっとむずかしいコードが出てくる。`while`の中の`条件`が、`"( x = input() ) != 0"`になっている。これはどういうことか。 +ここではちょっと難しいコードが出てくる。`while`の中の`条件`が、`"( x = input() ) != 0"`になっている。これはどういうことか。 実は`条件`は`bool型`に変換さえできればどんな式でも書ける。 @@ -739,17 +739,17 @@ int main() } ~~~ -このコードでは、"(x=1)"と"1"が等しいか"=="どうかを判断している。"(x=1)"という式は変数xに1を代入する式だ。`代入式`の値は、代入された変数の値になる。この場合変数xの値だ。変数xには1が代入されているので、その値は1、つまり"(x=1) == 1"は"1 == 1"と書くのと同じ意味になる。この結果は`true`だ。 +このコードでは、"`(x=1)`"と"`1`"が等しいか"`==`"どうかを判断している。"`(x=1)`"という式は変数`x`に`1`を代入する式だ。`代入式`の値は、代入された変数の値になる。この場合変数`x`の値だ。変数`x`には`1`が代入されているので、その値は1、つまり"`(x=1) == 1`"は"`1 == 1`"と書くのと同じ意味になる。この結果は`true`だ。 -さて、このことを踏まえて、"( x = input() ) != 0"を考えてみよう。 +さて、このことを踏まえて、"`( x = input() ) != 0`"を考えてみよう。 -"( x = input() )"は変数xに関数inputを呼び出した結果を代入している。関数inputはユーザーから入力を得て、その入力をそのまま返す。つまり変数xにはユーザーの入力した値が代入される。その結果が0と等しくないか"!="どうかを判断している。つまり、ユーザーが0を入力した場合はfalse、非ゼロを入力した場合はtrueとなる。 +"`( x = input() )`"は変数`x`に関数`input`を呼び出した結果を代入している。関数`input`はユーザーから入力を得て、その入力をそのまま返す。つまり変数`x`にはユーザーの入力した値が代入される。その結果が`0`と等しくないか"`!=`"どうかを判断している。つまり、ユーザーが`0`を入力した場合は`false`、非ゼロを入力した場合は`true`となる。 -`while(条件)`は`条件`が`true`となる場合に繰り返し実行をする。結果として、ユーザーが0を入力するまで繰り返し実行をするコードになる。 +`while(条件)`は`条件`が`true`となる場合に繰り返し実行をする。結果として、ユーザーが`0`を入力するまで繰り返し実行をするコードになる。 `goto文`を使った終了条件付きループと比較してみよう。 -~~~c++ +~~~cpp loop: if ( (x = input() ) != 0 ) { @@ -778,7 +778,7 @@ int main() } ~~~ -後は関数`hello_n(n)`がインデックスループを実装するだけだ。ただし`n`が負数ならば何も実行しないようにしよう。 +あとは関数`hello_n(n)`がインデックスループを実装するだけだ。ただし`n`が負数ならば何も実行しないようにしよう。 `goto文`でインデックスループを書くときに学んだように、 @@ -811,9 +811,9 @@ void hello_n( int n ) } ~~~ -重要な部分だけ抜き出すと以下の通り。 +重要な部分だけ抜き出すと以下のとおり。 -~~~c++ +~~~cpp while( i != n ) { std::cout << "hello\n"s ; @@ -823,7 +823,7 @@ while( i != n ) `goto文`を使ったインデックスループと比較してみよう。 -~~~c++ +~~~cpp loop : if ( i != n ) { @@ -835,7 +835,7 @@ loop : 読者の中にはあまり変わらないのではないかと思う人もいるかもしれない。しかし、次の問題を解くプログラムを書くと、`while文`がいかに楽に書けるかを実感するだろう。 -問題:以下のような九九の表を出力するプログラムを書きなさい +問題:以下のような九九の表を出力するプログラムを書きなさい。 ~~~ 1 2 3 4 5 6 7 8 9 @@ -861,7 +861,7 @@ int main() 逐次実行、条件分岐、ループまでを習得した誇りある本物のプログラマーである我々は、もちろん九九の表はループを書いて出力する。 -まず出力すべき表をみると、数値が左揃えになっていることに気がつくだろう。 +まず出力すべき表を見ると、数値が左揃えになっていることに気が付くだろう。 ~~~ 4 8 12 @@ -904,7 +904,7 @@ while ( 条件 ) 文 ~~~ -早速そのようなコードを書いてみよう。 +さっそくそのようなコードを書いてみよう。 ~~~cpp int main() @@ -958,11 +958,11 @@ loop_inner : ## for文 -ところで今まで`while文`で書いてきたインデックスループには特徴がある。 +ところでいままで`while文`で書いてきたインデックスループには特徴がある。 試しに1から100までの整数を出力するコードを見てみよう。 -~~~c++ +~~~cpp int main() { int i = 1 ; @@ -992,11 +992,11 @@ int main() } ~~~ -ここで真に必要なのは、「実際に繰り返したい文」だ。その他の処理は、ループを実現するために必要なコードだ。ループの実現に必要な処理が飛び飛びの場所にあるのは甚だわかりにくい。 +ここで真に必要なのは、「実際に繰り返したい文」だ。その他の処理は、ループを実現するために必要なコードだ。ループの実現に必要な処理が飛び飛びの場所にあるのは、はなはだわかりにくい。 `for文`はそのような問題を解決するための機能だ。 -~~~ +~~~c++ for ( 変数の宣言 ; 終了条件の確認 ; 各ループの最後に必ず行う処理 ) 文 ~~~ @@ -1043,7 +1043,6 @@ int main() `while文`を使ったコードと比べてみよう。 ~~~cpp - int main() { int a = 1 ; @@ -1063,10 +1062,9 @@ int main() 格段に読みやすくなっていることがわかる。 -C++ではカンマ`','`を使うことで、複数の`式`をひとつの`文`に書くことができる。 +C++ではカンマ`','`を使うことで、複数の`式`を1つの`文`に書くことができる。 ~~~cpp - int main() { int a = 0, b = 0 ; @@ -1087,7 +1085,7 @@ int main() 変数もカンマで複数宣言できると知った読者は、以下のように書きたくなるだろう。 -~~~c++ +~~~cpp int main() { for ( int a = 1, b = 1 ; @@ -1099,7 +1097,7 @@ int main() } ~~~ -これは動かない。なぜならば、`for文`を2つネストさせたループは、$a \times b$回のループで、変数`a`が`1`から`9`まで変化するそれぞれに対して、変数`b`が`1`から`9`まで変化する。しかし、上の`for文`ひとつのコードは、変数`a`, `b`ともに同時に`1`から`9`まで変化する。したがって、これは単に`a`回のループでしかない。`a`回のループの中で`b`回のループをすることで$a \times b$回のループを実現できる。 +これは動かない。なぜならば、`for文`を2つネストさせたループは、$a \times b$回のループで、変数`a`が`1`から`9`まで変化するそれぞれに対して、変数`b`が`1`から`9`まで変化する。しかし、上の`for文`1つのコードは、変数`a`, `b`ともに同時に`1`から`9`まで変化する。したがって、これは単に`a`回のループでしかない。`a`回のループの中で`b`回のループをすることで$a \times b$回のループを実現できる。 `for文`では使わない部分を省略することができる。 @@ -1130,13 +1128,13 @@ int main() `do文`は`while文`に似ている。 -~~~ +~~~c++ do 文 while ( 条件 ) ; ~~~ 比較のために`while文`の文法も書いてみると以下のようになる。 -~~~ +~~~c++ while ( 条件 ) 文 ~~~ @@ -1169,9 +1167,9 @@ int main() ## break文 -ループの実行の途中で、ループの中から外に脱出したくなった場合、どうすればいいのだろうか。例えばループを実行中に何らかのエラーを検出したので処理を中止したい場合などだ +ループの実行の途中で、ループの中から外に脱出したくなった場合、どうすればいいのだろうか。例えばループを実行中に何らかのエラーを検出したので処理を中止したい場合などだ。 -~~~c++ +~~~cpp while ( true ) { // 処理 @@ -1185,7 +1183,7 @@ while ( true ) `break文`はループの途中から脱出するための文だ。 -~~~ +~~~c++ break ; ~~~ @@ -1242,10 +1240,10 @@ int main() ## continue文 -ループの途中で、今のループを打ち切って次のループに進みたい場合はどうすればいいのだろう。例えば、ループの途中でエラーを検出したので、そのループについては処理を打ち切りたい場合だ。 +ループの途中で、いまのループを打ち切って次のループに進みたい場合はどうすればいいのだろう。例えば、ループの途中でエラーを検出したので、そのループについては処理を打ち切りたい場合だ。 -~~~c++ +~~~cpp while ( true ) { // 処理 @@ -1259,7 +1257,7 @@ while ( true ) `continue文`はループを打ち切って次のループに行くための文だ。 -~~~ +~~~c++ continue ; ~~~ @@ -1427,7 +1425,7 @@ int main() 関数`main`は関数`until_ten`に引数1を渡す。 -関数`until_ten`は引数が`10`より大きければ何もせず処理を戻し、そうでなければ引数を出力して再帰する。その時引数は$+1$される。 +関数`until_ten`は引数が`10`より大きければ何もせず処理を戻し、そうでなければ引数を出力して再帰する。そのとき引数は$+1$される。 これによりインデックスループが実現できる。 @@ -1516,15 +1514,15 @@ int main() } ~~~ -後は関数`convert`を実装すればよいだけだ。 +あとは関数`convert`を実装すればよいだけだ。 -関数`convert`に引数を渡した時の結果を考えてみよう。`convert(1010)`は`10`を返し、`convert(1111)`は`15`を返す。 +関数`convert`に引数を渡したときの結果を考えてみよう。`convert(1010)`は`10`を返し、`convert(1111)`は`15`を返す。 では`convert(-1010)`の結果はどうなるだろうか。これは`-10`になる。 負数と正数の違いを考えるのは面倒だ。ここでは正数を引数として与えると10進数から2進数へ変換した答えを返してくる魔法のような関数`solve`をすでに書き終えたと仮定しよう。我々はまだ関数`solve`を書いていないが、その問題は未来の自分に押し付けよう。 -~~~c++ +~~~cpp // 1,0のみを使った10進数から // 2進数へ変換する関数 int solve( int n ) ; @@ -1535,7 +1533,7 @@ int solve( int n ) ; 1. 引数が正数の場合はそのまま関数`solve`に渡して`return` 2. 引数が負数の場合は絶対値を関数`solve`に渡して負数にして`return` -~~~c++ +~~~cpp int convert( int n ) { // 引数が正数の場合 @@ -1554,7 +1552,7 @@ int convert( int n ) 今回、引数の整数を10進数で表現した場合に2,3,4,5,6,7,8,9が使われている場合は考えないものとする。 -~~~c++ +~~~cpp // OK solve(10111101) ; // ありえない @@ -1565,14 +1563,14 @@ solve(2) ; まずとても簡単な1桁の変換を考えよう。 -~~~c++ +~~~cpp solve(0) ; // 0 solve(1) ; // 1 ~~~ 引数が`0`か`1`の場合、単にその値を返すだけだ。関数`solve`には正数しか渡されないので、負数は考えなくてよい。すると、以下のように書ける。 -~~~c++ +~~~cpp int solve( int n ) { if ( n <= 1 ) @@ -1584,7 +1582,7 @@ int solve( int n ) その他の場合とは、桁数が多い場合だ。 -~~~c++ +~~~cpp solve(10) ; // 2 solve(11) ; // 3 solve(110) ; // 4 @@ -1593,7 +1591,7 @@ solve(111) ; // 5 関数`solve`が解決するのは最下位桁だ。`110`の場合は`0`で、`111`の場合は`1`となる。最も右側の桁のみを扱う。数値から10進数で表記したときの最下位桁を取り出すには、10で割った余りが使える。覚えているだろうか。剰余演算子の`operator %`を。 -~~~c++ +~~~cpp int solve( int n ) { if ( n <= 1 ) @@ -1605,7 +1603,7 @@ int solve( int n ) 結果は以下のようになる。 -~~~c++ +~~~cpp solve(10) ; // 0 solve(11) ; // 1 solve(110) ; // 0 @@ -1618,7 +1616,7 @@ solve(111) ; // 1 以下は`solve(n)`が再帰的に呼び出す関数だ。 -~~~c++ +~~~cpp solve(10) ; // solve(1) solve(11) ; // solve(1) solve(100) ; // solve(10)→solve(1) @@ -1629,7 +1627,7 @@ solve(111) ; // solve(11)→solve(1) 10進数表記された数値から最下位桁を取り除いた数値にするというのは、11を1に, 111を11にする処理だ。これは数値を10で割ればよい。 -~~~c++ +~~~cpp 10 / 10 ; // 1 11 / 10 ; // 1 100 / 10 ; // 10 @@ -1637,7 +1635,7 @@ solve(111) ; // solve(11)→solve(1) 111 / 10 ; // 11 ~~~ -10進数表記は桁が一つあがると10倍される。だから10で割れば最下位桁が消える。ところで、我々は計算しようとしているのは2進数だ。2進数では桁が一つあがると2倍される。なので、再帰的に関数`solve`を呼び出して得られた結果は2倍しなければならない。そして足し合わせる。 +10進数表記は桁が1つ上がると10倍される。だから10で割れば最下位桁が消える。ところで、我々は計算しようとしているのは2進数だ。2進数では桁が1つ上がると2倍される。なので、再帰的に関数`solve`を呼び出して得られた結果は2倍しなければならない。そして足し合わせる。 ~~~cpp int solve( int n ) @@ -1655,7 +1653,7 @@ int solve( int n ) } ~~~ -冗長なコメントを除いて短くすると以下の通り +冗長なコメントを除いて短くすると以下のとおり。 ~~~cpp int solve( int n ) @@ -1669,9 +1667,9 @@ int solve( int n ) 再帰ではないループで関数`solve`を実装するとどうなるのだろうか。 -引数の数値が何桁あっても対応できるよう、ループで1桁ずつ処理していくのはかわらない。 +引数の数値が何桁あっても対応できるよう、ループで1桁ずつ処理していくのは変わらない。 -もういちど2進数の計算を見てみよう。 +もう一度2進数の計算を見てみよう。 $$ @@ -1699,7 +1697,7 @@ int solve( int n ) result += n%10 * i ; // 次の桁に乗ずる値 i *= 2 ; - // 桁を一つ減らす + // 桁を1つ減らす n /= 10 ; } @@ -1712,14 +1710,14 @@ int solve( int n ) 再帰は万能ではない。そもそも関数とは、別の関数から呼ばれるものだ。関数`main`だけは特別で、関数`main`を呼び出すことはできない。 -~~~c++ +~~~cpp int main() { main() ; // エラー } ~~~ -関数の実行が終了した場合、呼び出し元に処理が戻る。そのために関数は呼び出し元を覚えていなければならない。これには通常`スタック`とよばれるメモリーを消費する。 +関数の実行が終了した場合、呼び出し元に処理が戻る。そのために関数は呼び出し元を覚えていなければならない。これには通常`スタック`と呼ばれるメモリーを消費する。 ~~~cpp void f() { } // gに戻る @@ -1737,7 +1735,7 @@ void g() int x {} ; std::cin >> x ; f() ; // 関数を呼び出す - // 関数を呼び出した後に変数を使う + // 関数を呼び出したあとに変数を使う std::cout << x ; } ~~~ diff --git a/009-vector.md b/009-vector.md index e108fad..75ff34e 100644 --- a/009-vector.md +++ b/009-vector.md @@ -1,8 +1,8 @@ -# メモリを無限に確保する +# メモリーを無限に確保する ## これまでのまとめ -ここまで読み進めてきた読者は、逐次実行、条件分岐、ループに加えて、変数と関数を理解した。これだけの要素を習得したならば、本質的にはプログラミングはほぼできるようになったと言ってよい。ただし、まだできないことがある。動的なメモリ確保だ。 +ここまで読み進めてきた読者は、逐次実行、条件分岐、ループに加えて、変数と関数を理解した。これだけの要素を習得したならば、本質的にはプログラミングはほぼできるようになったと言ってよい。ただし、まだできないことがある。動的なメモリー確保だ。 標準入力から`0`が入力されるまで任意個の整数値を受け取り、小さい値から順に出力するプログラムを実装しよう。以下はそのようなプログラムの実行例だ。 @@ -27,7 +27,7 @@ $ make run `0`が入力されるまで、1番目に、2番目に小さい値はわからない。そのため、この問題の解決には、入力をすべて保持しておく必要がある。 -ここで必要なのは、値をいくらでも保持しておく方法と、値に順番があり、i番目の値を間接的に指定して読み書きできる方法だ。その方法としてC++には標準ライブラリ`std::vector`がある。 +ここで必要なのは、値をいくらでも保持しておく方法と、値に順番があり、$i$番目の値を間接的に指定して読み書きできる方法だ。その方法としてC++には標準ライブラリ`std::vector`がある。 ## vector @@ -57,7 +57,7 @@ int main() } ~~~ -もちろん、上のvectorを保持するvectorも書ける。その場合、`std::vector>>`になる。このvectorを保持する`vector`も当然書けるが省略する。 +もちろん、上の`vector`を保持する`vector`も書ける。その場合、`std::vector>>`になる。このvectorを保持する`vector`も当然書けるが省略する。 `std::vector`型の変数にはメンバー関数`push_back`を使うことで値を保持できる。 @@ -74,7 +74,7 @@ int main() `メンバー関数`(member function)というのは特別な関数で、詳細はまだ説明しない。ここで覚えておくべきこととしては、メンバー関数は一部の変数に使うことができること、メンバー関数`f`を変数`x`に使うには`'x.f(...)'`のように書くこと、を覚えておこう。 -`std::vector`はメモリの続く限りいくらでも値を保持できる。試しに1000個の整数を保持させてみよう。 +`std::vector`はメモリーの続く限りいくらでも値を保持できる。試しに1000個の整数を保持させてみよう。 ~~~cpp int main() @@ -154,7 +154,7 @@ int main() この例ではループを使っている。読者はすでにループについては理解しているはずだ。上のコードが理解できないのであれば、もう一度ループの章に戻って学び直すべきだ。 -もし`at(i)`に要素数を超えるiを渡してしまった場合どうなるのだろうか。 +もし`at(i)`に要素数を超える`i`を渡してしまった場合どうなるのだろうか。 ~~~cpp int main() @@ -162,7 +162,7 @@ int main() std::vector v { } ; v.push_back(0) ; // vには0番目の要素しかない - // 1番目はあやまり + // 1番目は誤り std::cout << v.at(1) ; } ~~~ @@ -176,7 +176,7 @@ terminate called after throwing an instance of 'std::out_of_range' Aborted (core dumped) ~~~ -なにやら恐ろしげなメッセージが表示されるではないか。しかし心配することはない。このメッセージはむしろ嬉しいメッセージだ。変数vに1番目の要素がないことを発見してくれたという実行時のエラーメッセージだ。すでに学んだように、エラーメッセージは恐れるものではない。エラーメッセージは嬉しいものだ。エラーメッセージが出たらありがとう。エラーメッセージがあるおかげでバグの存在がわかる。 +なにやら恐ろしげなメッセージが表示されるではないか。しかし心配することはない。このメッセージはむしろうれしいメッセージだ。変数`v`に1番目の要素がないことを発見してくれたという実行時のエラーメッセージだ。すでに学んだように、エラーメッセージは恐れるものではない。エラーメッセージはうれしいものだ。エラーメッセージが出たらありがとう。エラーメッセージがあるおかげでバグの存在がわかる。 このメッセージの本当の意味はいずれ例外やデバッガーを解説する章で説明するとして、`vector`の要素数を超える指定をしてはいけないことを肝に銘じておこう。もちろん、`-1`もダメだ。 @@ -200,7 +200,7 @@ int main() なぜ`int`型ではダメなのか。その謎は整数の章で明らかになる。ここでは`std::size_t`型は負数が使えない整数型だということだけ覚えておこう。`std::size_t`型に`-1`はない。`vector`の要素指定では負数は使えないので、負数が使えない変数を使うのは理にかなっている。 -さて、これまでに学んだ知識だけを使って、`std::vector`のすべての要素を順番通りに出力するコードが書けるはずだ。 +さて、これまでに学んだ知識だけを使って、`std::vector`のすべての要素を順番どおりに出力するコードが書けるはずだ。 ~~~cpp int main() @@ -250,7 +250,7 @@ int main() 入力された順番に出力できるということは、その逆順にも出力できるということだ。 -~~~c++ +~~~cpp for ( std::size_t index = v.size()-1 ; index != 0 ; --index ) { std::cout << v.at(index) << " "s ; @@ -261,12 +261,12 @@ std::cout << v.at(0) ; 最後に`'v.at(0)'`を出力しているのは、ループが`'i == 0'`のときに終了してしまうからだ。つまり最後に出力すべき`vector`最初の要素である`'v.at(0)'`が出力されない。 -`std::size_t`型は`-1`が使えないため、このようなコードになってしまう。`int`型を使えば負数は使えるのだが、`int`型と`std::size_t`型の比較は様々な理由で問題がある。その理由は整数の章で深く学ぶことになるだろう。 +`std::size_t`型は`-1`が使えないため、このようなコードになってしまう。`int`型を使えば負数は使えるのだが、`int`型と`std::size_t`型の比較はさまざまな理由で問題がある。その理由は整数の章で深く学ぶことになるだろう。 ところで、問題は入力された整数を小さい順に出力することだった。この問題を考えるために、まず`vector`の中に入っている要素から最も小さい整数の場所を探すプログラムを考えよう。 -問題を考えるに当たって、いちいち標準入力から入力を取るのも面倒なので、あらかじめ`vector`に要素をいれておく方法を学ぶ。実は、`vector`の要素は以下のように書けば指定することができる。 +問題を考えるにあたって、いちいち標準入力から入力を取るのも面倒なので、あらかじめ`vector`に要素を入れておく方法を学ぶ。実は、`vector`の要素は以下のように書けば指定することができる。 ~~~cpp int main() @@ -287,13 +287,13 @@ int main() さて、以下のような要素の`vector`から最も小さい整数を探すプログラムを考えよう。 -~~~c++ +~~~cpp std::vector v = { 8, 3, 7, 4, 2, 9, 3 } ; ~~~ これを見ると、最も小さい整数は4番目(最初の要素は0番目なので4番目)にある`2`だ。ではどうやって探すのだろうか。 -解決方法としては先頭から末尾まで要素を一つずつ比較して、最も小さい要素を見つけ出す。まず0番目の`8`が最も小さいと仮定する。現在わかっている中で最も小さい要素のインデックスを記録するために変数`min`を作っておこう +解決方法としては先頭から末尾まで要素を1つずつ比較して、最も小さい要素を見つけ出す。まず0番目の`8`が最も小さいと仮定する。現在わかっている中で最も小さい要素のインデックスを記録するために変数`min`を作っておこう。 ~~~ min = 0 @@ -310,7 +310,7 @@ min = 1 ~~~ -2番目の`7`と`min`番目を比較するとまだ1番目のほうが小さい。3番目の4と比較してもまだ`min`番目の方が小さい。 +2番目の`7`と`min`番目を比較するとまだ1番目の方が小さい。3番目の4と比較してもまだ`min`番目の方が小さい。 4番目の`2`と`min`番目を比較すると、4番目の方が小さい。変数`min`に`4`を代入しよう。 @@ -412,7 +412,7 @@ int main() std::size_t size = v.size() ; // この部分を繰り返す? - { // これ全体がひとつのブロック文 + { // これ全体が1つのブロック文 std::size_t min = 0 ; for ( std::size_t index = 1 ; index != size ; ++index ) @@ -429,9 +429,9 @@ int main() } ~~~ -このコードはそのまま使えない。今回考えた方法では、先頭が一つずつずれていく。そのために、最も小さい要素を探すループを、更にループさせる。 +このコードはそのまま使えない。今回考えた方法では、先頭が1つずつずれていく。そのために、最も小さい要素を探すループを、さらにループさせる。 -~~~c++ +~~~cpp // 現在の先頭 for ( std::size_t head = 0 ; head != size ; ++head ) { @@ -475,7 +475,7 @@ int main() } ~~~ -変数と全く同じだ。 +変数とまったく同じだ。 しかし、変数`a`に変数bの値を代入すると、変数`a`の元の値は消えてしまう。 @@ -512,7 +512,7 @@ int main() } ~~~ -さて、これで問題を解く準備は全て整った。 +さて、これで問題を解く準備はすべて整った。 ~~~cpp int main() @@ -539,11 +539,11 @@ int main() v.at(min) = temp ; } - // 実行した後 + // 実行したあと } ~~~ -ところで、このプログラムの「実行した後」地点での`vector`の中身はどうなっているだろうか。 +ところで、このプログラムの「実行したあと」地点での`vector`の中身はどうなっているだろうか。 ~~~cpp int main() @@ -552,7 +552,7 @@ int main() // 上と同じコードなので省略 - // 実行した後 + // 実行したあと std::cout << "\n"s ; for ( std::size_t index = 0, size = v.size() ; index != size ; ++index ) @@ -572,7 +572,7 @@ $ make run なんと`vector`の要素も小さい順に並んでいる。この状態のことを、ソートされているという。ループの中で最も小さい値を出力していく代わりに、まずソートして先頭から値を出力してもよいということだ。 -ソートには様々な方法があるが、今回使ったのは選択ソート(selection sort)というアルゴリズムだ。 +ソートにはさまざまな方法があるが、今回使ったのは選択ソート(selection sort)というアルゴリズムだ。 `vector`を使う方法には、イテレーターというもっと便利な方法があるが、それはイテレーターの章で説明する。 diff --git a/010-debug-printf.md b/010-debug-printf.md index b6935d2..2129f5c 100644 --- a/010-debug-printf.md +++ b/010-debug-printf.md @@ -41,7 +41,7 @@ int main() } ~~~ -早速実行してみよう。 +さっそく実行してみよう。 ~~~ $ make run @@ -52,9 +52,9 @@ $ make run `printfデバッグ`を行うには、まずコード中の間違っていそうな箇所にアタリをつける必要がある。 -問題がどこにあるかわからないが、ループのどこかで間違っていそうだ。一番外側のループにアタリをつけよう。ループが実行されるごとに変数vの中身を表示してみる。 +問題がどこにあるかわからないが、ループのどこかで間違っていそうだ。一番外側のループにアタリをつけよう。ループが実行されるごとに変数`v`の中身を表示してみる。 -~~~c++ +~~~cpp for ( std::size_t head = 0 ; head != size ; ++head ) { // printfデバッグ @@ -83,11 +83,11 @@ debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, } 1 1 1 1 1 1 1 1 7 ~~~ -何故か`1`が増えている。明らかにおかしい。しかしまだ問題の特定にまでは至らない。 +なぜか`1`が増えている。明らかにおかしい。しかしまだ問題の特定にまでは至らない。 内側のループにも`printfデバッグ`を追加してみよう。 -~~~c++ +~~~cpp auto min = head ; for ( std::size_t index = head+1 ; index != size ; ++index ) { @@ -131,7 +131,7 @@ debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, } ひょっとしたら大小比較が間違っているのかもしれない。確かめてみよう。 -~~~c++ +~~~cpp for ( std::size_t index = head+1 ; index != size ; ++index ) { @@ -172,7 +172,7 @@ debug: v = { 1, 8, 2, 5, 6, 9, 4, 1, 7, } 大小比較も問題ないようだ。では最終的に見つけた最も小さい値は、本当に最も小さい値だろうか。 -~~~c++ +~~~cpp // 最小値を探すループ for ( std::size_t index = head+1 ; index != size ; ++index ) { @@ -212,7 +212,7 @@ debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, } ひょっとしたら要素の交換が間違っているのかもしれない。`printfデバッグ`してみよう。 -~~~c++ +~~~cpp // printfデバッグ std::cout << "debug before: "s << v.at(head) << ", " << v.at(min) << "\n"s ; // printfデバッグ @@ -242,18 +242,18 @@ debug after : 1, 1 debug: v = { 1, 1, 1, 5, 6, 9, 4, 1, 7, } ~~~ -これをみると、要素の値の交換が正しく行われていないことがわかる。 +これを見ると、要素の値の交換が正しく行われていないことがわかる。 -問題の場所がわかったので、早速コードを見てみよう。 +問題の場所がわかったので、さっそくコードを見てみよう。 -~~~c++ +~~~cpp v.at(head) = v.at(min) ; v.at(min) = v.at(head) ; ~~~ これは要するに以下のコードと同じだ。 -~~~c++ +~~~cpp int a = 0 ; int b = 1 ; @@ -261,11 +261,11 @@ a = b ; // a = 1 b = a ; // b = 1 ~~~ -変数`a`, `b`の値を交換したい場合、変数`a`に変数`b`を代入した後に、変数`b`に変数`a`を代入する処理は誤りだ。なぜならば、変数`b`の代入のときには、変数`a`の値は変数bの値になってしまっているからだ。 +変数`a`, `b`の値を交換したい場合、変数`a`に変数`b`を代入したあとに、変数`b`に変数`a`を代入する処理は誤りだ。なぜならば、変数`b`の代入のときには、変数`a`の値は変数bの値になってしまっているからだ。 前章で学んだように、こういう場合、別の変数に値を代入して退避させておく。 -~~~c++ +~~~cpp int a = 0 ; int b = 1 ; @@ -345,10 +345,9 @@ $ ./program > cout.txt 2> cerr.txt これを実行すると、ファイル`cout.txt`には`"standard output\n"`が、ファイル`cerr.txt`には`"standard error output\n"`が出力されている。 -これを使って先程のプログラムを書き直すと以下のようになる。 +これを使って先ほどのプログラムを書き直すと以下のようになる。 ~~~cpp - // 1 * 2 * 3 * ... * nを計算するプログラム int main() { @@ -374,12 +373,12 @@ int main() ## まとめ -`printfデバッグ`はコード中のどこに問題があるかを絞り込むための方法だ。プログラムに問題が存在し、問題の発生の有無はプログラムの状態を調べることで判断できるが、コード中のどこに問題が存在するかわからない時、`printfデバッグ`で問題の箇所を絞り込むことができる。 +`printfデバッグ`はコード中のどこに問題があるかを絞り込むための方法だ。プログラムに問題が存在し、問題の発生の有無はプログラムの状態を調べることで判断できるが、コード中のどこに問題が存在するかわからないとき、`printfデバッグ`で問題の箇所を絞り込むことができる。 -`printfデバッグ`のやり方は以下の通り。 +`printfデバッグ`のやり方は以下のとおり。 1. コード中の間違っていそうな箇所にアタリをつける 2. プログラムの状態を出力する -3. 出力結果が期待通りかどうかを調べる +3. 出力結果が期待どおりかどうかを調べる -`printfデバッグ`は原始的だが効果的なデバッグ方法だ。後の章ではデバッガーというより高級でプログラマーらしいデバッグ方法も紹介するが、そのような高級なデバッグ方法が使えない環境でも、`printfデバッグ`ならば使えることは多い。 +`printfデバッグ`は原始的だが効果的なデバッグ方法だ。あとの章ではデバッガーというより高級でプログラマーらしいデバッグ方法も紹介するが、そのような高級なデバッグ方法が使えない環境でも、`printfデバッグ`ならば使えることは多い。 diff --git a/011-integer.md b/011-integer.md index 02ee82b..8151d63 100644 --- a/011-integer.md +++ b/011-integer.md @@ -15,7 +15,7 @@ int main() } ~~~ -ここでは、`'123'`, `'0'`がリテラルだ。`'-123'`というのは演算子`operator -`に整数リテラル`123`を適用したものだ。リテラルは`123`だけだ。ただしこれは細かい詳細なので今はそれほど気にしなくてもよい。 +ここでは、`'123'`, `'0'`がリテラルだ。`'-123'`というのは演算子`operator -`に整数リテラル`123`を適用したものだ。リテラルは`123`だけだ。ただしこれは細かい詳細なのでいまはそれほど気にしなくてもよい。 ### 10進数リテラル @@ -140,9 +140,9 @@ auto max = 0b11111111 ; 正数だけを表現するならば話は簡単だ。1バイトの整数は0から255までの値を表現できる。これを符号なし整数(unsigned integer)という。 -では負数を表現するにはどうしたらいいだろう。正数と負数を両方扱える整数表現のことを、符号付き正数(signed integer)という。1バイトは256種類の状態しか表現できないので、もし$-1$を表現したい場合、-1から254までの値を扱えることになる。 +では負数を表現するにはどうしたらいいだろう。正数と負数を両方扱える整数表現のことを、符号付き正数(signed integer)という。1バイトは256種類の状態しか表現できないので、もし$-1$を表現したい場合、$-1$から254までの値を扱えることになる。 --1しか扱えないのでは実用的ではないので、負数と正数を同じ種類ぐらい表現したい。256の半分は128だが、1バイトで表現された整数は-128から128までを表現することはできない。0があるからだ。0を含めると、1バイトの整数は最大で-128から127までか、-127から128までを表現できる。どちらかに偏ってしまう。 +$-1$しか扱えないのでは実用的ではないので、負数と正数を同じ種類ぐらい表現したい。256の半分は128だが、1バイトで表現された整数は$-128$から128までを表現することはできない。0があるからだ。0を含めると、1バイトの整数は最大で$-128$から127までか、$-127$から128までを表現できる。どちらかに偏ってしまう。 では実際に1バイトで負数も表現できる正数表現を考えてみよう。 @@ -151,9 +151,9 @@ auto max = 0b11111111 ; 誰でも思いつきそうな表現方法に、符号ビットがある。これは最上位ビットを符号の有無を管理するフラグとして用いることにより、下位7ビットの値の符号を指定する方法だ。 -符号ビット表現では-1と1は以下のように表現できる。 +符号ビット表現では$-1$と1は以下のように表現できる。 -~~~ +~~~cpp // 1 0b0'0000001 // -1 @@ -162,26 +162,26 @@ auto max = 0b11111111 ; 最上位ビットが0であれば正数、1であれば負数だ。 -この一見わかりやすい表現方法には問題がある。まず表現できる値の範囲は-127から+127だ。さきほど、1バイトで正負になるべく均等に値を割り振る場合、-128から+127、もしくは-127から+128までを扱えると書いた。しかし符号ビット表現では-127から+127しか扱えない。残りの1はどこにいったのか。 +この一見わかりやすい表現方法には問題がある。まず表現できる値の範囲は$-127$から$+127$だ。先ほど、1バイトで正負になるべく均等に値を割り振る場合、$-128$から$+127$、もしくは$-127$から$+128$までを扱えると書いた。しかし符号ビット表現では$-127$から$+127$しか扱えない。残りの1はどこにいったのか。 -答えはゼロにある。符号ビット表現ではゼロに二通りの表現がある。+0と-0だ。 +答えはゼロにある。符号ビット表現ではゼロに2通りの表現がある。$+0$と$-0$だ。 -~~~ +~~~cpp // +0 0b0'0000000 // -0 0b1'0000000 ~~~ -+0も-0もゼロには違いない。しかし符号ビットが独立して存在しているために、ゼロが2種類ある。 +$+0$も$-0$もゼロには違いない。しかし符号ビットが独立して存在しているために、ゼロが2種類ある。 符号ビットは電子回路で実装するには複雑という問題もある。 #### 1の補数 -1の補数は負数を絶対値を2進数で表したときの各ビットを反転させた値で表現する。たとえば-1は1(`0b00000001`)の1の補数の`0b11111110`で表現される。 +1の補数は負数を絶対値を2進数で表したときの各ビットを反転させた値で表現する。たとえば$-1$は1(`0b00000001`)の1の補数の`0b11111110`で表現される。 -~~~ +~~~cpp // -1 0b11111110 @@ -189,9 +189,9 @@ auto max = 0b11111111 ; 0b11111101 ~~~ --1と-2を足すと結果は-3だ。この計算を1の補数で行うとどうなるか。 +$-1$と$-2$を足すと結果は$-3$だ。この計算を1の補数で行うとどうなるか。 -まず1の補数表現による-1と-2を足す。 +まず1の補数表現による$-1$と$-2$を足す。 ~~~ 11111110 @@ -209,9 +209,9 @@ auto max = 0b11111111 ; 11111100 ~~~ -1の補数による-3は3の各ビットを反転したものだ。3は`0b00000011`で、そのビットを反転させたものは`0b11111100`だ。上の計算結果は-3の1の補数表現になった。 +1の補数による$-3$は3の各ビットを反転したものだ。3は`0b00000011`で、そのビットを反転させたものは`0b11111100`だ。上の計算結果は$-3$の1の補数表現になった。 -もう一つ例を見てみよう。5と-2を足すと3になる。 +もう1つ例を見てみよう。5と$-2$を足すと3になる。 ~~~ 00000101 @@ -233,7 +233,7 @@ auto max = 0b11111111 ; 1の補数は引き算も足し算で表現できるので電子回路での実装が符号ビットよりもやや簡単になる。 -ただし、1の補数にも問題がある。0の表現だ。0というのは`0b00000000`だが1の補数では-xはxの各ビット反転ということを適用すると、-0は`0b11111111`になる。すると、符号ビット表現と同じく、+0と-0が存在することになる。したがって、1の補数8ビットで表現できる範囲は-127から+127になる。 +ただし、1の補数にも問題がある。0の表現だ。0というのは`0b00000000`だが1の補数では$-x$は$x$の各ビット反転ということを適用すると、$-0$は`0b11111111`になる。すると、符号ビット表現と同じく、$+0$と$-0$が存在することになる。したがって、1の補数8ビットで表現できる範囲は$-127$から$+127$になる。 #### 2の補数 @@ -241,14 +241,14 @@ auto max = 0b11111111 ; 2の補数表現による負数は1の補数表現の負数に、繰り上がり時に足すべき1を加えた値になる。 --1は1の補数表現では、1(`0b00000001`)の各ビットを反転させた値になる(`0b11111110`)。2の補数表現では、1の補数表現に1を加えた値になるので、`0b11111111`になる。 +$-1$は1の補数表現では、1(`0b00000001`)の各ビットを反転させた値になる(`0b11111110`)。2の補数表現では、1の補数表現に1を加えた値になるので、`0b11111111`になる。 -同様に、-2は`0b11111110`に、-3は`0b11111101`になる。 +同様に、$-2$は`0b11111110`に、$-3$は`0b11111101`になる。 00000011 11111100 -2の補数表現の-1と-2を足すと以下のようになる。 +2の補数表現の$-1$と$-2$を足すと以下のようになる。 ~~~ 11111111 @@ -257,9 +257,9 @@ auto max = 0b11111111 ; 1'11111101 ~~~ -9ビット目の繰り上がりを無視すると、計算結果は`0b11111101`になる。これは2の補数表現による-3と同じだ。 +9ビット目の繰り上がりを無視すると、計算結果は`0b11111101`になる。これは2の補数表現による$-3$と同じだ。 -5と-2の計算も見てみよう。 +5と$-2$の計算も見てみよう。 ~~~ 00000101 @@ -270,13 +270,13 @@ auto max = 0b11111111 ; 結果は3(`0b00000011`)だ。 -2の補数表現は引き算も足し算で実装できる上に、ゼロの表現方法は一つで、+0と-0が存在しない。8ビットの2の補数表現された整数の範囲は-128から+127になる。とても便利な負数の表現方法なので殆どのコンピューターで採用されている。 +2の補数表現は引き算も足し算で実装できる上に、ゼロの表現方法は1つで、$+0$と$-0$が存在しない。8ビットの2の補数表現された整数の範囲は$-128$から$+127$になる。とても便利な負数の表現方法なのでほとんどのコンピューターで採用されている。 ## 整数型 -C++には様々な整数型が存在する。C++はCから引き継いだ歴史的な経緯により、整数型の文法がわかりにくくなっている。 +C++にはさまざまな整数型が存在する。C++はCから引き継いだ歴史的な経緯により、整数型の文法がわかりにくくなっている。 -基本的には、符号付き整数型と符号なし整数型にわかれている。 +基本的には、符号付き整数型と符号なし整数型に分かれている。 符号付き整数型としては、`signed char`, `short int`, `int`, `long int`, `long long int`が存在する。符号付き整数型は負数を表現できる。 @@ -346,7 +346,7 @@ unsigned long b = 1 ; 通常、`int`を省略して単に`long`と書くことが多い。 -整数リテラルの値が`int型`で表現できない場合、`long型`になる。例えば、`int型`で100億を表現できないが、`long型`では表現できる実装の場合、以下の変数aは`long型`になる。 +整数リテラルの値が`int型`で表現できない場合、`long型`になる。例えば、`int型`で100億を表現できないが、`long型`では表現できる実装の場合、以下の変数`a`は`long型`になる。 ~~~cpp // 100億 @@ -381,7 +381,7 @@ auto b = 123lu ; `long long int型`は`long int型`以上の範囲の整数を扱える型だ。`long`と同じく`long long`は`long long int`と同じで、`unsigned long long int`もある。 -~~~ +~~~cpp // long long int long long a = 1 ; // unsigned long long int @@ -409,7 +409,7 @@ auto c = 123ull ; ### char型 -`char型`はやや特殊で、`char`, `signed char`, `unsigned char`の三種類の型がある。`signed char`と`char`は別物だ。`char型`は整数型であり、後で説明するように文字型でもある。`char型`の符号の有無は実装ごとに異なる。 +`char型`はやや特殊で、`char`, `signed char`, `unsigned char`の3種類の型がある。`signed char`と`char`は別物だ。`char型`は整数型であり、あとで説明するように文字型でもある。`char型`の符号の有無は実装ごとに異なる。 ## 整数型のサイズ @@ -453,14 +453,13 @@ int main() 8 ~~~ -どうやら筆者の環境では、`char`が1バイト、`short`が2バイト、`int`が4バイト、`long`と`long long`が8バイトのようだ。この結果は環境ごとに異なるので読者も自分で`sizeof`演算子を様々な型に適用して試してほしい。 +どうやら筆者の環境では、`char`が1バイト、`short`が2バイト、`int`が4バイト、`long`と`long long`が8バイトのようだ。この結果は環境ごとに異なるので読者も自分で`sizeof`演算子をさまざまな型に適用して試してほしい。 ## 整数型の表現できる値の範囲 整数型の表現できる値の最小値と最大値は`std::numeric_limits`で取得できる。最小値は`::min()`を、最大値は`::max()`で得られる。 ~~~cpp - int main() { std::cout @@ -476,7 +475,7 @@ int main() 2147483647 ~~~ -どうやら筆者の環境では`int`型は−21億4748万3648から21億4748万3647までの範囲の値を表現できるようだ。 +どうやら筆者の環境では`int`型は$−21億4748万3648$から21億4748万3647までの範囲の値を表現できるようだ。 `unsigned int`はどうだろうか。 @@ -498,9 +497,9 @@ int main() どうやら筆者の環境では`unsigned int`型は0から42億9496万7295までの範囲の値を表現できるようだ。`sizeof(int)`が4バイトであり、1バイトが8ビットの筆者の環境では自然な値だ。符号なしの4バイト整数型は0から$2^{32}-1$までの範囲の値を表現できる。符号付き4バイト整数型は$-2^{31}$から$2^{31}-1$までの範囲の値を表現できる。 -整数の最小値を-1したり、最大値を+1した場合、何が起こるのだろうか。 +整数の最小値を$-1$したり、最大値を$+1$した場合、何が起こるのだろうか。 -符号なし整数型の場合は簡単だ。最小値-1は最大値になる。最大値+1は最小値になる。 +符号なし整数型の場合は簡単だ。最小値$-1$は最大値になる。最大値$+1$は最小値になる。 ~~~cpp int main() @@ -516,14 +515,14 @@ int main() } ~~~ -8ビットの符号なし整数型があるとして、最小値は`0b00000000`(0)になるが、この値を-1すると`0b11111111`(255)となり、これは最大値になる。逆に、最大値である`0b11111111`(255)に+1すると`0b00000000`(0)となり、これは最小値になる。 +8ビットの符号なし整数型があるとして、最小値は`0b00000000`(0)になるが、この値を$-1$すると`0b11111111`(255)となり、これは最大値になる。逆に、最大値である`0b11111111`(255)に$+1$すると`0b00000000`(0)となり、これは最小値になる。 -これを数学的に厳密に書くと、「符号なし整数は算術モジュロ$2^n$の法に従う。ただしnは整数を表現する値のビット数である」となる。 +これを数学的に厳密に書くと、「符号なし整数は算術モジュロ$2^n$の法に従う。ただし$n$は整数を表現する値のビット数である」となる。 符号付き整数型の場合、挙動は定められていない。ただし、一般に普及している2の補数表現の場合は、以下のような挙動になることが多い。 -符号付き整数型の最小値を-1すると最大値になり、最大値を+1すると最小値になる。 +符号付き整数型の最小値を$-1$すると最大値になり、最大値を$+1$すると最小値になる。 ~~~cpp int main() @@ -539,11 +538,11 @@ int main() } ~~~ -これはなぜか。2の補数表現の8ビットの符号付き整数の最小値は`0b10000000`(-128)だが、これを-1すると`0b01111111`(127)となり、これは最大値となる。逆に最大値`0b01111111`(127)を+1すると`0b10000000`(-128)となり、これは最小値となる。 +これはなぜか。2の補数表現の8ビットの符号付き整数の最小値は`0b10000000`($-128$)だが、これを$-1$すると`0b01111111`(127)となり、これは最大値となる。逆に最大値`0b01111111`(127)を$+1$すると`0b10000000`($-128$)となり、これは最小値となる。 ## 整数型の変換 -整数型にはここで紹介しただけでも、様々な型がある。同じ型同士を使ったほうがよい。 +整数型にはここで紹介しただけでも、さまざまな型がある。同じ型同士を使った方がよい。 以下は型が一致している例だ。 @@ -598,7 +597,7 @@ int: 2147483647 どうやら筆者の環境では`short`型は約3万、`int`型は約21億ぐらいの値を表現できるようだ。 -では約3万までしか表現できない`short`型に4万を代入しようとするとどうなるのか。これは一つ前の整数型の表現できる値の範囲で説明したものと同じことがおこる。 +では約3万までしか表現できない`short`型に4万を代入しようとするとどうなるのか。これは1つ前の整数型の表現できる値の範囲で説明したものと同じことが起こる。 diff --git a/012-floating-point.md b/012-floating-point.md index e472638..5de67a6 100644 --- a/012-floating-point.md +++ b/012-floating-point.md @@ -45,19 +45,19 @@ double b = 0.00001 ; ~~~cpp int main() { - // 1万 + // 1万 float a = 10000.0 ; - // 1万分の1 + // 1万分の1 float b = 0.0001 ; - // 1万足す1万分の1 + // 1万足す1万分の1 float c = a + b ; std::cout << a << "\n"s << b << "\n"s << c ; } ~~~ -変数`a`の値は1万、変数bの値は1万分の1だ。変数`c`の値は`a+b`で`10000.0001`となるはずだが結果はどうだろう。 +変数`a`の値は1万、変数`b`の値は1万分の1だ。変数`c`の値は`a+b`で`10000.0001`となるはずだが結果はどうだろう。 ~~~ 10000 @@ -69,7 +69,7 @@ int main() ## 浮動小数点数リテラル -## 10進浮動小数点数リテラル +### 10進浮動小数点数リテラル 浮動小数点数リテラルの最も簡単な書き方は10進数で整数部を書き、小数点`'.'`を書き、続けて小数部を書く。末尾が`f`/`F`なら`float`型、末尾がなければ`double`型、末尾が`l`/`L`なら`long double`型だ。 @@ -104,7 +104,7 @@ $$123.456 \times 10^{0}$$ $$a \times 10^{b}$$ -浮動小数点数リテラルのもう一つの文法として、この`a`と`b`を指定するものがある。 +浮動小数点数リテラルのもう1つの文法として、この`a`と`b`を指定するものがある。 ~~~cpp // 値はすべて123.456 @@ -114,15 +114,15 @@ auto c = 123.456e0 ; auto d = 123.456E0 ; ~~~ -この文法は、`a`と`b`にe/Eを挟むことによって浮動小数点数の値を指定する。 +この文法は、`a`と`b`に`e`/`E`を挟むことによって浮動小数点数の値を指定する。 -この`a`を仮数部(fractional part)、`b`を指数部(exponent part)と言う。仮数のことは他にも、coefficient, significand, mantissaなどと呼ばれたりもする。 +この`a`を仮数部(fractional part)、`b`を指数部(exponent part)という。仮数のことはほかにも、coefficient, significand, mantissaなどと呼ばれたりもする。 そして、指数は底が10になる。 浮動小数点数は、値を正確に表現しているのではなく、仮数と指数の組み合わせで表現している。浮動小数点数が浮動と呼ばれる理由は、指数の存在によって小数点数が浮いているかのように動くからだ。 -例えば、仮数と指数がともに符号付き1バイトの整数で表現された2バイトの浮動小数点数があるとする。指数、仮数ともに、-128から127までの範囲の整数を表現できる。この浮動小数点数は10000(1万)も100000000(1億)も1000000000000(1兆)も表現できる。それぞれ、`1e4`, `1e8`, `1e12`だ。 +例えば、仮数と指数がともに符号付き1バイトの整数で表現された2バイトの浮動小数点数があるとする。指数、仮数ともに、$-128$から127までの範囲の整数を表現できる。この浮動小数点数は10000(1万)も100000000(1億)も1000000000000(1兆)も表現できる。それぞれ、`1e4`, `1e8`, `1e12`だ。 しかし、この浮動小数点数では1000100010000(1兆1億1万)を表現できない。なぜならば、この値を正確に表現するには、`100010001e4`を表現できる必要があるが、仮数は100010001を表現できないからだ。 @@ -146,7 +146,7 @@ auto c = 1.0e0l ; 浮動小数点数の仮数部と指数部によるリテラルは、16進数で記述することもできる。 -文法は、`0x`からはじめ、16進数の仮数部を書き、`e`/`E`の代わりに`p`/`P`を使い、指数部を10進数で指定する。このときの指数部の底は2になる。 +文法は、`0x`から始め、16進数の仮数部を書き、`e`/`E`の代わりに`p`/`P`を使い、指数部を10進数で指定する。このときの指数部の底は2になる。 値は @@ -163,12 +163,12 @@ double b = 0xde.fp5 ; ## 浮動小数点数の表現と特性 -浮動小数点数は指数と仮数で表現される。浮動小数点数の表現は様々だが、多くのアーキテクチャーでは国際標準規格のISO/IEC/IEEE 60559:2011が使われている。これは米国電気電子学会の規格IEEE 754-2008と同じ内容になっている。その大本はIntelが立案した規格、IEEE 754-1985だ。一般にはIEEE 754(アイトリプルイー 754)という名称で知られている。 +浮動小数点数は指数と仮数で表現される。浮動小数点数の表現はさまざまだが、多くのアーキテクチャーでは国際標準規格のISO/IEC/IEEE 60559:2011が使われている。これは米国電気電子学会の規格IEEE 754-2008と同じ内容になっている。その大本はIntelが立案した規格、IEEE 754-1985だ。一般にはIEEE 754(アイトリプルイー 754)という名称で知られている。 -IEEE 754では、浮動小数点数は符号ビット、仮数部、指数部からなる。本書ではIEEE 754のを前提として、浮動小数点数で気をつけるべき特性を説明する。 +IEEE 754では、浮動小数点数は符号ビット、仮数部、指数部からなる。本書ではIEEE 754を前提として、浮動小数点数で気を付けるべき特性を説明する。 -### +0.0と-0.0 +### $+0.0$と$-0.0$ IEEE 754では符号ビットがあるので、ゼロには2種類ある。正のゼロと負のゼロだ。 @@ -179,7 +179,7 @@ int main() } ~~~ -+0.0と-0.0の違いを浮動小数点数で表現することはできるが、値を比較すると同じものだとみなされる。 +$+0.0$と$-0.0$の違いを浮動小数点数で表現することはできるが、値を比較すると同じものだとみなされる。 ~~~cpp int main() @@ -193,7 +193,7 @@ int main() } ~~~ -### +∞と-∞(無限大) +### $+∞$と$-∞$(無限大) IEEE 754の浮動小数点数は正の無限と負の無限を表現できる。 @@ -257,9 +257,9 @@ int main() } ~~~ -浮動小数点数型Tの`numeric_limits`にはもう一つ、`max_digits10`がある。これは浮動小数点数を10進数表記にして、その10進数表記を浮動小数点数に戻したときに、浮動小数点数としての値を精度が落ちることなく再現できる桁数のことだ。 +浮動小数点数型`T`の`numeric_limits`にはもう1つ、`max_digits10`がある。これは浮動小数点数を10進数表記にして、その10進数表記を浮動小数点数に戻したときに、浮動小数点数としての値を精度が落ちることなく再現できる桁数のことだ。 -もうひとつ興味深い値としては、`numeric_limits::epsilon()`がある。これは浮動小数点数の1と比較可能な最小の値との差だ。 +もう1つ興味深い値としては、`numeric_limits::epsilon()`がある。これは浮動小数点数の1と比較可能な最小の値との差だ。 ~~~cpp int main() @@ -287,7 +287,7 @@ int main() } ~~~ -異なる浮動小数点数同士を演算すると、`float b ; コロンやアングルブラケットは名前に使える文字ではない。信じられない読者は試してみるとよい。 -~~~c++ +~~~cpp // エラー int :: = 0 ; int = 0 ; @@ -179,7 +179,7 @@ int = 0 ; 実は`std`というのは名前空間(namespace)の名前だ。ダブルコロン(`::`)は名前空間を指定する文法だ。 -名前空間の文法は以下の通り +名前空間の文法は以下のとおり。 ~~~c++ namespace ns { @@ -189,13 +189,13 @@ namespace ns { 名前空間の中の名前を参照するには`::`を使う。 -~~~c++ +~~~cpp ns::name ; ~~~ 名前空間の中には変数も書ける。この変数は関数の内部に限定されたローカル変数とは違い、どの関数からでも参照できる。 -~~~c++ +~~~cpp namespace ns { int name{} ; } @@ -213,7 +213,7 @@ int main() 名前空間の中で宣言された名前は、名前空間を指定しなければ使えなくなる。 -~~~c++ +~~~cpp namespace ns { int f() { return 0 ; } } @@ -249,7 +249,7 @@ int main() 例えば、アリスとボブがプログラムを共同で開発しているとする。あるプログラムのソースファイル`f`という名前の関数を書いたとする。ここで、同じプログラムを共同開発している他人も`f`という名前の関数を書いたらどうなるか。 -~~~c++ +~~~cpp // アリスの書いた関数f int f() { return 0 ; } @@ -259,7 +259,7 @@ int f() { return 1 ; } すでに宣言と定義で学んだように、このコードはエラーになる。なぜならば、同じ名前に対して定義が2つあるからだ。 -名前空間なしでこの問題を解決するためはに、アリスとボブが事前の申し合わせて、名前が衝突しないように調整する必要がある。 +名前空間なしでこの問題を解決するためはに、アリスとボブが事前に申し合わせて、名前が衝突しないように調整する必要がある。 しかし名前空間があるC++では、そのような面倒な調整は必要がない。アリスとボブが別の名前空間を使えばいいのだ。 @@ -351,7 +351,7 @@ int main() ### 名前空間名の別名を宣言する名前空間エイリアス -名前空間名には別名をつけることができる。これを名前空間エイリアスと呼ぶ。 +名前空間名には別名を付けることができる。これを名前空間エイリアスと呼ぶ。 たとえば名前空間名が重複することを恐れるあまり、とても長い名前空間名を付けたライブラリがあるとする。 @@ -366,9 +366,9 @@ int main() } ~~~ -この関数fを使うために毎回`very_long_name::f`と書くのは面倒だ。こういうときには名前空間エイリアスを使うとよい。名前空間エイリアスは名前空間名の別名を宣言できる。 +この関数`f`を使うために毎回`very_long_name::f`と書くのは面倒だ。こういうときには名前空間エイリアスを使うとよい。名前空間エイリアスは名前空間名の別名を宣言できる。 -~~~ +~~~c++ namespace 別名 = 名前空間名 ; ~~~ @@ -407,7 +407,7 @@ int main() 名前空間エイリアスを関数の中で宣言すると、その関数の中でだけ有効になる。 -~~~c++ +~~~cpp namespace A { int x { } ; } int f() @@ -427,7 +427,7 @@ int g() 名前空間エイリアスを名前空間の中で宣言すると、宣言以降の名前空間内で有効になる。 -~~~c++ +~~~cpp namespace ns { namespace A { int x { } ; } namespace B = A ; @@ -467,16 +467,16 @@ int main() } ~~~ -もし自分のソースファイルが`string`, `vector`, `cout`、その他`std`名前空間で使われる名前を一切使っていない場合、名前の衝突は発生しないことになる。その場合でも名前空間名を指定しなければならないのは面倒だ。 +もし自分のソースファイルが`string`, `vector`, `cout`、その他`std`名前空間で使われる名前をいっさい使っていない場合、名前の衝突は発生しないことになる。その場合でも名前空間名を指定しなければならないのは面倒だ。 C++では指定した名前空間を省略できる機能が存在する。`using`ディレクティブだ。 -~~~ +~~~c++ using namespace 名前空間名 ; ~~~ -これを使えば、先程のコードは以下のように書ける。 +これを使えば、先ほどのコードは以下のように書ける。 ~~~cpp int main() @@ -495,7 +495,7 @@ int main() では名前が衝突した場合はどうなるのか。 -~~~c++ +~~~cpp namespace abc { int f() { return 0 ; } } @@ -514,7 +514,7 @@ int main() 名前`f`に対してどの名前を使用するのか曖昧になってエラーになる。このエラーを回避するためには、名前空間を直接指定する。 -~~~c++ +~~~cpp namespace abc { int f() { return 0 ; } } @@ -550,7 +550,7 @@ namespace B { } ~~~ -名前空間のなかに`using`ディレクティブを書くと、その名前空間の中では指定した名前空間を省略できる。 +名前空間の中に`using`ディレクティブを書くと、その名前空間の中では指定した名前空間を省略できる。 グローバル名前空間は名前空間なので`using`ディレクティブが書ける。 @@ -558,19 +558,19 @@ namespace B { using namespace std ; ~~~ -ただし、グローバル名前空間の中に`using`ディレクティブを書くと、それ以降全ての箇所で指定した名前空間の省略ができてしまうので注意が必要だ。 +ただし、グローバル名前空間の中に`using`ディレクティブを書くと、それ以降すべての箇所で指定した名前空間の省略ができてしまうので注意が必要だ。 ### 名前空間を指定しなくてもよいinline名前空間 `inline名前空間`は`inline namespace`で定義する。 -~~~ +~~~c++ inline namespace name { } ~~~ `inline`名前空間内の名前は名前空間名を指定して使うこともできるし、名前空間を指定せずとも使うことができる。 -~~~c++ +~~~cpp inline namespace A { int a { } ; } @@ -589,14 +589,14 @@ int main() } ~~~ -読者が`inline`名前空間を使うことはほとんど無いだろうが、ライブラリのソースファイルを読むときには出てくるだろう。 +読者が`inline`名前空間を使うことはほとんどないだろうが、ライブラリのソースファイルを読むときには出てくるだろう。 ## 型名 型名とは型を表す名前だ。 -型名は`int`や`double`のように言語組み込みのキーワードを使うこともあれば、独自に作った型名を使うこともある。この独自に作った型名を専門用語ではユーザー定義された型(user-defined type)という。ユーザー定義された型を作る方法は様々だ。具体的に説明するのは本書のだいぶ後の方になるだろう。例としては、`std::string`や`std::vector`がある。標準ライブラリによってユーザー定義された型だ。 +型名は`int`や`double`のように言語組み込みのキーワードを使うこともあれば、独自に作った型名を使うこともある。この独自に作った型名を専門用語ではユーザー定義された型(user-defined type)という。ユーザー定義された型を作る方法はさまざまだ。具体的に説明するのは本書のだいぶあとの方になるだろう。例としては、`std::string`や`std::vector`がある。標準ライブラリによってユーザー定義された型だ。 ~~~cpp // 組み込みの型名 @@ -615,7 +615,7 @@ std::vector v ; 型名の別名を宣言するにはエイリアス宣言を使う。 -~~~ +~~~c++ using 別名 = 型名 ; ~~~ @@ -636,7 +636,7 @@ int main() 歴史的な経緯により、エイリアス宣言による型名の別名のことを、`typedef名`(typedef name)という。これは`typedef`名を宣言する文法が、かつては`typedef`キーワードを使ったものだったからだ。`typedef`キーワードを使った`typedef`名の宣言方法は、昔のコードによく出てくるので現代でも覚えておく必要はある。 -~~~ +~~~c++ typedef 型名 typedef名 ; ~~~ @@ -654,13 +654,13 @@ int main() これは変数の宣言と同じ文法だ。変数の宣言が以下のような文法で、 -~~~ +~~~c++ 型名 変数名 ; ~~~ これに`typedef`キーワードを使えば`typedef`名の宣言になる。 -しかし`typedef`キーワードによる`typedef`名の宣言は罠が多い。例えば熟練のC++プログラマーでも、以下のコードが合法だということに驚くだろう。 +しかし`typedef`キーワードによる`typedef`名の宣言はわなが多い。例えば熟練のC++プログラマーでも、以下のコードが合法だということに驚くだろう。 ~~~cpp int main() @@ -670,9 +670,9 @@ int main() } ~~~ -しかし本書ではまだ教えていない複雑な型名について、このようなコードを書こうとするとコンパイルエラーになることに熟練のC++プログラマーは気がつくはずだ。その理由はとても難しい。 +しかし本書ではまだ教えていない複雑な型名について、このようなコードを書こうとするとコンパイルエラーになることに熟練のC++プログラマーは気が付くはずだ。その理由はとても難しい。 -エイリアス宣言にはこのような罠はない。 +エイリアス宣言にはこのようなわなはない。 ## スコープ @@ -689,7 +689,7 @@ void f() } // 関数スコープの終わり ~~~ -これとは別にブロック文のスコープもある。ブロックとは関数の中で複数の文をたばねて一つの文として扱う機能だ。覚えているだろうか。 +これとは別にブロック文のスコープもある。ブロックとは関数の中で複数の文を束ねて1つの文として扱う機能だ。覚えているだろうか。 ~~~cpp void f() @@ -704,7 +704,7 @@ void f() スコープは`{`に始まり`}`に終わる。 -なぜスコープという概念について説明したかと言うと、宣言された名前が有効な範囲は、宣言された最も内側のスコープの範囲だからだ。 +なぜスコープという概念について説明したかというと、宣言された名前が有効な範囲は、宣言された最も内側のスコープの範囲だからだ。 ~~~cpp @@ -742,7 +742,7 @@ void f() その逆はできない。 -~~~c++ +~~~cpp void f() { { int a {} ; } @@ -753,7 +753,7 @@ void f() 名前空間も同じだ。 -~~~c++ +~~~cpp // グローバル名前空間スコープ namespace ns { @@ -805,6 +805,6 @@ int main() } ~~~ -宣言されている場所に注意が必要だ。名前`f`は3つある。最初の関数呼び出しの時点ではグローバル名前空間の`f`が呼ばれる。まだ名前`f`は関数`main`の中で宣言されていないからだ。そして関数`main`のスコープの中で名前`f`が宣言される。このときグローバル名前空間の`f`は隠される。そのため、次の関数`f`の呼び出しでは関数`main`の`f`が呼ばれる。次にブロックの中に入る。ここで関数`f`が呼ばれるが、まだこの`f`は関数`main`の`f`だ。その後にブロックの中で名前`f`が宣言される。すると次の関数`f`の呼び出しはブロックの`f`だ。ブロックから抜けた後の関数`f`の呼び出しは関数`main`の`f`だ。 +宣言されている場所に注意が必要だ。名前`f`は3つある。最初の関数呼び出しの時点ではグローバル名前空間の`f`が呼ばれる。まだ名前`f`は関数`main`の中で宣言されていないからだ。そして関数`main`のスコープの中で名前`f`が宣言される。このときグローバル名前空間の`f`は隠される。そのため、次の関数`f`の呼び出しでは関数`main`の`f`が呼ばれる。次にブロックの中に入る。ここで関数`f`が呼ばれるが、まだこの`f`は関数`main`の`f`だ。そのあとにブロックの中で名前`f`が宣言される。すると次の関数`f`の呼び出しはブロックの`f`だ。ブロックから抜けたあとの関数`f`の呼び出しは関数`main`の`f`だ。 この章では名前について解説した。名前は難しい。難しいが、プログラミングにおいては名前と向き合わなければならない。 diff --git a/014-iterator.md b/014-iterator.md index f2c7e89..3caa026 100644 --- a/014-iterator.md +++ b/014-iterator.md @@ -1,6 +1,6 @@ # イテレーターの基礎 -`vector`の章では`vector`の要素にアクセスする方法としてメンバー関数`at(i)`を学んだ。`at(i)`はi番目の要素にアクセスできる。ただし最初の要素は0番目だ。 +`vector`の章では`vector`の要素にアクセスする方法としてメンバー関数`at(i)`を学んだ。`at(i)`は`i`番目の要素にアクセスできる。ただし最初の要素は0番目だ。 ~~~cpp int main() @@ -220,7 +220,7 @@ int main() ## なんでもイテレーター -イテレーターというのは要素にアクセスする回りくどくて面倒な方法に見える。イテレーターという面倒なものを使わずに、`vector::at(i)`でi番目の要素にアクセスするほうが楽ではないか。そう考える読者もいるだろう。イテレーターの利点はその汎用性にある。イテレーターの作法に従うことで、様々な処理が同じコードで書けるようになるのだ。 +イテレーターというのは要素にアクセスする回りくどくて面倒な方法に見える。イテレーターという面倒なものを使わずに、`vector::at(i)`で`i`番目の要素にアクセスする方が楽ではないか。そう考える読者もいるだろう。イテレーターの利点はその汎用性にある。イテレーターの作法に従うことで、さまざまな処理が同じコードで書けるようになるのだ。 たとえば、`vector`の要素を先頭から順番に出力する処理を振り返ってみよう。 @@ -270,10 +270,10 @@ int main() } ~~~ -この`関数output_all`は`vector`以外のイテレーターにも対応している。C++には様々なイテレーターがある。例えば標準入力から値を受け取るイテレーターがある。早速使ってみよう。 +この`関数output_all`は`vector`以外のイテレーターにも対応している。C++にはさまざまなイテレーターがある。例えば標準入力から値を受け取るイテレーターがある。さっそく使ってみよう。 -~~~c++ +~~~cpp int main() { std::istream_iterator first( std::cin ), last ; @@ -284,9 +284,9 @@ int main() このプログラムは標準入力から`int`型の値を受け取り、それをそのまま標準出力する。 -C++には他にも、カレントディレクトリーにあるファイルの一覧を取得するイテレーターがある。 +C++にはほかにも、カレントディレクトリーにあるファイルの一覧を取得するイテレーターがある。 -~~~c++ +~~~cpp int main() { std::filesystem::directory_iterator first("./"), last ; @@ -295,11 +295,11 @@ int main() } ~~~ -`関数output_all`のコードは何も変えていないのに、様々なイテレーターに対応できる。イテレーターというお作法に乗っ取ることで、様々な処理が可能になるのだ。 +`関数output_all`のコードは何も変えていないのに、さまざまなイテレーターに対応できる。イテレーターというお作法に乗っ取ることで、さまざまな処理が可能になるのだ。 これは出力にも言えることだ。`関数output_all`は`std::cout`に出力していた。これをイテレーターに対する書き込みに変えてみよう。 -~~~c++ +~~~cpp auto output_all = []( auto first, auto last, auto output_iter ) { for ( auto iter = first ; iter != last ; ++iter, ++output_iter ) @@ -311,11 +311,11 @@ auto output_all = []( auto first, auto last, auto output_iter ) 書き換えた`関数output_all`は新しく`output_iter`という引数を取る。これはイテレーターだ。`std::cout`に出力する代わりに、このイテレーターに書き込むように変更している。 -こうすることによって、出力にも様々なイテレーターが使える。 +こうすることによって、出力にもさまざまなイテレーターが使える。 標準出力に出力するイテレーターがある。 -~~~c++ +~~~cpp int main() { std::vector v = {1,2,3,4,5} ; @@ -327,7 +327,7 @@ int main() `vector`も出力先にできる。つまり`vector`のコピーだ。 -~~~c++ +~~~cpp int main() { std::vector source = {1,2,3,4,5} ; @@ -340,14 +340,14 @@ int main() `destination(5)`というのは、`vector`にあらかじめ5個の要素を入れておくという意味だ。あらかじめ入っている要素の値は`int`の場合ゼロになる。 -この他にもイテレーターは様々ある。自分でイテレーターを作ることもできる。そして、`関数output_all`はイテレーターにさえ対応していれば様々な処理にコードを一行たりとも変えずに使えるのだ。 +このほかにもイテレーターはさまざまある。自分でイテレーターを作ることもできる。そして、`関数output_all`はイテレーターにさえ対応していればさまざまな処理にコードを1行たりとも変えずに使えるのだ。 ## イテレーターと添字の範囲 イテレーターは順序のある値の集合を表現するために、最初の要素への参照と、最後の次の要素への参照のペアを用いる。 -たとえば、`{1,2,3,4,5}`という順序の値の集合があった場合、イテレーターは最初の要素である`1`と最後の一つ次の要素である5の次の架空の要素を指し示す。 +たとえば、`{1,2,3,4,5}`という順序の値の集合があった場合、イテレーターは最初の要素である`1`と最後の1つ次の要素である5の次の架空の要素を指し示す。 ~~~cpp int main() @@ -363,7 +363,7 @@ int main() この状態から`{2,3,4,5}`のような値の集合を表現したい場合、イテレーター`i`をインクリメントすればよい。 -~~~ +~~~cpp ++i ; ~~~ @@ -389,9 +389,9 @@ int main() ではなぜなのか。なぜ`vector`では$n$個の要素の順番を0番目から$n-1$番目として表現するのか。 -実はC++に限らず、現在使われているすべてのプログラミングはインデックスを`0`から始めている。かつてはインデックスを`1`からはじめる言語も存在したが、そのような言語は今は使われていない。 +実はC++に限らず、現在使われているすべてのプログラミングはインデックスを`0`から始めている。かつてはインデックスを`1`から始める言語も存在したが、そのような言語はいまは使われていない。 -この疑問はエドガー・ダイクストラが"Why numbering should start at zero"(EWD831)で解説している。 +この疑問はエドガー・ダイクストラ(Edsger Wybe Dijkstra)が"Why numbering should start at zero"(EWD831)で解説している。 2, 3, ..., 12の範囲の自然数を表現するのに、慣習的に以下の4つの表記がある。 @@ -438,13 +438,13 @@ int main() a)とb)はどちらがいいのだろうか。b)を元にイテレーターを設計すると以下のようになる。 -~~~c++ +~~~cpp // b)案を採用する場合 int main() { std::vector v = {1,2,3,4,5} ; - // 最初の一つ前の架空の要素を指す + // 最初の1つ前の架空の要素を指す auto i = std::begin(v) ; // 最後の要素を指す auto j = std::end(v) ; @@ -489,11 +489,11 @@ int main() b)案では末尾から先頭まで後ろ向きに要素を一巡する操作はやりやすいが、実際には先頭から末尾まで一巡する操作の方が多い。 -C++では要素の順番を数値で指し示す時、最初の要素は0番目であり、次の要素は1番目であり、N個目の要素はN-1番目になっている。この数値で指し示すことを`添字`とか`インデックス`というがなぜ最初の要素を1番目にしないのか。 +C++では要素の順番を数値で指し示すとき、最初の要素は0番目であり、次の要素は1番目であり、$N$個目の要素は$N-1$番目になっている。この数値で指し示すことを`添字`とか`インデックス`というがなぜ最初の要素を1番目にしないのか。 -C++では様々なところでa)を採用している。これを添字に適用すると、最初の要素が1番目から始まる場合、N個の要素を参照する添字の範囲は$1 \le i \lt N+1$になる。そのような場合、以下のようなコードになる。 +C++ではさまざまなところでa)を採用している。これを添字に適用すると、最初の要素が1番目から始まる場合、$N$個の要素を参照する添字の範囲は$1 \le i \lt N+1$になる。そのような場合、以下のようなコードになる。 -~~~c++ +~~~cpp // 最初の要素が1番目の場合 int main() { diff --git a/015-reference.md b/015-reference.md index 304c722..0da1929 100644 --- a/015-reference.md +++ b/015-reference.md @@ -5,7 +5,7 @@ ## lvalueリファレンス -変数に変数を代入すると、代入元の値が代入先にコピーされる。代入先の値を変更しても、コピーされた値が変わるだけで、代入元には一切影響がない。 +変数に変数を代入すると、代入元の値が代入先にコピーされる。代入先の値を変更しても、コピーされた値が変わるだけで、代入元にはいっさい影響がない。 ~~~cpp int main() @@ -39,7 +39,7 @@ int main() } ~~~ -しかし、時には変数の値を直接書き換えたい場合がある。この時`lvalue`リファレンス(reference)が使える。`lvalue`リファレンスは変数に`&`を付けて宣言する +しかし、ときには変数の値を直接書き換えたい場合がある。このとき`lvalue`リファレンス(reference)が使える。`lvalue`リファレンスは変数に`&`を付けて宣言する ~~~cpp int main() @@ -58,7 +58,7 @@ int main() `lvalue`リファレンスは必ず初期化しなければならない。 -~~~c++ +~~~cpp int main() { // エラー @@ -100,7 +100,7 @@ int main() いちいち交換のために別の変数`temp`を作って3回代入を書くのは面倒だ。これを関数にしてしまいたい。 -~~~ +~~~cpp // 値を交換 swap( v.at(0), v.at(2) ) ; ~~~ @@ -148,13 +148,13 @@ int main() } ~~~ -ところで、この章では一貫して`lvalue`リファレンスと書いているのに気がついただろうか。`lvalue`とは何なのか、`lvalue`ではないリファレンスはあるのか。その疑問は後の章で解決する。 +ところで、この章では一貫して`lvalue`リファレンスと書いているのに気が付いただろうか。`lvalue`とは何なのか、`lvalue`ではないリファレンスはあるのか。その疑問はあとの章で解決する。 ## const -値を変更したくない変数は、`const`をつけることで変更を禁止できる。 +値を変更したくない変数は、`const`を付けることで変更を禁止できる。 -~~~c++ +~~~cpp int main() { int x = 0 ; @@ -179,7 +179,7 @@ int main() `const`は`lvalue`リファレンスと組み合わせることができる。 -~~~c++ +~~~cpp int main() { int x = 0 ; @@ -197,7 +197,7 @@ int main() `const`は本当に文法が変わっていて混乱する。`const int &`と`int const &`は同じ意味だが、`int & const`はエラーになる。 -~~~c++ +~~~cpp int main() { int a = 0 ; @@ -211,41 +211,41 @@ int main() } ~~~ -これはとても複雑なルールで決まっているので、こういうものだと諦めて覚えるしかない。 +これはとても複雑なルールで決まっているので、こういうものだとあきらめて覚えるしかない。 -`const`がついていない型のオブジェクトを`const`な`lvalue`リファレンスで参照することができる。 +`const`が付いていない型のオブジェクトを`const`な`lvalue`リファレンスで参照することができる。 ~~~cpp int main() { - // constのついていない型のオブジェクト + // constの付いていない型のオブジェクト int x = 0 ; // OK int & ref = x ; - // OK、constはつけてもよい + // OK、constは付けてもよい const int & cref = x ; } ~~~ -`const`のついている型のオブジェクトを`const`のついていない`lvalue`リファレンスで参照することはできない。 +`const`の付いている型のオブジェクトを`const`の付いていない`lvalue`リファレンスで参照することはできない。 -~~~c++ +~~~cpp int main() { - // constのついている型のオブジェクト + // constの付いている型のオブジェクト const int x = 0 ; // エラー、constがない。 int & ref = x ; - // OK、constがついている + // OK、constが付いている const int & cref = x ; } ~~~ -`const`のついている`lvalue`リファレンスは何の役に立つのかというと、関数の引数を取るときに役に立つ。 +`const`の付いている`lvalue`リファレンスは何の役に立つのかというと、関数の引数を取るときに役に立つ。 例えば以下のコードは非効率的だ。 diff --git a/016-algorithm.md b/016-algorithm.md index 21cad41..e2611b4 100644 --- a/016-algorithm.md +++ b/016-algorithm.md @@ -24,7 +24,7 @@ int main() このコードを書くのは難しい。このコードを書くには、イテレーターで要素の範囲を取り、ループを実行するごとにイテレーターを適切にインクリメントし、イテレーターが範囲内であるかどうかの判定をしなければならない。 -アルゴリズムを理解するだけでも難しいのに、正しくコード書くのは更に難しい。例えば以下はコンパイルが通る完全に合法なC++だが間違っている。 +アルゴリズムを理解するだけでも難しいのに、正しくコード書くのはさらに難しい。例えば以下はコンパイルが通る完全に合法なC++だが間違っている。 ~~~cpp int main() @@ -46,7 +46,7 @@ int main() まず比較の条件が間違っている。`i != j`となるべきところが`i == j`となっている。出力する部分も間違っている。イテレーター`i`が指し示す値を得るには`*i`としなければならないところ、単に`i`としている。 -毎回このようなイテレーターのループをするfor文を書くのは間違いの元だ。ここで重要なのは、要素のそれぞれに対して`std::cout << *i ;`を実行するということだ。要素を先頭から末尾まで順番に処理するというのはライブラリにやってもらいたい。 +毎回このようなイテレーターのループをする`for`文を書くのは間違いの元だ。ここで重要なのは、要素のそれぞれに対して`std::cout << *i ;`を実行するということだ。要素を先頭から末尾まで順番に処理するというのはライブラリにやってもらいたい。 そこでこの処理を関数に切り出してみよう。イテレーター`[first,last)`を渡すと、イテレーターを先頭から末尾まで順番に処理してくれる関数は以下のように書ける。 @@ -104,9 +104,9 @@ auto print_with_newline = []( auto first, auto last ) } ; ~~~ -これを見ると、`for`文によるイテレーターのループは全く同じコードだとわかる。 +これを見ると、`for`文によるイテレーターのループはまったく同じコードだとわかる。 -全く同じ`for`文を手で書くのは間違いの元だ。同じコードはできれば書きたくない。ここで必要なのは、共通な処理は一度書くだけで済ませ、特別な処理だけを記述すればすむような方法だ。 +まったく同じ`for`文を手で書くのは間違いの元だ。同じコードはできれば書きたくない。ここで必要なのは、共通な処理は一度書くだけで済ませ、特別な処理だけを記述すれば済むような方法だ。 この問題を解決するには、問題を分割することだ。問題を「`for`文によるループ」と「特別な処理」に分けることだ。 @@ -128,7 +128,7 @@ int main() ~~~cpp int main() { - // 関数を引数にとり呼び出す関数 + // 関数を引数に取り呼び出す関数 auto call_func = []( auto func ) { func(123) ; @@ -158,9 +158,9 @@ auto for_each = []( auto first, auto last, auto f ) } ; ~~~ -この関数はイテレーターをループで回す部分だけを実装していて、要素ごとの処理は引数に取った関数に任せている。早速使ってみよう。 +この関数はイテレーターをループで回す部分だけを実装していて、要素ごとの処理は引数に取った関数に任せている。さっそく使ってみよう。 -~~~c++ +~~~cpp int main() { std::vector v = {1,2,3,4,5} ; @@ -175,7 +175,7 @@ int main() for_each( std::begin(v), std::end(v), print_twice ) ; - // 引数を出力した後に改行を出力する関数 + // 引数を出力したあとに改行を出力する関数 auto print_with_newline = []( auto value ) { std::cout << value << "\n"s ; } ; for_each( std::begin(v), std::end(v), print_with_newline ) ; @@ -185,7 +185,7 @@ int main() 関数は変数に代入しなくても使えるので、上のコードは以下のようにも書ける。 -~~~c++ +~~~cpp int main() { std::vector v = {1,2,3,4,5} ; @@ -198,7 +198,7 @@ int main() for_each( std::begin(v), std::end(v), []( auto value ) { std::cout << 2 * value ; } ) ; - // 引数を出力した後に改行を出力する関数 + // 引数を出力したあとに改行を出力する関数 for_each( std::begin(v), std::end(v), []( auto value ) { std::cout << value << "\n"s ; } ) ; } @@ -220,7 +220,7 @@ int main() C++17の時点ではまだ使えないが、将来のC++では、イテレーターを渡さずに、`vector`を直接渡すことができるようになる予定だ。 -~~~c++ +~~~cpp // C++20予定 int main() @@ -270,21 +270,21 @@ int main() ## all_of/any_of/none_of -他のアルゴリズムも実装していくことで学んでいこう。 +ほかのアルゴリズムも実装していくことで学んでいこう。 `all_of(first, last, pred)`は、`[first,last)`の間のイテレーター`iter`のそれぞれに対して、`pred(*iter)`がすべて`true`を返すならば`true`、そうではないならば`false`を返すアルゴリズムだ。 -この`all_of`は要素が全て条件を満たすかどうかを調べるのに使える。 +この`all_of`は要素がすべて条件を満たすかどうかを調べるのに使える。 ~~~cpp -// 要素が全て偶数かどうか調べる関数 +// 要素がすべて偶数かどうか調べる関数 auto is_all_of_odd = []( auto first, auto last ) { return std::all_of( first, last, []( auto value ) { return value % 2 == 0 ; } ) ; } ; -// 要素が全て100以下かどうか調べる関数 +// 要素がすべて100以下かどうか調べる関数 auto is_all_of_le_100 = []( auto first, auto last ) { return std::all_of( first, last, @@ -325,21 +325,21 @@ auto all_of = []( auto first, auto last, auto pred ) `[first,last)`が空かどうかを確認する必要はない。というのも、空であればループは一度も実行されないからだ。 -`any_of(first, last, pred)`は`[first,last)`の間のイテレーター`iter`それぞれに対して、`pred(*iter)`がひとつでも`true`ならば`true`を返す。空の場合、すべて`false`の場合は`false`を返す。 +`any_of(first, last, pred)`は`[first,last)`の間のイテレーター`iter`それぞれに対して、`pred(*iter)`が1つでも`true`ならば`true`を返す。空の場合、すべて`false`の場合は`false`を返す。 -`any_of`は要素に一つでも条件を満たすものがあるかどうかを調べるのに使える。 +`any_of`は要素に1つでも条件を満たすものがあるかどうかを調べるのに使える。 ~~~cpp int main() { std::vector v = {1,2,3,4,5} ; - // 要素に一つでも3が含まれているか? + // 要素に1つでも3が含まれているか? // true bool has_3 = std::any_of( std::begin(v), std::end(v), []( auto x ) { return x == 3 ;} ) ; - // 要素に一つでも10が含まれているか? + // 要素に1つでも10が含まれているか? // false bool has_10 = std::any_of( std::begin(v), std::end(v), []( auto x ) { return x == 10 ;} ) ; @@ -460,9 +460,9 @@ auto pred = []( auto const & value ) -> bool } ; ~~~ -関数`pred`は値を一つ引数にとり、`bool`型を返す関数だ。 +関数`pred`は値を1つ引数に取り、`bool`型を返す関数だ。 -早速使ってみよう。 +さっそく使ってみよう。 ~~~cpp int main() @@ -639,7 +639,7 @@ auto count = []( auto first, auto last, auto pred ) ## equal -これまでのアルゴリズムは一つのイテレーターの範囲だけを扱ってきた。アルゴリズムの中には複数の範囲を取るものもある。 +これまでのアルゴリズムは1つのイテレーターの範囲だけを扱ってきた。アルゴリズムの中には複数の範囲を取るものもある。 `equal(first1, last1, first2, last2)`は`[first1, last1)`と`[first2, last2)`が等しい場合に`true`を返す。「等しい」というのは、要素の数が同じで、各要素がそれぞれ等しい場合を指す。 @@ -671,7 +671,7 @@ int main() } ~~~ -実装は、まず要素数を比較し、等しくなければ`false`を返す。次に各要素を一つずつ比較し、途中で等しくない要素が見つかれば`false`を、最後まで各要素が等しければ`true`を返す。 +実装は、まず要素数を比較し、等しくなければ`false`を返す。次に各要素を1つずつ比較し、途中で等しくない要素が見つかれば`false`を、最後まで各要素が等しければ`true`を返す。 イテレーターの範囲`[first, last)`の要素数は`last-first`で取得できる。 @@ -682,7 +682,7 @@ int main() // 最初の要素 auto first = std::begin(v) ; - // 最後の一つ次の要素 + // 最後の1つ次の要素 auto last = std::end(v) ; // 要素数: 5 @@ -714,7 +714,7 @@ int main() // 最初の要素 auto first = std::begin(v) ; - // 最後の一つ次の要素 + // 最後の1つ次の要素 auto last = std::end(v) ; // 要素数: 5 @@ -725,7 +725,7 @@ int main() } ~~~ -後は実装するだけだ(この実装は最も効率のいい実装ではない。理由についてはイテレーターの章を参照)。 +あとは実装するだけだ(この実装は最も効率のいい実装ではない。理由についてはイテレーターの章を参照)。 ~~~cpp auto equal = []( auto first1, auto last1, auto first2, auto last2) @@ -803,7 +803,7 @@ int main() この例では、`v1`の中に`v2`と同じ並びの`{4,5,6}`が存在するので`true`、`v3`と同じ並びの`{1,3,5}`は存在しないので`false`になる。 -`search`の実装例は今の読者にはまだ理解できない。`equal`や`search`を効率的に実装するにはイテレーターの詳細な理解が必要だ。 +`search`の実装例はいまの読者にはまだ理解できない。`equal`や`search`を効率的に実装するにはイテレーターの詳細な理解が必要だ。 ## copy @@ -863,7 +863,7 @@ int main() 例えば以下の例はエラーになる。 -~~~c++ +~~~cpp int main() { std::vector source = {1,2,3,4,5} ; @@ -902,7 +902,7 @@ int main() オーバーラップというのは、同じ要素を参照しているという意味だ。 -~~~c++ +~~~cpp int main() { std::vector v = {1,2,3} ; @@ -969,7 +969,7 @@ int main() } ~~~ -`result`に代入されるのは関数`op`の戻り値だ。関数`op`は値を一つの引数受け取り値を返す関数だ。 +`result`に代入されるのは関数`op`の戻り値だ。関数`op`は値を1つの引数受け取り値を返す関数だ。 ## replace @@ -999,7 +999,7 @@ auto replace = []( auto first, auto last, auto old_value, auto new_value ) ## fill -`fill(first, last, value)`はイテレーター`[first,last)`の範囲をのイテレーターが参照する要素に`value`を代入する。 +`fill(first, last, value)`はイテレーター`[first,last)`の範囲をイテレーターが参照する要素に`value`を代入する。 ~~~cpp int main() @@ -1036,7 +1036,7 @@ auto fill_n = []( auto first, auto n, auto value ) ## generate -`generate`は`fill`に似ているが、値として`value`をとるのではなく、関数`gen`を取る。 +`generate`は`fill`に似ているが、値として`value`を取るのではなく、関数`gen`を取る。 `generate(first, last, gen)`はイテレーター`[first, last)`の範囲のイテレーターが参照する要素に`gen()`を代入する。 @@ -1118,11 +1118,11 @@ auto generate_n = []( first, n, gen ) `remove`の戻り値は、新しいイテレーターの終端を返す。 -~~~c++ +~~~cpp auto last2 = remove( first, last, value ) ; ~~~ -この例では、`remove`は`[first, last)`から値`value`に等しい要素を取り除いたイテレーターの範囲を戻り値として返す。その戻り値が`last2`だ。`[first, last2)`が値を取り除いた後の新しいイテレーターの範囲だ。 +この例では、`remove`は`[first, last)`から値`value`に等しい要素を取り除いたイテレーターの範囲を戻り値として返す。その戻り値が`last2`だ。`[first, last2)`が値を取り除いたあとの新しいイテレーターの範囲だ。 `remove`を呼び出しても元の`vector`の要素数が変わることはない。`remove`は`vector`の要素の値を変更するだけだ。 diff --git a/017-lambda.md b/017-lambda.md index 1573a11..bb97dcd 100644 --- a/017-lambda.md +++ b/017-lambda.md @@ -10,15 +10,15 @@ auto function = []( auto value ) { return value } ; ## 基本 -`ラムダ式`の基本の文法は以下の通り +`ラムダ式`の基本の文法は以下のとおり。 -~~~cpp +~~~c++ [](){} ; ~~~ これを細かく分解すると以下のようになる。 -~~~cpp +~~~c++ [] // ラムダ導入子 () // 引数リスト {} // 複合文 @@ -65,7 +65,7 @@ int main() } ; ~~~ -最後の`文末`は`文`の最後につけるセミコロンだ。これは`"1+1 ;"`とするのと変わらない。`"1+1"`や`"[](){}"`は`式`で、`文`は`式`を使うことができる。`式`だけが入った`文`を専門用語では`式文`と呼ぶが特に覚える必要はない。 +最後の`文末`は`文`の最後に付けるセミコロンだ。これは`"1+1 ;"`とするのと変わらない。`"1+1"`や`"[](){}"`は`式`で、`文`は`式`を使うことができる。`式`だけが入った`文`を専門用語では`式文`と呼ぶが特に覚える必要はない。 ~~~cpp 1 + 1 ; // OK、式文 @@ -100,7 +100,7 @@ f // 関数 ( "hello"s ) ; // 関数呼び出し ~~~ -ラムダ式が引数を一つも取らない場合、`引数リスト`は省略できる。 +ラムダ式が引数を1つも取らない場合、`引数リスト`は省略できる。 ~~~cpp // 引数を取らないラムダ式 @@ -133,7 +133,7 @@ f // 関数 } ; ~~~ -戻り値の型を指定したい場合は`引数リスト`の後に`->`を書き、型名を書く。 +戻り値の型を指定したい場合は`引数リスト`のあとに`->`を書き、型名を書く。 ~~~cpp @@ -190,7 +190,7 @@ int main() `コピーキャプチャー`した変数はラムダ式の中で変更できない。 -~~~c++ +~~~cpp int main() { int x = 0 ; @@ -252,4 +252,4 @@ int main() } ~~~ -ラムダ式についてはまだ色々な機能があるが、本書での解説はここまでとする。 +ラムダ式についてはまだいろいろな機能があるが、本書での解説はここまでとする。 diff --git a/018-class.md b/018-class.md index 8b4ce57..fba33e2 100644 --- a/018-class.md +++ b/018-class.md @@ -2,7 +2,7 @@ C++はもともとC言語に`クラス`の機能を追加することを目的とした言語だった。 -`クラス`とは何か。クラスには様々な機能があるが、最も基本的な機能としては以下の2つがある。 +`クラス`とは何か。クラスにはさまざまな機能があるが、最も基本的な機能としては以下の2つがある。 + 変数をまとめる + まとめた変数に関数を提供する @@ -83,7 +83,7 @@ int main() 点を複数表現するのもわかりやすい。 -~~~c++ +~~~cpp point p1 ; point p2 ; point p3 ; @@ -258,7 +258,7 @@ int main() ところで、この出力を毎回書くのが面倒だ。こういう処理は関数にまとめたい。 -~~~c++ +~~~cpp double value( fractional & x ) { return static_cast(x.num) / static_cast(x.denom) ; @@ -300,9 +300,9 @@ struct fractional } ; ~~~ -メンバー関数を呼び出すには、クラスのオブジェクトに続いてドット文字を書き、メンバー関数名を書く。後は通常の関数のように書く。 +メンバー関数を呼び出すには、クラスのオブジェクトに続いてドット文字を書き、メンバー関数名を書く。あとは通常の関数のように書く。 -~~~c++ +~~~cpp int main() { fractional x{ 1, 2 } ; @@ -334,7 +334,7 @@ int main() この`print`を非メンバー関数として書くと以下のようになる。 -~~~c++ +~~~cpp void print( S & s ) { std::cout << s.x ; @@ -343,7 +343,7 @@ void print( S & s ) メンバー関数は隠し引数としてクラスのオブジェクトを受け取っている関数だ。メンバー関数の呼び出しには、対応するクラスのオブジェクトが必要になる。 -~~~c++ +~~~cpp struct S { void f() { } @@ -370,7 +370,7 @@ struct X } ; ~~~ -先程の分数クラスに値を設定するための`メンバー関数`を追加してみよう。 +先ほどの分数クラスに値を設定するための`メンバー関数`を追加してみよう。 ~~~cpp struct fractional diff --git a/019-operator-overloading.md b/019-operator-overloading.md index f2426d3..dd4f966 100644 --- a/019-operator-overloading.md +++ b/019-operator-overloading.md @@ -42,7 +42,7 @@ int main() これは読みにくい。できれば以下のように書きたいところだ。 -~~~c++ +~~~cpp int main() { fractional a = 1 ; @@ -119,7 +119,7 @@ struct class_name } ; ~~~ -`x(x)`の最初の`x`は`class_name::x`として、次の`x`は引数名の`x`として認識される。そのためこのコードは期待通りに動く。 +`x(x)`の最初の`x`は`class_name::x`として、次の`x`は引数名の`x`として認識される。そのためこのコードは期待どおりに動く。 `コンストラクター`の特別なメンバー初期化を使わずに、`コンストラクター`の関数の本体で`データメンバー`を変更してもよい。 @@ -139,7 +139,7 @@ struct class_name コンストラクターはクラスが初期化されるときに実行される。例えば以下のプログラムを実行すると、 -~~~c++ +~~~cpp int main() { S a(1) ; @@ -208,7 +208,7 @@ struct S このクラスを以下のように使うと、 -~~~c++ +~~~cpp int main() { S a(1) ; @@ -217,7 +217,7 @@ int main() } ~~~ -以下のように出力される +以下のように出力される。 ~~~ constructed: 1 @@ -237,7 +237,7 @@ destructed: 1 1. `c`が破棄される 1. `a`が破棄される -`b`はブロックスコープの終わりに達したので`a`の構築の後、`c`の構築の前に破棄される。破棄は構築の逆順で行われるので、`a`よりも先に`c`が破棄される。 +`b`はブロックスコープの終わりに達したので`a`の構築のあと、`c`の構築の前に破棄される。破棄は構築の逆順で行われるので、`a`よりも先に`c`が破棄される。 `コンストラクター`と`デストラクター`は戻り値を返さないので、`return文`には値を書かない。 @@ -279,9 +279,9 @@ int main() } ~~~ -複数の引数をとるコンストラクターを呼び出すには`"="`は使えない。`"()"`か`"{}"`を使う必要がある。 +複数の引数を取るコンストラクターを呼び出すには`"="`は使えない。`"()"`か`"{}"`を使う必要がある。 -上のコードを見ると、コンストラクターは引数の数以外にやっていることはほとんど同じだ。こういう場合、コンストラクターを一つにする方法がある。 +上のコードを見ると、コンストラクターは引数の数以外にやっていることはほとんど同じだ。こういう場合、コンストラクターを1つにする方法がある。 実はコンストラクターに限らず、関数は`デフォルト実引数`を取ることができる。書き方は仮引数に`"="`で値を書く。 @@ -298,7 +298,7 @@ int main() `デフォルト実引数`を指定した関数の仮引数に実引数を渡さない場合、`デフォルト実引数`で指定した値が渡される。 -ところで、`仮引数`、`実引数`という聞きなれない言葉が出てきた。これは関数の引数を区別するための言葉だ。`仮引数`は関数の宣言の引数。実引数は関数呼び出しのときに引数に渡す値のことを意味する。 +ところで、`仮引数`、`実引数`という聞き慣れない言葉が出てきた。これは関数の引数を区別するための言葉だ。`仮引数`は関数の宣言の引数。実引数は関数呼び出しのときに引数に渡す値のことを意味する。 ~~~cpp // xは仮引数 @@ -327,7 +327,6 @@ void g( int x, int y = 0, int z ) { } ~~~cpp - void f( int x = 0, int y = 0, int z = 0) { } int main() @@ -337,7 +336,7 @@ int main() } ~~~ -`デフォルト実引数`を使うと、コンストラクターを一つにできる。 +`デフォルト実引数`を使うと、コンストラクターを1つにできる。 ~~~cpp struct fractional @@ -358,7 +357,7 @@ int main() } ~~~ -コンストラクターの数を減らす方法はもう一つある。`デリゲートコンストラクター`だ。 +コンストラクターの数を減らす方法はもう1つある。`デリゲートコンストラクター`だ。 ~~~cpp struct fractional @@ -411,7 +410,7 @@ delegating constructor コンストラクターを減らすのはよいが、減らしすぎても不便だ。以下の例を見てみよう。 -~~~c++ +~~~cpp struct A { } ; struct B { B(int) { } } ; @@ -482,7 +481,7 @@ int main() 分数クラスの足し算を考えよう。 + 分母が同じならば分子を足す -+ 分母が異なるならば互いの分母を掛けて、分母を揃えて足す。 ++ 分母が異なるならば互いの分母を掛けて、分母をそろえて足す。 コードにすると以下のようになる。 @@ -509,7 +508,7 @@ fractional add( fractional & l, fractional & r ) しかし、この関数`add`を使ったコードは以下のようになる。 -~~~c++ +~~~cpp int main() { fractional a{1,2} ; @@ -521,7 +520,7 @@ int main() これはわかりにくい。できれば、以下のように書きたい。 -~~~c++ +~~~cpp auto c = a + b ; ~~~ @@ -545,7 +544,7 @@ fractional operator +( fractional & l, fractional & r ) このように`operator +`を書くと、以下のようなコードが書ける。 -~~~c++ +~~~cpp auto c = a + b ; ~~~ @@ -553,7 +552,7 @@ auto c = a + b ; 以下に関数の宣言を示すので実際に分数の計算を実装してみよう。 -~~~c++ +~~~cpp fractional operator -( fractional & l, fractional & r ) ; fractional operator *( fractional & l, fractional & r ) ; fractional operator /( fractional & l, fractional & r ) ; @@ -561,7 +560,7 @@ fractional operator /( fractional & l, fractional & r ) ; 引き算は足し算とほぼ同じだ。 -~~~c++ +~~~cpp fractional operator -( fractional & l, fractional & r ) { // 分母が同じ @@ -574,7 +573,7 @@ fractional operator -( fractional & l, fractional & r ) 掛け算と割り算は楽だ。 -~~~c++ +~~~cpp fractional operator *( fractional & l, fractional & r ) { return fractional{ l.num * r.num, l.denom * r.denom } ; @@ -590,9 +589,9 @@ fractional operator /( fractional & l, fractional & r ) ### 二項演算子 -C++には様々な演算子があるが、多くが`二項演算子`と呼ばれる演算子だ。`二項演算子`は2つの引数を取り、値を返す。 +C++にはさまざまな演算子があるが、多くが`二項演算子`と呼ばれる演算子だ。`二項演算子`は2つの引数を取り、値を返す。 -~~~c++ +~~~cpp a + b ; a - b ; a * b ; @@ -601,7 +600,7 @@ a / b ; このような演算子は`operator +`のように、キーワード`operator`に続いて演算子の文字を書くことで、関数名とする。あとは通常の関数と変わらない。 -~~~c++ +~~~cpp struct S { } ; S add( S a, S b ) ; @@ -643,7 +642,7 @@ int main() `演算子のオーバーロード`では、少なくとも1つのユーザー定義された型がなければならない。つまり以下のような演算子のオーバーロードはできないということだ。 -~~~c++ +~~~cpp int operator +( int, int ) ; int operator +( int, double ) ; ~~~ @@ -651,7 +650,7 @@ int operator +( int, double ) ; 二項演算子には`オペランド`と呼ばれる式を取る。 -~~~c++ +~~~cpp a + b ; ~~~ @@ -681,7 +680,7 @@ int main() そのため、上の例で`"x+y"`と`"y+x"`を両方使いたい場合は、 -~~~c++ +~~~cpp void operator +(Y,X) { } ~~~ @@ -692,7 +691,7 @@ void operator +(Y,X) { } ~~~cpp struct S { } ; -// 引数名は様々 +// 引数名はさまざま S operator +( S const & left, S const & right ) { @@ -703,7 +702,7 @@ S operator +( S const & left, S const & right ) `const`というのは値を変更しない変数を宣言する機能だ。 -~~~c++ +~~~cpp int main() { int x = 0 ; @@ -714,7 +713,7 @@ int main() } ~~~ -`const`をつけると値を変更できなくなる。 +`const`を付けると値を変更できなくなる。 一般に`operator +`のような演算子は、オペランドに渡した変数を書き換えない処理をすることが期待されている。 @@ -749,7 +748,7 @@ IntLike operator + ( IntLike const & l, IntLike const & r ) ### 単項演算子 -`単項演算子`はオペランドを一つしか取らない演算子のことだ。 +`単項演算子`はオペランドを1つしか取らない演算子のことだ。 `単項演算子`についてはまだ説明していないものも多い。例えば、`operator +`や`operator -`がある。 @@ -762,7 +761,7 @@ int main() } ~~~ -`単項演算子`は引数を一つしか取らない関数として書く。 +`単項演算子`は引数を1つしか取らない関数として書く。 ~~~cpp struct IntLike{ int data ;} ; @@ -780,7 +779,7 @@ IntLIke operator -( IntLike const & obj ) ### インクリメント/デクリメント -`インクリメント演算子`と`デクリメント演算子`はやや変わっている。この演算子には、オペランドの前に書く前置演算子(`++i`)と、後に書く後置演算子(`i++`)がある。 +`インクリメント演算子`と`デクリメント演算子`はやや変わっている。この演算子には、オペランドの前に書く前置演算子(`++i`)と、あとに書く後置演算子(`i++`)がある。 ~~~cpp int main() @@ -794,9 +793,9 @@ int main() } ~~~ -前置演算子を評価すると、演算子を評価した後の値になる。 +前置演算子を評価すると、演算子を評価したあとの値になる。 -~~~c++ +~~~cpp int i = 0 ; ++i ; // 1 i ; // 1 @@ -804,7 +803,7 @@ i ; // 1 一方、後置演算子を評価すると、演算子を評価する前の値になる。 -~~~c++ +~~~cpp int i = 0 ; i++ ; // 0 i ; // 1 @@ -812,7 +811,7 @@ i ; // 1 さらに前置演算子を評価した結果はリファレンスになるので代入やさらなる演算子の適用ができる。 -~~~c++ +~~~cpp int i = 0 ; ++i = 0 ; // iは0 ++++i ; // iは2 @@ -879,7 +878,7 @@ IntLike operator --( IntLike & obj, int ) } ~~~ -後置演算子は2つめの引数として`int`型を取る。この引数はダミーで前置演算子と後置演算子を区別する以外の意味はない。意味はないので引数名は省略している。 +後置演算子は2つ目の引数として`int`型を取る。この引数はダミーで前置演算子と後置演算子を区別する以外の意味はない。意味はないので引数名は省略している。 ~~~cpp struct S { } ; @@ -898,12 +897,12 @@ void operator ++( S, int ) ; 例えば、 -~~~c++ +~~~cpp S s ; s + s ; ~~~ -を可能にするクラスSに対する`operator +`は、 +を可能にするクラス`S`に対する`operator +`は、 ~~~cpp struct S { } @@ -922,7 +921,7 @@ struct S } ; ~~~ -演算子のオーバーロードをメンバー関数で書く場合、最初のオペランドがメンバー関数の属するクラスのオブジェクト、2つめのオペランドが一つ目の引数になる。 +演算子のオーバーロードをメンバー関数で書く場合、最初のオペランドがメンバー関数の属するクラスのオブジェクト、2つ目のオペランドが1つ目の引数になる。 ~~~cpp struct IntLike @@ -948,11 +947,11 @@ int main() 普通のメンバー関数のように呼ぶこともできる。 -~~~c++ +~~~cpp IntLike c = a.operator +( b ) ; ~~~ -一見戸惑うかも知れないが、これは普通のメンバー関数呼び出しと何ら変わらない。 +一見戸惑うかもしれないが、これは普通のメンバー関数呼び出しと何ら変わらない。 ~~~cpp struct S diff --git a/020-array.md b/020-array.md index 8ebec79..bcff526 100644 --- a/020-array.md +++ b/020-array.md @@ -51,7 +51,7 @@ int main() 一方、`array`はコンパイル時に要素数を決める。標準入力から得た値は実行時のものなので、使うことはできない。 -~~~c++ +~~~cpp int main() { std::size_T N{} ; @@ -78,7 +78,7 @@ int main() `array`は`push_back`も`resize`も提供していない。 -`vector`も`array`もメンバー関数`at(i)`で`i`番目の要素にアクセスできる。実は、`i`番目にアクセスする方法は他にもある。`[i]`を使う方法だ。 +`vector`も`array`もメンバー関数`at(i)`で`i`番目の要素にアクセスできる。実は、`i`番目にアクセスする方法はほかにもある。`[i]`を使う方法だ。 ~~~cpp int main() diff --git a/021-three-virtues-of-a-programmer.md b/021-three-virtues-of-a-programmer.md index 48685d6..51ac5c6 100644 --- a/021-three-virtues-of-a-programmer.md +++ b/021-three-virtues-of-a-programmer.md @@ -1,14 +1,14 @@ # プログラマーの三大美徳 -プログラミング言語Perlの作者、Larry Wallは著書「プログラミング言語Perl」の初版で以下のように宣言した。 +プログラミング言語Perlの作者、Larry Wallは著書『プログラミングPerl』の初版で以下のように宣言した。 > 読者はプログラマーの三大美徳である、怠惰、短気、傲慢を会得すべきである。 -第二版の巻末の用語集では、以下のような定義が与えらた。 +第2版の巻末の用語集では、以下のような定義が与えらた。 怠惰 -: プログラマーは労力を削減するための労力を惜しまないこと。怠惰のために書いたプログラムは他人にも便利であり、そしてドキュメントを書くことにより自ら他人の質問に答えずにすむようにすること。これがプログラマーの第一の美徳である。これが本書の書かれた理由である。 +: プログラマーは労力を削減するための労力を惜しまないこと。怠惰のために書いたプログラムは他人にも便利であり、そしてドキュメントを書くことにより自ら他人の質問に答えずに済むようにすること。これがプログラマーの第一の美徳である。これが本書の書かれた理由である。 短気 @@ -18,4 +18,4 @@ : ゼウスも罰したもう過剰なまでの奢り。他人がそしりを入れられぬほどのプログラムを書く推進剤。これがプログラマーの第三の美徳である。 -これから学ぶ`array`を実装するためのC++の機能を学ぶときに、このプログラマーの三大美徳のことを頭にいれておこう。 +これから学ぶ`array`を実装するためのC++の機能を学ぶときに、このプログラマーの三大美徳のことを頭に入れておこう。 diff --git a/022-implement-array.md b/022-implement-array.md index 95fa9c8..4759916 100644 --- a/022-implement-array.md +++ b/022-implement-array.md @@ -4,7 +4,7 @@ `std::array`を実装してみよう。すでにクラスを作る方法については学んだ。 -`std::array`は`T`型の要素を`N`個保持するクラスだ。この``についてはまだ学んでいないので、今回は`int`型を3個確保する。今までに学んだ要素だけで実装してみよう。 +`std::array`は`T`型の要素を`N`個保持するクラスだ。この``についてはまだ学んでいないので、今回は`int`型を3個確保する。いままでに学んだ要素だけで実装してみよう。 ~~~cpp struct array_int_3 @@ -15,7 +15,7 @@ struct array_int_3 } ; ~~~ -そして`operator []`を実装しよう。引数が`0`なら`m0`を、1ならm1を、2ならm2を返す。それ以外の値の場合、プログラムを強制的に終了させる標準ライブラリ、`std::abort`を呼び出す。 +そして`operator []`を実装しよう。引数が`0`なら`m0`を、`1`なら`m1`を、`2`なら`m2`を返す。それ以外の値の場合、プログラムを強制的に終了させる標準ライブラリ、`std::abort`を呼び出す。 @@ -49,21 +49,21 @@ struct array_int_3 `std::array`を実装するには、`配列(array)`を使う。 -int型の要素数10の配列aは以下のように書く。 +`int`型の要素数10の配列`a`は以下のように書く。 ~~~cpp int a[10] ; ~~~ -double型の要素数5の配列bは以下のように書く。 +`double`型の要素数5の配列`b`は以下のように書く。 ~~~cpp double b[5] ; ~~~ -`配列`の要素数は`std::array`のNと同じようにコンパイル時定数でなければならない。 +`配列`の要素数は`std::array`の`N`と同じようにコンパイル時定数でなければならない。 -~~~c++ +~~~cpp int main() { std::size_t size ; @@ -97,10 +97,9 @@ int main() 配列にはメンバー関数はない。`at(i)`や`size()`のような便利なメンバー関数はない。 -配列のサイズは`sizeof`で取得できる。配列のサイズは配列の要素の型のサイズかけることの要素数のサイズになる。 +配列のサイズは`sizeof`で取得できる。配列のサイズは配列の要素の型のサイズ掛けることの要素数のサイズになる。 ~~~cpp - int main() { auto print = [](auto s){ std::cout << s << "\n"s ; } ; @@ -136,22 +135,23 @@ int main() int a[5] ; ~~~ -のような配列があり、int型が4バイトの環境では、20バイトのストレージが確保され、その先頭の4バイトが最初の0番目の要素に、その次の4バイトが1番目の要素になる。最後の4番目の要素は最後の4バイトになる。 +のような配列があり、`int`型が4バイトの環境では、20バイトのストレージが確保され、その先頭の4バイトが最初の0番目の要素に、その次の4バイトが1番目の要素になる。最後の4番目の要素は最後の4バイトになる。 ~~~ 配列のストレージ上のイメージ図 -□一つが1バイトのストレージ +□1つが1バイトのストレージ 1番目のint |--| □□□□□□□□□□□□□□□□□□□□ |--| |--| 0番目のint 4番目のint ~~~ +fig/fig22-01.png 配列にはメンバー関数がない上、コピーもできない。`std::array`はコピーできる。 -~~~c++ +~~~cpp int main() { int a[5] = {1,2,3,4,5} ; @@ -207,7 +207,6 @@ struct array_int_3 std::begin(storage) ) ; - } } ~~~ @@ -233,4 +232,4 @@ int main() } ~~~ -`std::array`にはまだ様々なメンバーがある。一つずつ順番に学んでいこう。 +`std::array`にはまださまざまなメンバーがある。1つずつ順番に学んでいこう。 diff --git a/023-template.md b/023-template.md index 40d8590..9de10a1 100644 --- a/023-template.md +++ b/023-template.md @@ -2,11 +2,11 @@ ## 問題点 -前回、我々は`'std::array'`のようなものを実装した。C++を何も知らなかった我々がとうとうクールなキッズは皆やっているというクラスを書くことができた。素晴らしい成果だ。 +前章で我々は`'std::array'`のようなものを実装した。C++を何も知らなかった我々がとうとうクールなキッズは皆やっているというクラスを書くことができた。素晴らしい成果だ。 しかし、我々の書いた`'array_int_10'`は`'std::array'`とは異なる。 -~~~c++ +~~~cpp // 標準ライブラリ std::array a ; // 我々のクラス @@ -44,7 +44,7 @@ struct array_double_1 // array_double_2, array_double_3, ... ~~~ -これは怠惰で短気なプログラマーには耐えられない作業だ。C++にはこのような退屈なコードを書かなくてもすむ機能がある。しかしその前に、引数について考えてみよう。 +これは怠惰で短気なプログラマーには耐えられない作業だ。C++にはこのような退屈なコードを書かなくても済む機能がある。しかしその前に、引数について考えてみよう。 ## 関数の引数 @@ -66,7 +66,7 @@ int two_twice() } ~~~ -すばらしい。では3を2倍する関数、4を2倍する関数...と考えていこう。 +素晴らしい。では3を2倍する関数、4を2倍する関数...と考えていこう。 ここまで読んで`three_twice`や`four_twice`を思い浮かべた読者にはプログラマーに備わるべき美徳が欠けている。怠惰で短気で傲慢なプログラマーはそんなコードを書かない。引数を使う。 @@ -81,7 +81,7 @@ int twice( int n ) ## 関数のテンプレート引数 -`twice`を様々な型に対応させるにはどうすればいいだろう。例えば`int`型と`double`型に対応させてみよう。 +`twice`をさまざまな型に対応させるにはどうすればいいだろう。例えば`int`型と`double`型に対応させてみよう。 ~~~cpp int twice( int n ) @@ -95,7 +95,7 @@ double twice( double n ) } ~~~ -整数型には`int`の他にも、`short`, `long`, `long long`といった型がある。浮動小数点数型には`float`と`long double`もある。ということは以下のような関数も必要だ。 +整数型には`int`のほかにも、`short`, `long`, `long long`といった型がある。浮動小数点数型には`float`と`long double`もある。ということは以下のような関数も必要だ。 ~~~cpp short twice( short n ) @@ -168,7 +168,7 @@ bigint twice( bigint n ) そろそろ怠惰と短気を美徳とするプログラマー読者は耐えられなくなってきただろう。これまでのコードは、単にある型`T`に対して、 -~~~c++ +~~~cpp T twice( T n ) { return n * 2 ; @@ -177,7 +177,7 @@ T twice( T n ) と書いているだけだ。型`T`がコピーと`operator *(T, int)`に対応していればいい。型`T`の具体的な型について知る必要はない。 -関数が具体的な値を知らなくても引数によって汎用的なコードをかけるように、具体的な型を知らなくても汎用的なコードを書けるようになりたい。その怠惰と短気に答えるのが`テンプレート`だ。 +関数が具体的な値を知らなくても引数によって汎用的なコードを書けるように、具体的な型を知らなくても汎用的なコードを書けるようになりたい。その怠惰と短気に答えるのが`テンプレート`だ。 ## テンプレート @@ -216,7 +216,7 @@ T f( T n ) } ~~~ -`関数`が`引数`をとるように、`テンプレート`は`テンプレート引数`を取る。 +`関数`が`引数`を取るように、`テンプレート`は`テンプレート引数`を取る。 ~~~cpp // テンプレートはテンプレート引数template_parameterを取る @@ -325,7 +325,7 @@ int main() テンプレートを使ったコードは、与えられた`テンプレート引数`に対して妥当でなければならない。 -~~~c++ +~~~cpp template < typename vec > void f( vec & v ) { @@ -371,7 +371,7 @@ template < typename T > // テンプレート struct S { } ; // クラス ~~~ -関数の中でテンプレート引数名を型や値として使えるように。 +関数の中でテンプレート引数名を型や値として使えるように、 ~~~cpp template < typename T, T N > diff --git a/024-more-array.md b/024-more-array.md index f110f5a..3092145 100644 --- a/024-more-array.md +++ b/024-more-array.md @@ -1,6 +1,6 @@ # arrayをさらに実装 -`'std::array'`をもっと実装していこう。前回、以下のような簡単な`'array'`を実装した。 +`'std::array'`をもっと実装していこう。前章では以下のような簡単な`'array'`を実装した。 ~~~cpp template < typename T, std::size_t N > @@ -19,7 +19,7 @@ struct array ## ネストされた型名 -エイリアス宣言を覚えているだろうか。型名に別名をつける機能だ。 +エイリアス宣言を覚えているだろうか。型名に別名を付ける機能だ。 ~~~cpp int main() @@ -91,11 +91,11 @@ int main() } ~~~ -信じられないことに昔のC++には`auto`がなかったのだ。その他、様々な利点があるのだが、そのすべてを理解するには、まだ読者のC++力が足りない。 +信じられないことに昔のC++には`auto`がなかったのだ。その他、さまざまな利点があるのだが、そのすべてを理解するには、まだ読者のC++力が足りない。 ## 要素数の取得: size() -`std::array`には`size()`というメンバー関数がある。要素数をかえす。 +`std::array`には`size()`というメンバー関数がある。要素数を返す。 `array`の場合、`N`を返せばよい。 @@ -110,7 +110,7 @@ int main() } ~~~ -早速実装しよう。 +さっそく実装しよう。 ~~~cpp template < typename T, std::size_t N > @@ -154,9 +154,9 @@ void S::f() { } ## メンバー関数のconst修飾 -`const`をつけた変数は値を変更できなくなることはすでに学んだ。 +`const`を付けた変数は値を変更できなくなることはすでに学んだ。 -~~~c++ +~~~cpp int main() { int x = 0 ; @@ -189,7 +189,7 @@ int main() 関数`print`がテンプレートなのは任意の`T`と`N`を使った`std::array`を受け取れるようにするためだ。 -関数のリファレンスを引数として渡すと、関数の中で変更できてしまう。しかし、上の例のような関数`print`では、引数を書き換える必要はない。この関数を使う人間も、引数を勝手に書き換えないことを期待している。この場合、`const`をつけることで値の変更を防ぐことができる。 +関数のリファレンスを引数として渡すと、関数の中で変更できてしまう。しかし、上の例のような関数`print`では、引数を書き換える必要はない。この関数を使う人間も、引数を勝手に書き換えないことを期待している。この場合、`const`を付けることで値の変更を防ぐことができる。 ~~~cpp template < typename Container > @@ -204,7 +204,7 @@ void print( Container const & c ) ではさっそくこれまで実装してきた自作の`array`クラスを使ってみよう。 -~~~c++ +~~~cpp int main() { array a = {1,2,3,4,5} ; @@ -219,7 +219,7 @@ int main() クラスのメンバー関数はデータメンバーを変更できる。 -~~~c++ +~~~cpp struct S { int data {} ; @@ -238,7 +238,7 @@ int main() ということは、`const S`はメンバー関数`f()`を呼び出すことができない。 -~~~c++ +~~~cpp int main() { S s ; @@ -268,9 +268,9 @@ int main() } ~~~ -まだエラーになる。この理由を完全に理解するためには、まだ説明していない`ポインター`という機能について学ばなければならない。ポインターの説明はこの次の章で行うとして、今はさしあたり必要な機能である`メンバー関数のconst修飾`を説明する。 +まだエラーになる。この理由を完全に理解するためには、まだ説明していない`ポインター`という機能について学ばなければならない。ポインターの説明はこの次の章で行うとして、いまはさしあたり必要な機能である`メンバー関数のconst修飾`を説明する。 -`const`をつけていないメンバー関数を`const`なクラスのオブジェクトから呼び出せない理由は、メンバー関数がデータメンバーを変更しない保証がないからだ。その保証をつけるのが`メンバー関数のconst修飾`だ。 +`const`を付けていないメンバー関数を`const`なクラスのオブジェクトから呼び出せない理由は、メンバー関数がデータメンバーを変更しない保証がないからだ。その保証を付けるのが`メンバー関数のconst修飾`だ。 メンバー関数は関数の引数のあと、関数の本体の前に`const`を書くことで`const`修飾できる。 @@ -313,7 +313,7 @@ int main() } ~~~ -そしてもう一つ重要なのは、`const`修飾されたメンバー関数がデータメンバーへのリファレンスを返す場合、 +そしてもう1つ重要なのは、`const`修飾されたメンバー関数がデータメンバーへのリファレンスを返す場合、 ~~~cpp struct S @@ -345,7 +345,6 @@ struct S return data ; } } ; - ~~~ 自作の`'array'`の`operator []`を`const`に対応させよう。`'std::array'`は`const`なリファレンスを`const_reference`というネストされた型名にしている。 @@ -391,7 +390,6 @@ int main() `front/back`には`reference`を返すバージョンと`const_reference`を返すバージョンがある。 ~~~cpp - template < typename T, std::size_t N > struct array { @@ -445,7 +443,7 @@ struct array しかし、せっかく`std::fill`があるのだから以下のように書きたい。 -~~~c++ +~~~cpp void fill( T const & u ) { std::fill( begin(), end(), u ) ; diff --git a/025-array-iterator.md b/025-array-iterator.md index fc52faa..54837cb 100644 --- a/025-array-iterator.md +++ b/025-array-iterator.md @@ -2,7 +2,7 @@ ## イテレーターの中身 -自作の`array`をイテレーターに対応させる前に、まず`'std::array'`のイテレーターについて一通り調べよう。 +自作の`array`をイテレーターに対応させる前に、まず`'std::array'`のイテレーターについてひと通り調べよう。 イテレーターは`std::begin/std::end`で取得する @@ -18,7 +18,7 @@ int main() `std::begin/std::end`は何をしているのか見てみよう。 -~~~c++ +~~~cpp namespace std { template < typename C > @@ -41,7 +41,7 @@ namespace std なんと、単に引数に対してメンバー関数`begin/end`を呼び出してその結果を返しているだけだ。 -早速確かめてみよう。 +さっそく確かめてみよう。 ~~~cpp int main() @@ -59,7 +59,7 @@ int main() すると自作の`array`でイテレーターに対応する方法がわかってきた。 -~~~c++ +~~~cpp // イテレーターを表現するクラス struct array_iterator { } @@ -77,7 +77,7 @@ struct array } ; ~~~ -イテレーターに対応するには、おおむねこのような実装になるとみていいだろう。おそらく細かい部分で微調整が必要になるが、今はこれでよしとしよう。ではイテレーターが具体的に何をするかを見ていこう。 +イテレーターに対応するには、おおむねこのような実装になるとみていいだろう。おそらく細かい部分で微調整が必要になるが、いまはこれでよしとしよう。ではイテレーターが具体的に何をするかを見ていこう。 すでに学んだように、イテレーターは`operator *`で参照する要素の値を取得できる。また書き込みもできる。 @@ -95,13 +95,13 @@ int main() 問題を簡単にするために、これまでに作った自作の`array`で最初の要素にアクセスする方法を考えてみよう -~~~c++ +~~~cpp array a = {1,2,3,4,5} ; int x = a[0] ; // 1 a[0] = 0 ; ~~~ -このことから考えると、先頭要素を指すイテレーターは`operator *`をオーバーロードして先頭要素をリファレンスで返せば良い。 +このことから考えると、先頭要素を指すイテレーターは`operator *`をオーバーロードして先頭要素をリファレンスで返せばよい。 ~~~cpp struct array_iterator_int_5_begin @@ -117,9 +117,9 @@ struct array_iterator_int_5_begin しかし、この実装では`array`にしか対応できない。`array`や`array`には対応できない。なぜなら、`array`に渡すテンプレート実引数が違うと、別の型になるからだ。 -`array_iterator`で様々な`array`を扱うにはどうすればいいのか。テンプレートを使う。 +`array_iterator`でさまざまな`array`を扱うにはどうすればいいのか。テンプレートを使う。 -~~~c++ +~~~cpp template < typename Array > struct array_iterator_begin { @@ -137,9 +137,9 @@ struct array_iterator_begin } ; ~~~ -しかしなぜかエラーだとコンパイラーに怒られる。この理由を説明するのはとても難しい。気になる読者は近所のC++グルに教えを乞おう。ここでは答えだけを教える。 +しかしなぜかエラーだとコンパイラーに怒られる。この理由を説明するのはとても難しい。気になる読者は近所のC++グルに教えを請おう。ここでは答えだけを教える。 -`T::Y`において、`T`がテンプレート引数に依存する名前で、`Y`がネストされた型名の場合、`typename`キーワードをつけなければならない。 +`T::Y`において、`T`がテンプレート引数に依存する名前で、`Y`がネストされた型名の場合、`typename`キーワードを付けなければならない。 ~~~cpp template < typename T > @@ -164,9 +164,9 @@ int main() わかっただろうか。わからなくても無理はない。この問題を理解するにはテンプレートに対する深い理解が必要だ。理解した暁には読者はC++グルとして崇拝されているだろう。 -さしあたって必要なのは`Array::reference`の前に`typename`キーワードをつけることだ。 +さしあたって必要なのは`Array::reference`の前に`typename`キーワードを付けることだ。 -~~~c++ +~~~cpp typename Array::reference array_iterator_begin::operator * () { @@ -270,11 +270,11 @@ int main() } ~~~ -クラスの意義は変数と関数を結びつけることだ。このように変数と関数がバラバラではわかりにくいので、メンバー関数という形で`object.set(...)`のようにわかりやすく呼び出せるし、その際クラス`S`のオブジェクトは変数`object`であることが文法上わかるので、わざわざ関数の実引数の形で書くことは省略できるようにしている。 +クラスの意義は変数と関数を結び付けることだ。このように変数と関数がバラバラではわかりにくいので、メンバー関数という形で`object.set(...)`のようにわかりやすく呼び出せるし、その際クラス`S`のオブジェクトは変数`object`であることが文法上わかるので、わざわざ関数の実引数の形で書くことは省略できるようにしている。 メンバー関数の中で、メンバー関数が呼ばれているクラスのオブジェクトを参照する方法が`*this`だ。 -しかしなぜ`*this`なのか。もっとわかりやすいキーワードでもいいのではないか。なぜ`*`がついているのか。この謎を理解するためには、これまたポインターの理解が必要になるが、それは次の章で学ぶ。 +しかしなぜ`*this`なのか。もっとわかりやすいキーワードでもいいのではないか。なぜ`*`が付いているのか。この謎を理解するためには、これまたポインターの理解が必要になるが、それは次の章で学ぶ。 黒魔術3は`iterator(*this)`だ。クラス名に`()`や`{}`を続けると、コンストラクターを呼び出した結果のクラスの値を得ることができる。 @@ -329,7 +329,7 @@ int main() 以上を踏まえて、自作の`array_iterator`の宣言を書いてみよう。 -~~~c++ +~~~cpp template < typename Array > struct array_iterator { @@ -350,7 +350,7 @@ struct array_iterator イテレーターの実装で先頭の要素を参照するのは`a[0]`だった。その次の要素を参照するには`a[1]`だ。その次の要素は`a[2]`となり、その前の要素は`a[1]`だ。 -~~~c++ +~~~cpp array a = {1,2,3,4,5} ; auto iter = a.begin() ; // 最初の要素 @@ -363,7 +363,7 @@ auto iter = a.begin() ; // 最初の要素 では最初の要素の前の要素や、最後の要素の次の要素を参照しようとするとどうなるのか。 -~~~c++ +~~~cpp auto first = a.begin() ; --first ; *first ; // 最初の前の要素? @@ -372,13 +372,13 @@ auto last = a.end() ; *last ; // 最後の次の要素? ~~~ -これはエラーになる。このようなエラーを起こさないように務めるのはユーザーの責任で、イテレーター実装者の責任ではない。しかし、必要であればイテレーターの実装者はこのようなエラーを防ぐような実装もできる。それは後の章で学ぶ。ここでは、こういう場合が起こることは考えなくてもよいとしよう。 +これはエラーになる。このようなエラーを起こさないように務めるのはユーザーの責任で、イテレーター実装者の責任ではない。しかし、必要であればイテレーターの実装者はこのようなエラーを防ぐような実装もできる。それはあとの章で学ぶ。ここでは、こういう場合が起こることは考えなくてもよいとしよう。 これを考えていくと、イテレーターの実装をどうすればいいのかがわかってくる。 `array_iterator`の`operator *`は`a[i]`を返す。 -~~~c++ +~~~cpp typename Array::reference array_iterator::operator *() { return a[i] ; @@ -387,9 +387,9 @@ typename Array::reference array_iterator::operator *() `i`は`std::size_t`型のデータメンバーで、イテレーターが現在参照している`i`番目の要素を記録している。 -ということは先程の`array_iterator`の宣言にはデータメンバー`i`を追加する修正が必要だ。 +ということは先ほどの`array_iterator`の宣言にはデータメンバー`i`を追加する修正が必要だ。 -~~~c++ +~~~cpp template < typename Array > struct array_iterator { @@ -431,11 +431,11 @@ struct array } ; ~~~ -何度も書くように、インデックスは`0`から始まる。要素がN個ある場合、最初の要素は0番目で、最後の要素はN-1番目だ。 +何度も書くように、インデックスは`0`から始まる。要素が$N$個ある場合、最初の要素は0番目で、最後の要素は$N-1$番目だ。 インクリメント演算子`operator ++`にも対応しよう。 -~~~c++ +~~~cpp array_iterator & array_iterator::operator ++() { ++i ; @@ -443,7 +443,7 @@ array_iterator & array_iterator::operator ++() } ~~~ -これで最低限のイテレーターは実装できた。早速試してみよう。 +これで最低限のイテレーターは実装できた。さっそく試してみよう。 ~~~cpp int main() @@ -475,7 +475,7 @@ int main() `int`型では、前置`operator ++`はオペランドの値を1加算した値にする。後置`operator ++`はオペランドの値を1加算するが、式を評価した結果は前のオペランドの値になる。 -~~~c++ +~~~cpp ++i ; // i+1 i++ ; // i、ただしiの値はi+1 ~~~ @@ -507,14 +507,14 @@ struct IntLike まず演算子オーバーロードの宣言だ。 -~~~c++ +~~~cpp // 前置 IntLike & operator ++() ; // 後置 IntLike operator ++(int) ; ~~~ -前置はリファレンスを返す。前置演算子の適用結果は更に変更できるようにするためだ。 +前置はリファレンスを返す。前置演算子の適用結果はさらに変更できるようにするためだ。 ~~~cpp int main() @@ -536,7 +536,7 @@ struct S ただし、その場合`operator ++`に対して通常期待されるコードが書けなくなる。理由がない限り演算子の自然な挙動を目指すべきだ。 -前置と後置は区別できる必要がある。C++はその区別の方法として、`int`型の仮引数をひとつとる`operator ++`を後置演算子だと認識する文法を採用した。この`int`型の実引数は前置と後置を区別するためだけのもので、値に意味はない。 +前置と後置は区別できる必要がある。C++はその区別の方法として、`int`型の仮引数を1つ取る`operator ++`を後置演算子だと認識する文法を採用した。この`int`型の実引数は前置と後置を区別するためだけのもので、値に意味はない。 ~~~cpp struct S @@ -562,7 +562,7 @@ int main() 前置は自然な挙動のためにリファレンスを返すが、後置はリファレンスではなくコピーした値を返す。 -~~~c++ +~~~cpp // 後置 IntLike IntLike::operator ++(int) { @@ -580,7 +580,7 @@ IntLike IntLike::operator ++(int) `++*this`は後置インクリメント演算子が呼ばれたオブジェクトに対して前置インクリメント演算子を使用している。わかりにくければ前置インクリメントと同じ処理を書いてもいい。 -~~~c++ +~~~cpp IntLike IntLike::operator ++(int) { IntLike copy = *this ; @@ -590,11 +590,11 @@ IntLike IntLike::operator ++(int) } ~~~ -`IntLike`のように簡単な処理であればこれでもいいが、もっと複雑な何行もある処理の場合は、すでに実装した前置インクリメントを呼び出したほうが楽だ。コードの重複を省けるのでインクリメントの処理を変更するときに、二箇所に同じ変更をしなくても済む。 +`IntLike`のように簡単な処理であればこれでもいいが、もっと複雑な何行もある処理の場合は、すでに実装した前置インクリメントを呼び出した方が楽だ。コードの重複を省けるのでインクリメントの処理を変更するときに、2箇所に同じ変更をしなくても済む。 以上を踏まえて、`array_iterator`に後置インクリメント演算子を実装しよう。 -~~~c++ +~~~cpp array_iterator array_iterator::operator ++(int) { array_iterator copy = *this ; @@ -605,7 +605,7 @@ array_iterator array_iterator::operator ++(int) デクリメント演算子`operator --`の実装はインクリメント演算子`operator ++`と同じだ。ただ処理がインクリメントではなくデクリメントになっているだけだ。 -~~~c++ +~~~cpp // 前置 array_iterator & array_iterator::operator --() { @@ -621,7 +621,7 @@ array_iterator array_iterator::operator --(int) } ~~~ -ここまでくればイテレーターに必要な操作はあと一つ。比較だ。 +ここまでくればイテレーターに必要な操作はあと1つ。比較だ。 イテレーターは同じ要素を指している場合に等しい。つまり、オペレーター`a`と`b`が同じ要素を指しているならば、`a == b`は`true`で`a != b`は`false`だ。違う要素を指しているならば`a == b`は`false`で`a != b`は`true`だ。 @@ -669,7 +669,7 @@ int main() `array_iterator`の比較は、単にデータメンバー`i`の比較でよい。 -~~~c++ +~~~cpp bool array_iterator::operator ==( array_iterator const & right ) { return i == right.i ; @@ -682,7 +682,7 @@ bool array_iterator::operator !=( array_iterator const & right ) これで自作の`array`と`array_iterator`はアルゴリズムに渡せるようになった。 -~~~c++ +~~~cpp int main() { array a = {1,2,3,4,5} ; @@ -698,7 +698,7 @@ int main() 例えばイテレーター`i`の参照する要素を3つ進めたい場合を考えよう。 -~~~c++ +~~~cpp ++i ; // 1 ++i ; // 2 ++i ; // 3 @@ -706,7 +706,7 @@ int main() これは非効率的だ。もっと効率的なイテレーターの進め方として、`operator +=`がある。 -~~~c++ +~~~cpp i += 3 ; ~~~ @@ -714,7 +714,7 @@ i += 3 ; `operator +`もある -~~~c++ +~~~cpp auto j = i + 3 ; ~~~ @@ -765,7 +765,7 @@ int main() カッコが必要なのは、演算子の評価順序の都合だ。`*i + 3`は`(*i) + 3`であり、`i`の指す要素に対して`+3`される。`*(i+3)`は`i`の指す要素の3つ先の要素の値を読む。 -イテレーターiのn個先の要素を読み書きするのにいちいち`*(i+n)`と書くのは面倒なので、`std::array`や`std::vector`のイテレーターには`operator []`がある。これを使うと`i[n]`と書ける。 +イテレーター`i`の`n`個先の要素を読み書きするのにいちいち`*(i+n)`と書くのは面倒なので、`std::array`や`std::vector`のイテレーターには`operator []`がある。これを使うと`i[n]`と書ける。 ~~~cpp @@ -781,9 +781,9 @@ int main() } ~~~ -`operator []`の実装は文字通り`*(i+n)`と同じことをするだけでよい。 +`operator []`の実装は文字どおり`*(i+n)`と同じことをするだけでよい。 -~~~c++ +~~~cpp template < typename Array > struct array_iterator { @@ -801,11 +801,11 @@ struct array_iterator `*this`というのはこのイテレーターのオブジェクトなので、それに対してすでに実装済みの`operator +`を適用し、その結果に`operator *`を適用している。既存の実装を使わない場合、`return`文は以下のようになる。 -~~~c++ +~~~cpp return a[i+n] ; ~~~ -こちらのほうが一見簡単なように見えるが、`operator +`や`operator *`の実装が複雑な場合、この方法では同じコードを複数の箇所に書かなければならず、コードを修正するときは同じ変更を複数の箇所に行わなければならない。すでに実装したメンバー関数は積極的に使って楽をしていこう。 +こちらの方が一見簡単なように見えるが、`operator +`や`operator *`の実装が複雑な場合、この方法では同じコードを複数の箇所に書かなければならず、コードを修正するときは同じ変更を複数の箇所に行わなければならない。すでに実装したメンバー関数は積極的に使って楽をしていこう。 イテレーターは大小比較ができる。 @@ -853,7 +853,7 @@ struct array_iterator ## constなイテレーター: const_iterator -`std::array`は通常のイテレーターである`std::array::iterator`の他に、`const`なイテレーターである`std::array::const_iterator`を提供している。 +`std::array`は通常のイテレーターである`std::array::iterator`のほかに、`const`なイテレーターである`std::array::const_iterator`を提供している。 ~~~cpp int main() @@ -869,7 +869,7 @@ int main() `const_iterator`は`const iterator`ではない。`const_iterator`とはそれ自体が型名だ。`const`というのは型名を修飾する別の機能だ。 -そのため、`const`の有無の2種類の状態と、`iterator`, `const_iterator`の2つの型をかけ合わせた、以下の型が存在する。 +そのため、`const`の有無の2種類の状態と、`iterator`, `const_iterator`の2つの型を掛け合わせた、以下の型が存在する。 + `iterator` + `const iterator` @@ -896,7 +896,7 @@ int main() `const_iterator`は`iterator`とは別の型だ。自作の`array`に実装するならば以下のようになる。 -~~~c++ +~~~cpp template < typename T, std::size_t N > struct array { @@ -905,7 +905,7 @@ struct array } ; ~~~ -それぞれの型に対して、`const`キーワードをつけた型とそうでない型が存在する。 +それぞれの型に対して、`const`キーワードを付けた型とそうでない型が存在する。 `const_iterator`を得る方法はいくつかある。 @@ -953,7 +953,7 @@ int main() なぜ`const_iterator`が存在するのか。`const iterator`ではだめなのか。その理由は、`const iterator`は値の変更ができないためだ。 -~~~c++ +~~~cpp int main() { using Array = std::array ; @@ -991,7 +991,7 @@ int main() std::cout << *citer ; // エラー - // 要素を変更してる + // 要素を変更している *citer = 0 ; } ~~~ @@ -999,7 +999,7 @@ int main() `const const_iterator`は`const_iterator`の`const`だ。`const const_iterator`は`const iterator`と同じく、イテレーター自体の変更ができない。 -~~~c++ +~~~cpp int main() { using Array = std::array ; @@ -1018,11 +1018,11 @@ int main() } ~~~ -`auto const`もしくは`const auto`を使うと、変数の型を自動で推定してくれるが、`const`がつくようになる。 +`auto const`もしくは`const auto`を使うと、変数の型を自動で推定してくれるが、`const`が付くようになる。 `const_iterator`はどう実装するのか。まず`array`にネストされた型名`const_iterator`を追加する。 -~~~c++ +~~~cpp template < typename T, std::size_t N > struct array { @@ -1089,7 +1089,7 @@ int main() これに対応するために、`const_iterator`のコンストラクターは`iterator`から変換するためのコンストラクターも持つ。 -~~~c++ +~~~cpp template < typename Array > struct array_const_iterator @@ -1107,7 +1107,7 @@ struct array_const_iterator 例えば`operator ++`は完全に同じだ。 -~~~c++ +~~~cpp // iterator版 array_iterator & array_iterator::operator++() { @@ -1122,9 +1122,9 @@ array_const_iterator & array_const_iterator::operator ++() } ~~~ -`operator *`や`operator []`はconstなリファレンスを返す。 +`operator *`や`operator []`は`const`なリファレンスを返す。 -~~~c++ +~~~cpp typename Array::const_reference operator *() const { return a[i] ; @@ -1138,7 +1138,7 @@ typename Array::const_reference operator []( std::size_t i ) const このために、`array`クラスにもネストされた型名`const_reference`を宣言しておく。 -~~~c++ +~~~cpp template < typename T, std::size_t N > struct array { diff --git a/026-exception.md b/026-exception.md index 8dcae3f..457ba91 100644 --- a/026-exception.md +++ b/026-exception.md @@ -2,11 +2,11 @@ ## 例外を投げる -`std::array`の実装方法はほとんど解説した。読者は`std::array`の実装方法を知り、確固たる自信のもとに`std::array`を使えるようになった。ただし、ひとつだけ問題がある。 +`std::array`の実装方法はほとんど解説した。読者は`std::array`の実装方法を知り、確固たる自信の元に`std::array`を使えるようになった。ただし、1つだけ問題がある。 `"std::array"`のユーザーはあらかじめ設定した要素数を超える範囲の要素にアクセスすることができてしまう。 -~~~c++ +~~~cpp int main() { // 妥当な要素はa[0]のみ @@ -21,7 +21,7 @@ int main() `operator []`に範囲外チェックを入れるのは簡単だ。問題は、エラーをユーザーに通知する方法がない。 -~~~c++ +~~~cpp reference array::operator [] ( std::size_t i ) { // 範囲外チェック @@ -39,7 +39,7 @@ reference array::operator [] ( std::size_t i ) `vector`で一番最初に説明した要素アクセスの方法であるメンバー関数`at`を覚えているだろうか。実はメンバー関数`at`はエラーチェックをする。試してみよう。 -~~~c++ +~~~cpp int main() { std::array a = {1} ; @@ -68,9 +68,9 @@ terminate called after throwing an instance of 'std::out_of_range' 例外は通常の処理をすっ飛ばして特別なエラー処理をする機能だ。何もエラー処理をしない場合、プログラムは終了する。例外を発生させることを、「例外を投げる」という。 -例外は文字通り投げるという意味の`throw`キーワードを使い、何らかの値を投げる(throw)。 +例外は文字どおり投げるという意味の`throw`キーワードを使い、何らかの値を投げる(throw)。 -~~~c++ +~~~cpp // int型の値123を投げる throw 123 ; @@ -111,7 +111,7 @@ int main() `std::array`や`std::vector`のメンバー関数`at(n)`は`n`が要素数を超える場合、例外を投げている。 -~~~c++ +~~~cpp array::reference array::at( std::size_t n ) { if ( n >= size() ) @@ -123,7 +123,7 @@ array::reference array::at( std::size_t n ) 投げる例外は、`std::out_of_range`というクラスの値だ。このクラスを完全に説明するのは現時点では難しいが、以下のように振る舞うと考えておこう。 -~~~c++ +~~~cpp namespace std { struct out_of_range @@ -151,10 +151,10 @@ int main() コンストラクターでエラー内容を表現した文字列を受け取り、メンバー関数`what`でエラー内容の文字列を取得する。 -必要な情報は全て学んだ。あとはメンバー関数`at`を実装するだけだ。 +必要な情報はすべて学んだ。あとはメンバー関数`at`を実装するだけだ。 -~~~c++ +~~~cpp array::reference array::at( std::size_t n ) { if ( n >= size() ) @@ -297,7 +297,7 @@ int main() } ~~~ -この例では、関数`main`が関数`h`を呼び出し、その結果として最終的に関数`f`の中で例外が投げられる。投げられた例外は関数呼び出しを巻き戻して関数`main`の中のtryブロックまで到達し、対応する`catch`に捕まる。 +この例では、関数`main`が関数`h`を呼び出し、その結果として最終的に関数`f`の中で例外が投げられる。投げられた例外は関数呼び出しを巻き戻して関数`main`の中の`try`ブロックまで到達し、対応する`catch`に捕まる。 もし関数`main`を抜けてもなお対応する`catch`がない場合はどうなるのか。 @@ -343,7 +343,7 @@ int main() 上のコードは複雑な`tryブロック`のネストが行われている。プログラムがどのように実行されるのかを考えてみよう。 -まず関数`main`が関数`f`を呼び出す。関数`f`は例外を投げる。関数`f`の中の`try`ブロックは対応する`catch`がないので関数`main`に巻き戻る。関数`main`の内側の`try`ブロック、ソースコードでは`// try 2` とコメントをしている`try`ブロックの`catch`には対応しない。更に上の`try`ブロックに巻き戻る。`// try 1`の`tryブロック`の`catch`は`int`型なので、この`catch`に捕まる。 +まず関数`main`が関数`f`を呼び出す。関数`f`は例外を投げる。関数`f`の中の`try`ブロックは対応する`catch`がないので関数`main`に巻き戻る。関数`main`の内側の`try`ブロック、ソースコードでは`// try 2` とコメントをしている`try`ブロックの`catch`には対応しない。さらに上の`try`ブロックに巻き戻る。`// try 1`の`tryブロック`の`catch`は`int`型なので、この`catch`に捕まる。 例外が投げられ、`スタックアンワインディング`による巻き戻しが発生した場合、通常のプログラムの実行は行われない。例えば以下のプログラムは何も出力しない。 @@ -403,7 +403,7 @@ obj is destructed. 例外のスタックアンワインディングでは関数内の変数が破棄される。つまりデストラクターが実行される。 -~~~c++ +~~~cpp void f() { Object obj("f"s) ; @@ -457,7 +457,7 @@ main is destructed. C++20以降では、標準ライブラリに`std::scope_exit`が追加される予定だ。`std::scope_exit`は渡した関数オブジェクトをデストラクターで実行してくれる。 -~~~c++ +~~~cpp int f() { auto ptr = new ; diff --git a/027-pointer.md b/027-pointer.md index 0238f5b..6ab91d9 100644 --- a/027-pointer.md +++ b/027-pointer.md @@ -1,6 +1,6 @@ # ポインター -ポインターは難しいとよく言われる。世の中にはポインターのためにC言語とC++を挫折し、他の軟弱な言語に逃げるプログラマーがいる。ポインターしか解説していない本が出版される。Joel Spolskyがエッセイを書く。 +ポインターは難しいとよく言われる。世の中にはポインターのためにC言語とC++を挫折し、ほかの軟弱な言語に逃げるプログラマーがいる。ポインターしか解説していない本が出版される。Joel Spolskyがエッセイを書く。 ポインターの理解は優秀なプログラマーとなるために必須である。ポインターを理解できない人間は優秀なプログラマーにはなれない。もし、本書を読んでポインターが理解できない場合、プログラマーには向いていないということだ。 diff --git a/028-pointer-semantics.md b/028-pointer-semantics.md index 9b3c4c7..8552679 100644 --- a/028-pointer-semantics.md +++ b/028-pointer-semantics.md @@ -72,30 +72,30 @@ using ptr_type = int * ; リファレンスの初期化は、単に参照したい変数名をそのまま書けばよかった。 -~~~c++ +~~~cpp int object { } ; int & reference = object ; ~~~ -ポインターの場合、参照したい変数名に、`&`をつける必要がある。 +ポインターの場合、参照したい変数名に、`&`を付ける必要がある。 -~~~c++ +~~~cpp int object { } ; int * pointer = &object ; ~~~ リファレンスを経由してリファレンスが参照するオブジェクトを操作するには、単にリファレンス名を使えばよかった。 -~~~c++ +~~~cpp // 書き込み reference = 0 // 読み込み int read = reference ; ~~~ -ポインターの場合、ポインター名に`*`をつける必要がある。 +ポインターの場合、ポインター名に`*`を付ける必要がある。 -~~~c++ +~~~cpp // 書き込み *pointer = 0 ; // 読み込み @@ -120,13 +120,13 @@ int * p2 = p1 ; ### リファレンスと違う機能 -リファレンスがポインターの機能制限版だというのであれば、ポインターにあってリファレンスにはない機能はなんだろうか。代入と、何も参照しない状態だ。 +リファレンスがポインターの機能制限版だというのであれば、ポインターにあってリファレンスにはない機能は何だろうか。代入と、何も参照しない状態だ。 ### 代入 リファレンスは代入ができないが、ポインターは代入ができる。 -~~~c++ +~~~cpp int x { } ; int y { } ; @@ -144,7 +144,7 @@ pointer = &y ; リファレンスは必ず初期化しなければならない。 -~~~c++ +~~~cpp // エラー、初期化されていない int & reference ; ~~~ @@ -153,13 +153,13 @@ int & reference ; ポインターは初期化しなくてもよい。 -~~~c++ +~~~cpp int * pointer ; ~~~ この場合、具体的に何かを参照していない状態になる。この場合にポインターの値はどうなるかはわからない。初期化のない整数の値がわからないのと同じだ。 -~~~c++ +~~~cpp // 値はわからない int data ; ~~~ @@ -219,7 +219,7 @@ double * p1 = nullptr ; std::string p2 = nullptr ; ~~~ -C言語とC++では歴史的な理由で、`nullptr`の他にも`NULL`もnullポインター値 +C言語とC++では歴史的な理由で、`nullptr`のほかにも`NULL`もnullポインター値 ~~~cpp int * pointer = NULL ; @@ -235,7 +235,7 @@ int * pointer = 0 ; ### 無効な参照先の作り方 -ポインターやリファレンスによって参照先が参照される時点では有効だったが、後に無効になる参照先を作ることができてしまう。 +ポインターやリファレンスによって参照先が参照される時点では有効だったが、あとに無効になる参照先を作ることができてしまう。 例えば以下のコードだ。 @@ -258,7 +258,7 @@ int main() このコードの問題は、関数`f`の中の変数`variable`の寿命は関数`f`の中だけで、呼び出し元に戻ったときには寿命が尽きるというところにある。変数`variable`へのポインターは変数`variable`の寿命が尽きたあとも存在してしまうので、存在しないオブジェクトにポインター経由でアクセスしようとしてエラーになる。 -同じ問題はリファレンスでも起きるが、ポインターのほうがこの問題を起こしやすい。 +同じ問題はリファレンスでも起きるが、ポインターの方がこの問題を起こしやすい。 ~~~cpp diff --git a/029-pointer-syntax.md b/029-pointer-syntax.md index 14f235d..da35e9e 100644 --- a/029-pointer-syntax.md +++ b/029-pointer-syntax.md @@ -1,7 +1,7 @@ ## 文法上のポインター -ポインターが難しいと言われる理由の一つに、ポインターの文法が難しい問題がある。 +ポインターが難しいと言われる理由の1つに、ポインターの文法が難しい問題がある。 ### ポインターとconstの関係 @@ -34,7 +34,7 @@ using t3 = const int ; using t4 = int const ; ~~~ -`const int`と`int const`は同じ型だ。この場合、`const`は`int`型のあとにつけても前につけても同じ意味になる。 +`const int`と`int const`は同じ型だ。この場合、`const`は`int`型のあとに付いても前に付いても同じ意味になる。 すると当然の疑問が生じる。組み合わせるとどうなるのかということだ。 @@ -47,7 +47,7 @@ using type = int * & ; リファレンス型へのポインター型はできない。 -~~~c++ +~~~cpp // エラー、できない using error = int & * ; ~~~ @@ -63,9 +63,9 @@ int & ref = data ; int * ptr = &ref ; ~~~ -リファレンスは参照先のオブジェクトと全く同じように振る舞うのでリファレンス自体のポインターの値を得ることはできない。 +リファレンスは参照先のオブジェクトとまったく同じように振る舞うのでリファレンス自体のポインターの値を得ることはできない。 -ポインターのリファレンスを得るのは、ポインター以外の値と全く同じだ。 +ポインターのリファレンスを得るのは、ポインター以外の値とまったく同じだ。 ~~~cpp int * ptr = nullptr ; @@ -118,7 +118,7 @@ using const_T_const_pointer_2 = T const * const ; 順番に見ていこう。まずは組み合わせない型から。 -~~~c++ +~~~cpp using T = int ; // どちらもconstなT using const_T_1 = const T ; @@ -135,7 +135,7 @@ using T_pointer = T * ; 次を見ていこう。 -~~~c++ +~~~cpp // どちらもconstなTへのポインター using const_T_pointer_1 = const T * ; using const_T_pointer_2 = T const * ; @@ -166,7 +166,7 @@ int main() `const`な`int`へのポインターなので、このポインターの参照先を変更することはできない。ポインターは変更できる。 -~~~c++ +~~~cpp int main() { const int x {} ; @@ -202,7 +202,7 @@ int main() この場合、リファレンスやポインターは`const int`扱いなので、リファレンスやポインターを経由して読むことはできるが変更はできない。 -~~~c++ +~~~cpp int main() { int data = 123 ; @@ -216,16 +216,16 @@ int main() } ~~~ -その次はconstなポインターだ。 +その次は`const`なポインターだ。 -~~~c++ +~~~cpp // Tへのconstなポインター using T_const_pointer = T * const ; ~~~ これはポインターが`const`なのであって、`T`は`const`ではない。したがってポインターを経由して参照先を変更することはできるが、ポインターの値自体は変更できない型だ。 -~~~c++ +~~~cpp int main() { int data { } ; @@ -243,7 +243,7 @@ int main() 最後は`const`な`T`への`const`なポインターだ。 -~~~c++ +~~~cpp // どちらもconstなTへのconstなポインター using const_T_const_pointer_1 = const T * const ; using const_T_const_pointer_2 = T const * const ; @@ -313,11 +313,11 @@ int main() } ~~~ -`x`は`int`だ。`p`は`int`へのポインターだ。ここまでは今までどおりだ。 +`x`は`int`だ。`p`は`int`へのポインターだ。ここまではいままでどおりだ。 -`pp`は`int **`という型で、「`int`へのポインターへのポインター」型だ。このポインターの値のためには「`int`へのポインターのポインター」が必要だ。変数pのポインターは`&p`で得られる。この場合、変数`p`は「`int`へのポインター」でなければならない。そうした場合、変数`p`のポインターは「`int`へのポインターのポインター」型の値になる。 +`pp`は`int **`という型で、「`int`へのポインターへのポインター」型だ。このポインターの値のためには「`int`へのポインターのポインター」が必要だ。変数`p`のポインターは`&p`で得られる。この場合、変数`p`は「`int`へのポインター」でなければならない。そうした場合、変数`p`のポインターは「`int`へのポインターのポインター」型の値になる。 -変数`pp`は「`int`へのポインターのポインター」だ。変数`pp`の参照先の変数pを読み書きするには、`*pp`と書く。これはまだ「`int`へのポインター」だ。ここからさらに参照先の`int`型のオブジェクトにアクセスするには、その結果にさらに`*`を書く。結果として`**pp`となる。 +変数`pp`は「`int`へのポインターのポインター」だ。変数`pp`の参照先の変数`p`を読み書きするには、`*pp`と書く。これはまだ「`int`へのポインター」だ。ここからさらに参照先の`int`型のオブジェクトにアクセスするには、その結果にさらに`*`を書く。結果として`**pp`となる。 わかりにくければ変数に代入するとよい。 @@ -372,7 +372,7 @@ using type = int *** ; using pointer_to_type = type * ; ~~~ -もちろん`const`もつけられる。 +もちろん`const`も付けられる。 ~~~cpp using type = int const * const * const * const ; @@ -398,12 +398,12 @@ using g_type = double ( double, double ) ; となる。関数から関数名を取り除いたものが関数の型だ。すると関数へのポインター型は以下のようになる。 -~~~c++ +~~~cpp using f_pointer = f_type * ; using g_pointer = g_type * ; ~~~ -早速試してみよう。 +さっそく試してみよう。 ~~~cpp // 実引数を出力して返す関数 @@ -427,7 +427,7 @@ int main() 動くようだ。最後の関数呼び出しはまず参照先を得て`(*ptr)`、その後に関数呼び出し`(123)`をしている。これは面倒なので、C++では特別に関数へのポインターはそのまま関数呼び出しすることができるようになっている。 -~~~c++ +~~~cpp // 関数へのポインターを経由した関数呼び出し ptr(123) ; ~~~ @@ -447,14 +447,14 @@ int (*ptr)(int) = &f ; ここでは詳細を飛ばして重要な部分だけ伝えるが、型名のうちポインターであることを指定する`*`は、名前にかかる。 -~~~c++ +~~~cpp // この *はnameにかかる int * name ; ~~~ つまり以下のような意味だ。 -~~~c++ +~~~cpp int (*name) ; ~~~ @@ -468,7 +468,7 @@ using type = int * ; つまり以下のような意味だ。 -~~~c++ +~~~cpp using type = int (*) ; ~~~ @@ -478,7 +478,7 @@ using type = int (*) ; int * f( int ){ return nullptr ; } ~~~ -そうではなく、「`int`型の引数をとり`int`型の戻り値を返す関数へのポインター」を書きたい場合は、 +そうではなく、「`int`型の引数を取り`int`型の戻り値を返す関数へのポインター」を書きたい場合は、 ~~~cpp using type = int (*)(int) ; @@ -486,7 +486,7 @@ using type = int (*)(int) ; としなければならない。 -変数の名前を入れる場所は以下の通り +変数の名前を入れる場所は以下のとおり。 ~~~cpp using type = @@ -513,12 +513,12 @@ function_pointer_type ptr = nullptr ; 関数へのポインターは型であり、値でもある。値であるということは、関数は引数として関数へのポインターを受け取ったり、関数へのポインターを返したりできるということだ。 -早速書いてみよう。 +さっそく書いてみよう。 ~~~cpp int f( int x ) { return x ; } using f_ptr = int (*) (int ) ; -// 関数へのポインターを引数にとり +// 関数へのポインターを引数に取り // 関数へのポインターを戻り値として返す // 関数g f_ptr g( f_ptr p ) @@ -543,13 +543,13 @@ auto ptr = &g ; 以下のようになる。 -~~~c++ +~~~cpp int (*(*ptr)(int (*)(int)))(int) = &g ; ~~~ なぜこうなるのか。分解すると以下のようになる。 -~~~c++ +~~~cpp int (* // 戻り値型前半 (*ptr) // 変数名 (// 関数の引数 @@ -563,13 +563,13 @@ int (* // 戻り値型前半 これはわかりにくい。戻り値の型を後ろに書く文法を使うと少し読みやすくなる。 -~~~c++ +~~~cpp auto (*ptr)( int (*)(int) ) -> int (*)(int) = &g ; ~~~ これを分解すると以下のようになる。 -~~~c++ +~~~cpp auto // プレイスホルダー (*ptr) // 変数名 ( int (*)(int) ) // 引数 @@ -577,9 +577,9 @@ auto // プレイスホルダー = &g ; // 初期化子 ~~~ -もちろん、これでもまだわかりにくいので、エイリアス宣言を使ったほうがよい。 +もちろん、これでもまだわかりにくいので、エイリアス宣言を使った方がよい。 -~~~c++ +~~~cpp using func_ptr = int(*)(int) ; auto (*ptr)(func_ptr) -> func_ptr = &g ; @@ -598,7 +598,7 @@ using int5 = int [5] ; using double10 = double [10] ; ~~~ -関数型と同じく、ポインター宣言子である`*`は名前につく。 +関数型と同じく、ポインター宣言子である`*`は名前に付く。 ~~~cpp // 要素型int、要素数5の配列へのポインター型 @@ -637,20 +637,20 @@ int main() ### ポインター型の作り方 -T型へのポインター型は`T *`で作ることができる。 +`T`型へのポインター型は`T *`で作ることができる。 ただし、`T`が`int (int)`のような関数型である場合は、`int (*)(int)`になる。配列型の場合は要素数`N`まで必要で`T (*)[N]`になる。 -エイリアス宣言で型に別名をつけると`T *`でよくなる。 +エイリアス宣言で型に別名を付けると`T *`でよくなる。 ~~~cpp using function_type = int (int) ; using pointer_to_function_type = function_type * ; ~~~ -ポインターの型を書く際に、このようなことをいちいち考えるのは面倒だ。ここで必要のなのは、ある型`T`を受け取ったときに型`T *`を得るような方法だ。ところで、物覚えのいい読者は前にも似たような文章を読んだことに気がつくだろう。そう、テンプレートだ。 +ポインターの型を書く際に、このようなことをいちいち考えるのは面倒だ。ここで必要のなのは、ある型`T`を受け取ったときに型`T *`を得るような方法だ。ところで、物覚えのいい読者は前にも似たような文章を読んだことに気が付くだろう。そう、テンプレートだ。 -テンプレートは型を引数化できる機能だ。今まではクラスや関数にしか使っていなかったが、実はエイリアス宣言にも使えるのだ。 +テンプレートは型を引数化できる機能だ。いままではクラスや関数にしか使っていなかったが、実はエイリアス宣言にも使えるのだ。 ~~~cpp template < typename T > @@ -672,11 +672,11 @@ type> c = {1,2,3,4,5} ; `using type = int ;`というエイリアス宣言があるとき`type`の型は`int`だ。エイリアス宣言は新しい`type`という型を作るわけではない。 -同様に、上のエイリアステンプレートtypeによる`type`の型は`int`だ。新しい`type`という型ができるわけではない。 +同様に、上のエイリアステンプレート`type`による`type`の型は`int`だ。新しい`type`という型ができるわけではない。 もう少し複雑な使い方もしてみよう。 -~~~c++ +~~~cpp // int type> a = 0 ; // int @@ -686,16 +686,16 @@ type>> b = 0 ; `type`の型は`int`なので、それを引数に渡した`type< type >`も`int`だ。`type`をいくつネストしようとも`int`になる。 -~~~c++ +~~~cpp // std::vector std::vector< type > a = {1,2,3,4,5} ; // std::vector type>> b = {1,2,3,4,5} ; ~~~ -`type`は`int`なので、`std::vector>`は`std::vector`になる。それを更に`type`で囲んでも同じ型だ。 +`type`は`int`なので、`std::vector>`は`std::vector`になる。それをさらに`type`で囲んでも同じ型だ。 -`type`は面白いがなんの役に立つのだろうか。`type`は型として使える。つまり`type *`はポインターとして機能するのだ。 +`type`は面白いが何の役に立つのだろうか。`type`は型として使える。つまり`type *`はポインターとして機能するのだ。 ~~~cpp template < typename T > using type = T ; @@ -732,7 +732,7 @@ add_pointer_t d = nullptr ; どうやら動くようだ。もっと複雑な例も試してみよう。 -~~~c++ +~~~cpp // int ** add_pointer_t> a = nullptr ; ~~~ @@ -755,9 +755,9 @@ add_pointer_t ptr = nullptr ; std::add_pointer_t ptr = nullptr ; ~~~ -標準ライブラリ`std::add_pointer_t`は、`T`がリファレンス型の場合、リファレンスは剥がしてポインターを付与するという実装になっている。これをどうやって実装するかについてだが、まだ読者の知識では実装できない。テンプレートについて深く学ぶ必要がある。今は標準ライブラリに頼っておこう。 +標準ライブラリ`std::add_pointer_t`は、`T`がリファレンス型の場合、リファレンスは剝がしてポインターを付与するという実装になっている。これをどうやって実装するかについてだが、まだ読者の知識では実装できない。テンプレートについて深く学ぶ必要がある。いまは標準ライブラリに頼っておこう。 -標準ライブラリには他にも、ポインターを取り除く`std::remove_pointer_t`もある。 +標準ライブラリにはほかにも、ポインターを取り除く`std::remove_pointer_t`もある。 ~~~cpp // int @@ -772,7 +772,7 @@ std::remove_pointer_t< ### クラスへのポインター -クラスへのポインターは今までに学んだものと同じ文法だ。 +クラスへのポインターはいままでに学んだものと同じ文法だ。 ~~~cpp struct C { } ; @@ -809,7 +809,7 @@ int main() 以下のように書くとエラーだ。 -~~~c++ +~~~cpp int main() { C object ; @@ -824,12 +824,12 @@ int main() この理由は演算子の優先順位の問題だ。上の式は以下のように解釈される。 -~~~c++ +~~~cpp *(pointer.data_member) = 0 ; *(pointer.member_function()) ; ~~~ -ポインターを参照する演算子`*`よりも、演算子ドット(`'.'`)のほうが演算子の優先順位が高い。 +ポインターを参照する演算子`*`よりも、演算子ドット(`'.'`)の方が演算子の優先順位が高い。 このような式を可能にする変数`pointer`とは以下のようなものだ。 @@ -860,14 +860,14 @@ int main() 演算子`*`を先にポインターの値である`pointer`に適用するには、括弧を使う。 -~~~c++ +~~~cpp (*pointer).data_member = 0 ; (*pointer).member_function() ; ~~~ リファレンスを使ってポインターを参照した結果をリファレンスに束縛して使うこともできる。 -~~~c++ +~~~cpp C & ref = *pointer ; ref.data_member = 0 ; ref.member_function() ; @@ -875,14 +875,14 @@ ref.member_function() ; ただし、ポインターを介してクラスを扱う際に、毎回括弧を使ったりリファレンスを使ったりするのは面倒なので、簡単なシンタックスシュガーとして演算子`->`が用意されている。 -~~~c++ +~~~cpp pointer->data_member = 0 ; pointer->member_function() ; ~~~ `a->b`は、`(*(a))->b`と同じ意味になる。そのため、上は以下のコードと同じ意味になる。 -~~~c++ +~~~cpp (*(pointer)).data_member = 0 ; (*(pointer)).member_function() ; @@ -915,9 +915,9 @@ int main() } ~~~ -すでに説明したように、メンバー関数が自分を呼びだしたクラスのオブジェクトのサブオブジェクトを参照できるのは、クラスのオブジェクトへの参照を知っているからだ。内部的には以下のような隠し引数を持つコードが生成されたかのような挙動になる。 +すでに説明したように、メンバー関数が自分を呼び出したクラスのオブジェクトのサブオブジェクトを参照できるのは、クラスのオブジェクトへの参照を知っているからだ。内部的には以下のような隠し引数を持つコードが生成されたかのような挙動になる。 -~~~c++ +~~~cpp // コンパイラーが生成するコードのたとえ struct C { @@ -949,9 +949,9 @@ struct C } ; ~~~ -さきほど、関数`C::set`の中で`data = n ;`と書いたのは、`this->data = n ;`と書いたのと同じ意味になる。 +先ほど、関数`C::set`の中で`data = n ;`と書いたのは、`this->data = n ;`と書いたのと同じ意味になる。 -`this`はリファレンスではなくてポインターだ。この理由は歴史的なものだ。本来ならばリファレンスのほうがよいのだが、今更変更できないのでポインターになっている。わかりにくければリファレンスに束縛してもよい。 +`this`はリファレンスではなくてポインターだ。この理由は歴史的なものだ。本来ならばリファレンスの方がよいのだが、いまさら変更できないのでポインターになっている。わかりにくければリファレンスに束縛してもよい。 ~~~cpp struct S @@ -984,7 +984,7 @@ struct S この理由は、`const`なメンバー関数はクラスのオブジェクトへの参照として`const`なリファレンスを隠し引数として持つからだ。 -~~~c++ +~~~cpp // コンパイラーが生成するコードのたとえ struct S { } ; @@ -1060,7 +1060,7 @@ int main() 細かい文法はあとで学ぶとして、肝心の機能としてはこうだ。クラスのオブジェクトからは独立したデータメンバーやメンバー関数自体へのポインターを取得する。 -~~~c++ +~~~cpp struct Object { int data_member ; @@ -1072,7 +1072,7 @@ int Object::*int_ptr = &Object::data_member ; このポインターをクラスのオブジェクトと組み合わせることで、ポインターが参照するクラスのメンバーで、かつオブジェクトのサブオブジェクトの部分を参照できる。 -~~~c++ +~~~cpp Object object ; // メンバーへのポインターをオブジェクトに適用してサブオブジェクトを参照する @@ -1114,7 +1114,7 @@ struct DEF 順を追って説明していこう。まずクラス`ABC`のメンバー、 -~~~c++ +~~~cpp // int ABC::* int x ; // int ABC::* @@ -1123,7 +1123,7 @@ int y ; このメンバーへのポインターの型はどちらも`int ABC::*`になる。データメンバーの型は`int`で、クラス名が`ABC`なので、`型名 クラス名::*`に当てはめると`int ABC::*`になる。 -~~~c++ +~~~cpp // double ABC::* double d ; ~~~ @@ -1132,7 +1132,7 @@ double d ; 最後のクラス`ABC`のメンバー、 -~~~c++ +~~~cpp // int * ABC::* int * ptr ; ~~~ @@ -1171,14 +1171,14 @@ int main() } ~~~ -分かりづらければエイリアス宣言を使うとよい。 +わかりづらければエイリアス宣言を使うとよい。 ~~~cpp using type = int C::* ; type x_ptr = &C::x ; ~~~ -あるいは`auto`使うという手もある。 +あるいは`auto`を使うという手もある。 ~~~cpp // int C::* @@ -1187,7 +1187,7 @@ auto x_ptr = &C::x ; メンバー関数へのポインターは、メンバーへのポインターと関数へのポインターを組み合わせた複雑な文法となるので、とてもわかりづらい。 -復習すると、`int`型の引数を一つ受け取り`int`型の戻り値を返す関数へのポインターの型は`int (*)(int)`だ。 +復習すると、`int`型の引数を1つ受け取り`int`型の戻り値を返す関数へのポインターの型は`int (*)(int)`だ。 ~~~cpp int f(int) { return 0 ; } @@ -1225,30 +1225,30 @@ int main() メンバー関数へのポインターは難しい。 -関数`f`の型は`int (int)`で、そのポインターの型は`int (*)(int)`だ。するとクラス名Cのメンバー関数`f`へのポインターの型は、`int (C::*)(int)`になる。 +関数`f`の型は`int (int)`で、そのポインターの型は`int (*)(int)`だ。するとクラス名`C`のメンバー関数`f`へのポインターの型は、`int (C::*)(int)`になる。 メンバー関数へのポインター型の変数を宣言してその値を`C::f`へのポインターに初期化しているのが以下の行だ。 -~~~c++ +~~~cpp // メンバー関数へのポインター int (C::*ptr)(int) = &C::f ; ~~~ -この`ptr`を経由したメンバー関数fの呼び出し方だが、まずクラスのオブジェクトが必要になるので作る。 +この`ptr`を経由したメンバー関数`f`の呼び出し方だが、まずクラスのオブジェクトが必要になるので作る。 -~~~c++ +~~~cpp C object ; ~~~ そして演算子の`operator .*`を使う。 -~~~c++ +~~~cpp (object.*ptr)(123) ; ~~~ `object.*ptr`を括弧で囲んでいるのは、演算子の優先順位のためだ。もしこれを以下のように書くと、 -~~~c++ +~~~cpp object.*ptr(123) ~~~ @@ -1269,7 +1269,7 @@ int main() 演算子の優先順位の問題のために、`(object.*ptr)`と括弧で包んで先に評価させ、その後に関数呼び出し式である`(123)`を評価させる。 -実は演算子`operator .*`の他に、`operator ->*`という演算子がある。 +実は演算子`operator .*`のほかに、`operator ->*`という演算子がある。 `.*`はクラスのオブジェクトがリファレンスの場合の演算子だが、`->*`はクラスのオブジェクトがポインターの場合の演算子だ。 @@ -1291,13 +1291,13 @@ int main() 上の例で、 -~~~c++ +~~~cpp c_ptr->*object = 123 ; ~~~ は、以下と同じだ。 -~~~c++ +~~~cpp (*(c_ptr)).*object = 123 ; ~~~ @@ -1305,7 +1305,7 @@ c_ptr->*object = 123 ; `f`がデータメンバーへのポインターで、`t1`がクラスのオブジェクトの場合、`std::invoke(f, t1)`は以下のような関数になる。 -~~~c++ +~~~cpp template < typename F, typename T1 > 適切な戻り値の型 std::invoke( F f, T1 t1 ) { @@ -1316,7 +1316,7 @@ template < typename F, typename T1 > なので以下のように書ける。 -~~~c++ +~~~cpp struct C { int data { } ; } ; int main() @@ -1333,7 +1333,7 @@ int main() 便利なことに`t1`がポインターの場合は、 -~~~c++ +~~~cpp template < typename F, typename T1 > 適切な戻り値の型 std::invoke( F f, T1 t1 ) { @@ -1343,7 +1343,7 @@ template < typename F, typename T1 > という関数として振る舞う。そのため、リファレンスでもポインターでも気にせずに使うことができる。 -~~~c++ +~~~cpp C * c_ptr = &object ; // どちらも同じ意味 @@ -1352,7 +1352,7 @@ std::invoke( data_ptr, c_ptr ) = 123 ; ~~~ -`std::invoke`が更に凄いことに、メンバー関数へのポインターにも対応している。 +`std::invoke`がさらにすごいことに、メンバー関数へのポインターにも対応している。 `std::invoke( f, t1, ... )`で、`f`がメンバー関数へのポインターで、`t1`がクラスのオブジェクトへのリファレンスで、`...`が関数呼び出しの際の引数の場合、以下のような関数として振る舞う。 diff --git a/030-pointer-details.md b/030-pointer-details.md index 8d318cc..631d7b9 100644 --- a/030-pointer-details.md +++ b/030-pointer-details.md @@ -4,19 +4,19 @@ ### キロバイトとキビバイト -メモリとアドレスについて解説する前に、キロバイト(Kilo byte)とキビバイト(Kibi byte)の違いについて解説する。 +メモリーとアドレスについて解説する前に、キロバイト(Kilo byte)とキビバイト(Kibi byte)の違いについて解説する。 -キロ(Kilo)というのはSI接頭語で、$1000^1$を意味する。1キロは1000だ。SI接頭語には他にもメガ(Mega, $1000^2$)、ギガ(Giga, $1000^3$)やテラ(Tera, $1000^4$)などの接頭語もある。 +キロ(Kilo)というのはSI接頭語で、$1000^1$を意味する。1キロは1000だ。SI接頭語にはほかにもメガ(Mega, $1000^2$)、ギガ(Giga, $1000^3$)やテラ(Tera, $1000^4$)などの接頭語もある。 長さ1キロメートルは1000メートルで、重さ1キログラムは1000グラムだ。 -今「このCPUのクロック周波数は1GHzだ」と言ったとき、それは$1000^3 Hz = 1000000000 Hz$のことだ。 +いま「このCPUのクロック周波数は1GHzだ」と言ったとき、それは$1000^3$Hz = $1000000000$Hzのことだ。 -しかし、メモリ容量だけは慣習的に$1000^n$ではなく、$1024^n$を使う。 +しかし、メモリー容量だけは慣習的に$1000^n$ではなく、$1024^n$を使う。 -一般人が「このメモリは1KBだ」と言ったとき、それは1024バイトのことだ。1GBのメモリは$1024^3 バイト = 1073741824 バイト$だ。筆者が本書を執筆するのに使ったラップトップコンピューターは32GBのメモリを積んでいるがこれは34359738368バイトだ。 +一般人が「このメモリーは1KBだ」と言ったとき、それは1024バイトのことだ。1GBのメモリーは$1024^3 バイト = 1073741824 バイト$だ。筆者が本書を執筆するのに使ったラップトップコンピューターは32GBのメモリーを積んでいるがこれは34359738368バイトだ。 -メモリの容量が10進数ではなく2進数で数えられているのは、メモリは2進数で扱うのがハードウェア的に都合がいいからだ。そのため、慣習的にキロは$1000^1$ではなく$1024^1$を意味するようになってしまった。 +メモリーの容量が10進数ではなく2進数で数えられているのは、メモリーは2進数で扱うのがハードウェア的に都合がいいからだ。そのため、慣習的にキロは$1000^1$ではなく$1024^1$を意味するようになってしまった。 このため、IEEE 1541では10進SI接頭語と対になる2進接頭語を定義した。 @@ -31,32 +31,32 @@ 本書では1KBは1000バイトで、1KiBが1024バイトを意味する。 -### メモリとアドレス +### メモリーとアドレス -コンピューターにはメモリーやストレージと呼ばれる記憶領域がある。情報の最小単位はすでに学んだようにビットだが、情報をビット単位で扱うのは不便なので、慣習的に複数の連続したビットを束ねたバイトという単位で扱っている。1バイトはほとんどのアーキテクチャで8ビットだ。メモリは複数の連続したバイト列で成り立っている。 +コンピューターにはメモリーやストレージと呼ばれる記憶領域がある。情報の最小単位はすでに学んだようにビットだが、情報をビット単位で扱うのは不便なので、慣習的に複数の連続したビットを束ねたバイトという単位で扱っている。1バイトはほとんどのアーキテクチャで8ビットだ。メモリーは複数の連続したバイト列で成り立っている。 -この連続したバイト列の中の任意の1バイトを指し示すのがアドレスだ。メモリのバイト列の最初の1バイトのアドレスを0とし、次の1バイトアドレスを1とし、以降、その次を前のアドレスに1加えた値にしてみよう。 +この連続したバイト列の中の任意の1バイトを指し示すのがアドレスだ。メモリーのバイト列の最初の1バイトのアドレスを0とし、次の1バイトアドレスを1とし、以降、その次を前のアドレスに1加えた値にしてみよう。 -そのようなメモリーとアドレスのコンピューターでは、1バイトの符号なし整数で表現されたアドレスは、256バイトのメモリの中の任意の1バイトをアドレスとして参照することができる。 +そのようなメモリーとアドレスのコンピューターでは、1バイトの符号なし整数で表現されたアドレスは、256バイトのメモリーの中の任意の1バイトをアドレスとして参照することができる。 -これはとても抽象化された計算機で、現実の計算機はもっと複雑な実装になっている。しかしC++の規格としては、メモリとはフラットな連続したバイト列であって、その任意の各バイトをアドレスから参照可能だという想定になっている。 +これはとても抽象化された計算機で、現実の計算機はもっと複雑な実装になっている。しかしC++の規格としては、メモリーとはフラットな連続したバイト列であって、その任意の各バイトをアドレスから参照可能だという想定になっている。 -アドレスが1バイトの符号なし整数で表現され、そのすべてのビットが使われる場合、256バイトの連続したメモリをアドレス可能だ。 +アドレスが1バイトの符号なし整数で表現され、そのすべてのビットが使われる場合、256バイトの連続したメモリーをアドレス可能だ。 -アドレスが2バイトならば、64KiBのメモリをアドレス可能だ。 +アドレスが2バイトならば、64KiBのメモリーをアドレス可能だ。 -アドレスが4バイトならば、4GiBのメモリをアドレス可能だ。 +アドレスが4バイトならば、4GiBのメモリーをアドレス可能だ。 -アドレスが8バイトならば、16EiBのメモリをアドレス可能だ。 +アドレスが8バイトならば、16EiBのメモリーをアドレス可能だ。 ポインターの値というのは、このアドレスの値のことだ。 ### ポインターのサイズ -ポインターの値というのはアドレスの値だ。ポインターの値を格納するのにもメモリが必要だ。ではポインターのサイズは何バイトあるのだろう。 +ポインターの値というのはアドレスの値だ。ポインターの値を格納するのにもメモリーが必要だ。ではポインターのサイズは何バイトあるのだろう。 -型Tのサイズを調べるには`sizeof(T)`を使う。 +型`T`のサイズを調べるには`sizeof(T)`を使う。 ~~~cpp template @@ -102,7 +102,7 @@ int main() ポインターも`std::uintptr_t`も8バイトだ。ポインターのバイト列を`std::uintptr_t`として強引に解釈すれば、符号なし整数としての値を出力してみよう。 -ある値`from`のバイト列を、同じバイト数のある型toの値として強引に解釈するC++20で追加された標準ライブラリに、`std::bit_cast(from)`がある。 +ある値`from`のバイト列を、同じバイト数のある型`to`の値として強引に解釈するC++20で追加された標準ライブラリに、`std::bit_cast(from)`がある。 ~~~cpp #include @@ -150,7 +150,7 @@ int main() `std::byte`というのは`sizeof(std::byte)`の結果が1になる、サイズが1バイトの符号なし整数型だ。 -`std::byte`はC++で1バイトの生の値を表現するために使うことができる。配列は連続したバイト列なので、4バイトの`int`型は、本質的には上のようなコードになる。ただし上のコードはアライメントという概念が欠けている。これについては後で説明する。 +`std::byte`はC++で1バイトの生の値を表現するために使うことができる。配列は連続したバイト列なので、4バイトの`int`型は、本質的には上のようなコードになる。ただし上のコードはアライメントという概念が欠けている。これについてはあとで説明する。 ところで、`std::bit_cast`は2020年に制定される国際標準規格C++20から入った。しかるに筆者がこの文章を書いているのは2018年だ。まだC++20を完全に実装したC++コンパイラーは存在しない。この本が出版されてしばらくは、読者の手元にもC++20コンパイラーは存在しないだろう。 @@ -160,7 +160,7 @@ int main() 今回実装する`bit_cast`は以下のような関数テンプレートだ。 -~~~c++ +~~~cpp template < typename To, typename From > To bit_cast( From const & from ) { @@ -168,7 +168,7 @@ To bit_cast( From const & from ) } ~~~ -`bit_cast`の実装にはポインターが必要だ。`From`の値を表現するバイト列への先頭のポインターをとり、バイト単位で`To`の値を表現するバイト列にコピーすればよい。 +`bit_cast`の実装にはポインターが必要だ。`From`の値を表現するバイト列への先頭のポインターを取り、バイト単位で`To`の値を表現するバイト列にコピーすればよい。 標準ライブラリにはそのような処理を行ってくれる`std::memcpy(dest, src, n)`がある。ポインター`src`から`n`バイトをポインター`dest`から`n`バイトに書き込む関数だ。 @@ -186,13 +186,13 @@ To bit_cast( From const & from ) ### `std::memcpy`の実装 -`std::memcpy`はC++コンパイラーによって効率の良いコードに置き換えられる。そのため自分で実装した`std::memcpy`を標準ライブラリと同じ効率にすることは難しいが、機能的にはほとんど同じものを作ることができる。 +`std::memcpy`はC++コンパイラーによって効率のよいコードに置き換えられる。そのため自分で実装した`std::memcpy`を標準ライブラリと同じ効率にすることは難しいが、機能的にはほとんど同じものを作ることができる。 `memcpy`の実装にはポインターの詳細な理解が必要だ。 `std::memcpy`関数は以下のようになっている。 -~~~c++ +~~~cpp void * memcpy( void * dest, void const * src, std::size_t n ) { // srcの先頭バイトからnバイトを @@ -201,7 +201,7 @@ void * memcpy( void * dest, void const * src, std::size_t n ) } ~~~ -みなれない`void *`という型が出てきた。まずはこれについて学ぼう。 +見慣れない`void *`という型が出てきた。まずはこれについて学ぼう。 #### void型 @@ -228,18 +228,18 @@ void f() C++17では、`void`型の変数は作れない。 -~~~c++ +~~~cpp // エラー void x ; ~~~ -ところで、読者が本書を読む頃には、C++規格では`void`型の変数が作れるようになっているかもしれない。これは`void`型だけ変数を作れないのが面倒だからつくれるようになるだけで、具体的な値のない変数になる。 +ところで、読者が本書を読むころには、C++規格では`void`型の変数が作れるようになっているかもしれない。これは`void`型だけ変数を作れないのが面倒だから作れるようになるだけで、具体的な値のない変数になる。 #### void *型 `void *`型は「`void`型へのポインター型」だ。`int *`が「`int`型へのポインター型」であるのと同じだ。 -`void *`型の値は、ある型`T`へのポインター型から型`T`という情報が消え去ったポインターの値だ。ポインターの値というのはアドレスで、アドレスというのは単なるバイト単位のメモリを指す整数値だということを学んだ。`void *`型は特定の型を意味しないポインター型だ。 +`void *`型の値は、ある型`T`へのポインター型から型`T`という情報が消え去ったポインターの値だ。ポインターの値というのはアドレスで、アドレスというのは単なるバイト単位のメモリーを指す整数値だということを学んだ。`void *`型は特定の型を意味しないポインター型だ。 ある型`T`へのポインター型の値は、`void *`型に変換できる。 @@ -283,7 +283,7 @@ int main() `memcpy`は`void *`を使うことで、どんなポインターの値でも取れるようにしている。C++にはテンプレートがあるので以下のように宣言してもよいのだが、 -~~~c++ +~~~cpp template < typename Dest, typename Src > Dest * memcpy( Dest * dest, Src const * src, std::size_t n ) ; ~~~ @@ -300,7 +300,7 @@ Dest * memcpy( Dest * dest, Src const * src, std::size_t n ) ; `std::byte`はとても厳格に1バイトの符号なし整数として振る舞うので、普通の整数で初期化や代入をすることができない。 -~~~c++ +~~~cpp // エラー std::byte a = 123 ; std::byte b(123) ; @@ -330,18 +330,18 @@ std::byte a = static_cast(123) ; std::byte b = std::byte(123) ; ~~~ -何故使ってはならないかというと、範囲外の値を無理やり変換してしまうからだ。 +なぜ使ってはならないかというと、範囲外の値を無理やり変換してしまうからだ。 ~~~cpp std::byte a = static_cast(256) ; std::byte b = std::byte(-1) ; ~~~ -#### 配列のメモリ上での表現 +#### 配列のメモリー上での表現 -配列は要素型を表現するバイト列をメモリ上に連続して配置する。 +配列は要素型を表現するバイト列をメモリー上に連続して配置する。 -例えば`int [3]`という配列があり、`sizeof(int)`が`4`の場合、全体で12バイトのメモリが確保される。 +例えば`int [3]`という配列があり、`sizeof(int)`が`4`の場合、全体で12バイトのメモリーが確保される。 ~~~cpp int data[3] = {1,2,3} ; @@ -351,11 +351,11 @@ int data[3] = {1,2,3} ; 次の4バイト(4バイト目から7バイト目まで)の領域は1番目の要素である`data[1]`で、その値は`2`だ。 -最後の4バイト(8バイト目から11バイト目まで)の領域は2番めの要素である`data[2]`で、その値は`3`だ。 +最後の4バイト(8バイト目から11バイト目まで)の領域は2番目の要素である`data[2]`で、その値は`3`だ。 ~~~ -TODO: メモリの図示 +TODO: メモリーの図示 ↓最初の4バイト <-----> @@ -365,6 +365,7 @@ TODO: メモリの図示 <-----> ↑最後の4バイト ~~~ +fig/fig30-01.png 実際にアドレスの生の値を出力して確かめてみよう。 @@ -538,7 +539,7 @@ int main() } ~~~ -配列が要素型のバイト列を連続して配置したメモリレイアウトをしているように、クラスもデータメンバーを連続して配置したメモリーレイアウトをしている。 +配列が要素型のバイト列を連続して配置したメモリーレイアウトをしているように、クラスもデータメンバーを連続して配置したメモリーレイアウトをしている。 たとえば以下のようなクラス`Object`がある場合、 diff --git a/031-iterator-operations.md b/031-iterator-operations.md index 6b0f39b..8ec4915 100644 --- a/031-iterator-operations.md +++ b/031-iterator-operations.md @@ -4,7 +4,7 @@ `array`のイテレーターの実装を振り返ろう。前回実装したイテレーターは、リファレンスとインデックスを使うものだった。 -~~~c++ +~~~cpp template < Array > struct array_iterator { @@ -101,6 +101,7 @@ TODO: 図示 ランダムアクセスイテレーター → 双方向イテレーター → 前方イテレーター → 入力イテレーター → 出力イテレーター ~~~ +fig/fig31-01.png 矢印`A → B`は`A`が`B`であることを意味している。 @@ -112,7 +113,7 @@ TODO: 図示 ### ランダムアクセスイテレーター -ランダムアクセスイテレーターは名前の通りランダムアクセスができる。イテレーターが`n`番目の要素を指すとき、`n+m`番目の要素を指すことができる。`m`は負数でもよい。 +ランダムアクセスイテレーターは名前のとおりランダムアクセスができる。イテレーターが`n`番目の要素を指すとき、`n+m`番目の要素を指すことができる。`m`は負数でもよい。 ~~~cpp template < typename RandomAccessIterator > @@ -136,7 +137,7 @@ void f( RandomAccessIterator i, int n ) イテレーター間の距離を計算したいときはイテレーター同士を引き算する。 -~~~c++ +~~~cpp template < typename RandomAccessIterator > void f( RandomAccessIterator a, RandomAccessIterator b ) { @@ -147,7 +148,7 @@ void f( RandomAccessIterator a, RandomAccessIterator b ) イテレーター間の距離は負数にもなる。 -~~~c++ +~~~cpp template < typename RandomAccessIterator > void f( RandomAccessIterator a ) { @@ -159,7 +160,7 @@ void f( RandomAccessIterator a ) } ~~~ -イテレーター`b`は`a`より3進んでいるので、`a`から`b`までの距離である`b - a`は3になる。では`b`から`a`までの距離である`a - b`はどうなるかというと、-3になる。`b`にとって`a`は3戻っているからだ。 +イテレーター`b`は`a`より3進んでいるので、`a`から`b`までの距離である`b - a`は3になる。では`b`から`a`までの距離である`a - b`はどうなるかというと、$-3$になる。`b`にとって`a`は3戻っているからだ。 イテレーター `i`の`n`個先の要素を参照したい場合は、 @@ -231,9 +232,9 @@ void f( Iterator i ) ### 双方向イテレーター -双方向イテレーターは名前の通り双方向のイテレーターの移動ができる。双方向というのはイテレーターが参照している`n`番目の要素の`n-1`番目の要素と`n+1`番目の要素だ。 +双方向イテレーターは名前のとおり双方向のイテレーターの移動ができる。双方向というのはイテレーターが参照している`n`番目の要素の`n-1`番目の要素と`n+1`番目の要素だ。 -~~~c++ +~~~cpp template < typename BidirectionalIterator > void f( BidirectionalIterator i ) { @@ -262,7 +263,7 @@ nth_next( BidirectionalIterator iter, std::size_t n ) } ~~~ -たしかにこれはできる。できるが、効率的ではない。双方向イテレーターが提供される場合というのは、ランダムアクセスが技術的に可能ではあるが非効率的な場合だ。具体的なデータ構造を出すと、例えばリンクリストがある。リンクリストに対するランダムアクセスは技術的に可能であるが非効率的だ。 +確かにこれはできる。できるが、効率的ではない。双方向イテレーターが提供される場合というのは、ランダムアクセスが技術的に可能ではあるが非効率的な場合だ。具体的なデータ構造を出すと、例えばリンクリストがある。リンクリストに対するランダムアクセスは技術的に可能であるが非効率的だ。 ### 前方イテレーター @@ -276,7 +277,7 @@ void f( ForwardIterator i ) } ~~~ -前方イテレーターにはマルチパス保証がある。イテレーターの指す要素を動かす前のイテレーターの値を保持しておき、保持した値を動かしたとき、ふたつのイテレーターは同一になるという保証だ。 +前方イテレーターにはマルチパス保証がある。イテレーターの指す要素を動かす前のイテレーターの値を保持しておき、保持した値を動かしたとき、2つのイテレーターは同一になるという保証だ。 ~~~cpp template < typename ForwardIterator > @@ -326,7 +327,7 @@ void f( InputIterator i, InputIterator j ) 入力イテレーターの参照は、読み込みことしか保証されていない。 -~~~c++ +~~~cpp template < typename InputIterator > void f( InputIterator i ) { @@ -416,7 +417,7 @@ void f( Iterator i ) `iterator_category`はイテレーターカテゴリーを示す型で、以下のようになっている。 -~~~c++ +~~~cpp namespace std { struct input_iterator_tag { }; struct output_iterator_tag { }; @@ -426,7 +427,7 @@ struct random_access_iterator_tag: public bidirectional_iterator_tag { }; } ~~~ -`forward_iterator_tag`以降のコロン文字の後に続くコードについては、今は気にしなくてもよい。これは派生というまだ説明していないクラスの機能だ。 +`forward_iterator_tag`以降のコロン文字のあとに続くコードについては、いまは気にしなくてもよい。これは派生というまだ説明していないクラスの機能だ。 あるイテレーターがあるイテレーターカテゴリーを満たすかどうかを調べるには以下のようにする。 @@ -512,9 +513,9 @@ int main() } ~~~ -`cout_iterator`は`*i = x;`と書いたときに、値xを`std::cout`で出力する。 +`cout_iterator`は`*i = x;`と書いたときに、値`x`を`std::cout`で出力する。 -`cout_iterator`は出力イテレーターの要件を満たすので`std::copy`に渡せる。`std::copy`はイテレーターを順番に`*out = *i ;`のように実行するので、結果として値が全て`std::cout`で出力される。 +`cout_iterator`は出力イテレーターの要件を満たすので`std::copy`に渡せる。`std::copy`はイテレーターを順番に`*out = *i ;`のように実行するので、結果として値がすべて`std::cout`で出力される。 `cout_iterator`はとても便利なので、標準ライブラリには`std::ostream_iterator`がある。 @@ -532,7 +533,7 @@ int main() 上のような出力イテレーターが`operator =`で以下のようなことをしていたらどうだろう。 -~~~c++ +~~~cpp template < typename Container > struct back_inserter ; { @@ -579,7 +580,7 @@ int main() ただし、`std::back_inserter`は古いライブラリなので、ここで示した方法とは少し違う実装がされている。 -~~~c++ +~~~cpp // 出力イテレーター template < typename Container > struct back_insert_iterator @@ -629,7 +630,7 @@ C++17以前のC++では関数の実引数からテンプレート仮引数`T`の 入力イテレーターの実例はどうか。 -`std::cin`からT型を読み込む入力イテレーターの実装は以下のようになる。 +`std::cin`から`T`型を読み込む入力イテレーターの実装は以下のようになる。 ~~~cpp template < typename T > @@ -690,7 +691,7 @@ bool operator !=( cin_iterator const & l, cin_iterator const & r ) 以下のように使える。 -~~~c++ +~~~cpp int main() { cin_iterator input, fail(true) ; @@ -702,7 +703,7 @@ int main() 実装としては、まずボイラープレートコード -~~~c++ +~~~cpp using difference_type = std::ptrdiff_t ; using value_type = T ; using reference = T & ; @@ -719,7 +720,7 @@ using iterator_category = std::input_iterator_tag ; データメンバーが2つ。 -~~~c++ +~~~cpp bool fail ; value_type value ; ~~~ @@ -739,7 +740,7 @@ int main() イテレーターから値を読み込むのは`operator *`の仕事だ。これは単に`value`を返す。 -~~~c++ +~~~cpp const reference operator *() const { return value ; } ~~~ @@ -748,7 +749,7 @@ const reference operator *() const 実際に`std::cin`から値を読み込むのは`operator ++`で行われる。 -~~~c++ +~~~cpp cin_iterator & operator ++() { // 失敗状態でなければ @@ -767,7 +768,7 @@ cin_iterator & operator ++() 後置インクリメントは前置インクリメントを呼び出すだけの汎用的な実装だ。 -~~~c++ +~~~cpp cin_iterator operator ++(int) { // 元の値をコピーし @@ -781,7 +782,7 @@ cin_iterator operator ++(int) コンストラクターに`true`を渡すと、イテレーターを最初から失敗状態にしておく -~~~c++ +~~~cpp cin_iterator( bool fail = false ) : fail(fail) { ++*this ; } @@ -791,7 +792,7 @@ cin_iterator( bool fail = false ) 入力イテレーターは同値比較ができる。 -~~~c++ +~~~cpp template < typename T > bool operator ==( cin_iterator const & l, cin_iterator const & r ) { return l.fail == r.fail ; } @@ -818,7 +819,7 @@ void print( InputIterator iter, InputIterator end_iter ) このような関数`print`に、`vector`の`begin`/`end`を渡すと、`vector`の要素をすべて標準出力する。 -~~~c++ +~~~cpp int main() { std::vector v = {1,2,3,4,5} ; @@ -828,7 +829,7 @@ int main() `cin_iterator`を渡した場合、失敗状態になるまで標準出力する。 -~~~c++ +~~~cpp int main() { cin_iterator iter, fail(true) ; @@ -860,7 +861,7 @@ int main() 以下のように使える。 -~~~c++ +~~~cpp int main() { iota_iterator iter(0) ; @@ -881,7 +882,7 @@ int main() } ~~~ -早速実装してみよう。まずはネストされた型名と初期化から。 +さっそく実装してみよう。まずはネストされた型名と初期化から。 ~~~cpp template < typename T > @@ -910,7 +911,7 @@ struct iota_iterator これでイテレーターとしてオブジェクトを作ることができるようになる。コピーは自動的に生成されるので書く必要はない。 -~~~c++ +~~~cpp int main() { // i(0) @@ -925,7 +926,7 @@ int main() 残りのコードも書いていこう。`operator *`は単に`value`を返すだけだ。 -~~~c++ +~~~cpp // 非const版 reference operator *() noexcept { return value ; } @@ -936,7 +937,7 @@ const reference operator *() const noexcept 非`const`版と`const`版があるのは、`const`な`iota_iterator`のオブジェクトからも使えるようにするためだ。 -~~~c++ +~~~cpp int main() { // 非constなオブジェクト @@ -959,7 +960,7 @@ int main() `operator ++`を実装しよう。 -~~~c++ +~~~cpp // 前置 iota_iterator & operator ++() noexcept { @@ -977,7 +978,7 @@ iota_iterator operator ++(int) noexcept すでに説明したようにインクリメント演算子には前置後置の2種類が存在する。 -~~~c++ +~~~cpp ++i ; // 前置 i++ ; // 後置 ~~~ @@ -990,7 +991,7 @@ i++ ; // 後置 最後は比較演算子だ。 -~~~c++ +~~~cpp bool operator == ( iota_iterator const & i ) const noexcept { return value == i.value ; @@ -1001,7 +1002,7 @@ bool operator != ( iota_iterator const & i ) const noexcept } ~~~ -前方イテレーターがサポートする比較演算子は2つ、`operator ==`と`operator !=`だ。`!=`は`==`で実装してしまうとして、`==`は単に`value`を比較する。通常、イテレーターの比較は要素の値の比較ではなく、同じ要素を参照するイテレーターかどうかの比較になるが、`iota_iterator`の場合、`vector`や`array`のようなメモリ上に構築された要素は存在しないので、`value`の比較でよい。 +前方イテレーターがサポートする比較演算子は2つ、`operator ==`と`operator !=`だ。`!=`は`==`で実装してしまうとして、`==`は単に`value`を比較する。通常、イテレーターの比較は要素の値の比較ではなく、同じ要素を参照するイテレーターかどうかの比較になるが、`iota_iterator`の場合、`vector`や`array`のようなメモリー上に構築された要素は存在しないので、`value`の比較でよい。 前方イテレーターが提供される実例としては、前方リンクリストがある。 @@ -1026,7 +1027,7 @@ int main() このような`forward_link_list`へのイテレーターの骨子は以下のように書ける。 -~~~c++ +~~~cpp template < typename T > struct iterator { @@ -1048,7 +1049,7 @@ struct iterator 前方リンクリストは`vector`や`array`のように要素の線形の集合を表現できる。`n`番目の要素から`n+1`番目の要素を返すことはできる。 -~~~c++ +~~~cpp // n+1番目の要素を返す関数 template < typename T > forward_link_list & next( forward_link_list & list ) noexcept @@ -1060,7 +1061,7 @@ forward_link_list & next( forward_link_list & list ) noexcept ただし`n-1`番目の要素を返すことはできない。その方法がないからだ。 -前方イテレーターが入力/出力イテレーターと違う点は、マルチパス保証があることだ。イテレーターのコピーを使いまわして複数回同じ要素をたどることができる。 +前方イテレーターが入力/出力イテレーターと違う点は、マルチパス保証があることだ。イテレーターのコピーを使い回して複数回同じ要素をたどることができる。 ~~~cpp template < typename ForwardIterator > @@ -1090,7 +1091,7 @@ void f( ForwardIterator first, ForwardIterator last ) 双方向イテレーターは`n`番目の要素を指すイテレーターから`n-1`番目を指すイテレーターを得られるイテレーターだ。`n-1`番目を指すには`operator --`を使う。 -~~~c++ +~~~cpp template < typename Iterator > void f( Iterator i ) { @@ -1101,7 +1102,7 @@ void f( Iterator i ) `iota_iterator`を双方向イテレーターにするのは簡単だ。 -~~~c++ +~~~cpp template < typename T > struct iota_iterator { @@ -1145,8 +1146,7 @@ struct bidirectional_link_list 双方向リンクリストに対するイテレーター操作の骨子は以下のようになる。 -~~~c++ - +~~~cpp template < typename T > struct iterator { @@ -1171,7 +1171,7 @@ struct iterator イテレーターの参照する要素の移動の部分。 -~~~c++ +~~~cpp template < typename T > struct iota_iterator { @@ -1223,7 +1223,7 @@ iota_iterator operator - 参考に、クラス外のフリー関数として実装する場合は以下のようになる。 -~~~c++ +~~~cpp template < typename T > iota_iterator operator + ( @@ -1242,13 +1242,13 @@ iota_iterator operator - { return i - n ; } ~~~ -`n + i`は必ずクラス外のフリー関数として実装しなければならない。クラスのメンバー関数として演算子のオーバーロードをする場合はオペランドがthisになるからだ。 +`n + i`は必ずクラス外のフリー関数として実装しなければならない。クラスのメンバー関数として演算子のオーバーロードをする場合はオペランドが`this`になるからだ。 イテレーターの距離の実装は`iota_iterator`の場合、単に`value`の差だ。 -メンバー関数として実装する場合は以下の通り。 +メンバー関数として実装する場合は以下のとおり。 -~~~c++ +~~~cpp template < typename T > struct iota_iterator { @@ -1259,9 +1259,9 @@ struct iota_iterator } ; ~~~ -クラス外のフリー関数として実装する場合は以下の通り。 +クラス外のフリー関数として実装する場合は以下のとおり。 -~~~c++ +~~~cpp template < typename T > typename iota_iterator::difference_type ( iota_iterator const & a, iota_iterator const & b ) @@ -1272,7 +1272,7 @@ typename iota_iterator::difference_type 大小比較の実装も`value`を比較するだけだ。 -~~~c++ +~~~cpp template < typename T > struct iota_iterator { @@ -1288,11 +1288,11 @@ struct iota_iterator } ; ~~~ -ランダムアクセスイテレーターの実例としては、連続したメモリ上に構築された要素の集合に対するイテレーターがある。標準ライブラリでは、`vector`や`array`が該当する。 +ランダムアクセスイテレーターの実例としては、連続したメモリー上に構築された要素の集合に対するイテレーターがある。標準ライブラリでは、`vector`や`array`が該当する。 -`vector`や`array`の中身は連続したメモリ上に確保された要素で、要素の参照にはポインターか、ポインターとインデックスが用いられる。 +`vector`や`array`の中身は連続したメモリー上に確保された要素で、要素の参照にはポインターか、ポインターとインデックスが用いられる。 -~~~c++ +~~~cpp // arrayやvectorのイテレーター template < typename T > struct iterator @@ -1310,7 +1310,7 @@ struct iterator そこで、C++標準ライブラリの実装によっては、`vector`や`array`の実装は単に生のポインターを返す。 -~~~c++ +~~~cpp template < typename T, std::size_t N > struct array { @@ -1349,7 +1349,7 @@ get_value( Iterator i ) } ~~~ -そのため、イテレーターのネストされた型名を使うときには、直接使うのではなく、一度`iterator_traits`を経由してつかうとよい。 +そのため、イテレーターのネストされた型名を使うときには、直接使うのではなく、一度`iterator_traits`を経由して使うとよい。 ## イテレーター操作 @@ -1359,20 +1359,20 @@ get_value( Iterator i ) イテレーター `i`を`n`回移動したいとする。ランダムアクセスイテレーターならば以下のようにする。 -~~~c++ +~~~cpp i += n ; ~~~ しかし前方イテレーターの場合、`operator +=`は使えない。`n`回`operator ++`を呼び出す必要がある。 -~~~c++ +~~~cpp for ( auto count = 0 ; count != n ; ++count ) ++i ; ~~~ 双方向イテレーターの場合、`n`は負数の場合がある。`n`が負数の場合、`n`回`operator --`を呼び出すことになる。 -~~~c++ +~~~cpp if ( n > 0 ) for ( auto count = 0 ; count != n ; ++count ) ++i ; @@ -1383,9 +1383,9 @@ else 双方向イテレーター用のコードはランダムアクセスイテレーターでも動くが非効率的だ。 -今使っているイテレーターの種類を把握して適切な方法を選ぶコードを書くのは面倒だ。そこで標準ライブラリには、イテレーター `i`を`n`回移動してくれる`advance(i, n)`がある。 +いま使っているイテレーターの種類を把握して適切な方法を選ぶコードを書くのは面倒だ。そこで標準ライブラリには、イテレーター `i`を`n`回移動してくれる`advance(i, n)`がある。 -~~~c++ +~~~cpp // iを1前方に移動 std::advance(i, 1) ; // iを5前方に移動 @@ -1400,7 +1400,7 @@ std::advance(i, 0) ; `advance(i,n)`はi自体が書き換わる。 -~~~c++ +~~~cpp i ; // n番目を指す std::advance( i, 1 ) ; i ; // n+1番目を指す @@ -1412,13 +1412,13 @@ i ; // n+1番目を指す ランダムアクセスイテレーターならば以下のようにする。 -~~~c++ +~~~cpp auto dist = last - first ; ~~~ それ以外のイテレーターならば、`first`が`last`と等しくなるまで`operator ++`を呼び出す。 -~~~c++ +~~~cpp std::size_t dist = 0 ; for ( auto iter = first ; iter != last ; ++iter ) ++dist ; @@ -1428,7 +1428,7 @@ for ( auto iter = first ; iter != last ; ++iter ) `distance( first, last )`は`first`から`last`までの距離を返す。 -~~~c++ +~~~cpp // iからjまでの距離を返す auto dist = std::distance( i, j ) ; ~~~ @@ -1439,7 +1439,7 @@ auto dist = std::distance( i, j ) ; ### next/prev : 移動したイテレーターを返す -`advance(i, n)`はイテレーターiを変更してしまう。イテレーターを変更させずに移動後のイテレーターも欲しい場合、以下のように書かなければならない。 +`advance(i, n)`はイテレーター`i`を変更してしまう。イテレーターを変更させずに移動後のイテレーターもほしい場合、以下のように書かなければならない。 ~~~cpp template < typename Iterator > @@ -1471,7 +1471,7 @@ void f( Iterator i ) { auto j = std::prev( i, 3 ) ; // jはiより3後方に移動している - // jはstd::advance(i, 3)した後のiと同じ値 + // jはstd::advance(i, 3)したあとのiと同じ値 } ~~~ @@ -1491,7 +1491,7 @@ void f( Iterator i ) ## リバースイテレーター -イテレーターは要素を順番通りにたどる。例えば以下は要素を順番に出力する関数テンプレート`print`だ。 +イテレーターは要素を順番どおりにたどる。例えば以下は要素を順番に出力する関数テンプレート`print`だ。 ~~~cpp template < typename Iterator > @@ -1519,7 +1519,7 @@ void reverse_print( Iterator first, Iterator last ) } ~~~ -しかしイテレーターを正順にたどるか逆順にたどるかという違いだけで、本質的に同じアルゴリズム、同じコードを二度も書きたくはない。そういうときに役立つのがリバースイテレーターだ。 +しかしイテレーターを正順にたどるか逆順にたどるかという違いだけで、本質的に同じアルゴリズム、同じコードを2度も書きたくはない。そういうときに役立つのがリバースイテレーターだ。 `std::reverse_iterator`はイテレーター`Iterator`に対するリバースイテレーターを提供する。リバースイテレーターはイテレーターのペア `[first,last)`を受け取り、`last`の1つ前の要素が先頭で`first`の要素が末尾になるような順番のイテレーターにしてくれる。 diff --git a/032-memory-allocation.md b/032-memory-allocation.md index b2f5dae..75c4280 100644 --- a/032-memory-allocation.md +++ b/032-memory-allocation.md @@ -1,8 +1,8 @@ -# 動的メモリ確保 +# 動的メモリー確保 ## 概要 -動的メモリ確保は任意のサイズのメモリを確保できる機能だ。 +動的メモリー確保は任意のサイズのメモリーを確保できる機能だ。 例えば`std::vector`は任意個の要素を保持できる。 @@ -20,32 +20,32 @@ int main() このプログラムは任意個の`int`型の値を保持する。いくつ保持するかはコンパイル時にはわからないし、実行途中にもわからない。プログラムが終了するまで、実際にいくつ値を保持したのかはわからない。 -このような事前にいくつの値を保持するかわからない状況では、動的メモリ確保を使う。 +このような事前にいくつの値を保持するかわからない状況では、動的メモリー確保を使う。 ## malloc/free -`malloc`/`free`はC言語から受け継いだ素朴な動的メモリ確保のライブラリだ。 +`malloc`/`free`はC言語から受け継いだ素朴な動的メモリー確保のライブラリだ。 -~~~c++ +~~~cpp namespace std { void * malloc ( std::size_t size ) ; void free ( void * ptr ) ; } ~~~ -`malloc(n)`は`n`バイトの生のメモリを確保して、その先頭バイトへのポインターを返す。 +`malloc(n)`は`n`バイトの生のメモリーを確保して、その先頭バイトへのポインターを返す。 -~~~c++ -// 5バイトのメモリを確保 +~~~cpp +// 5バイトのメモリーを確保 void * ptr = std::malloc( 5 ) ; ~~~ -これによって確保されるメモリは、1バイトごとのメモリが配列のように連続したメモリだ。型で書くと、`std::byte [5]`のようなものだ。 +これによって確保されるメモリーは、1バイトごとのメモリーが配列のように連続したメモリーだ。型で書くと、`std::byte [5]`のようなものだ。 -確保したメモリは`free`で解放するまで有効だ。`free(ptr)`は`malloc`が返したポインター`ptr`を解放する。その結果、メモリはまた再び`malloc`によって再利用できるようになる。 +確保したメモリーは`free`で解放するまで有効だ。`free(ptr)`は`malloc`が返したポインター`ptr`を解放する。その結果、メモリーはまた再び`malloc`によって再利用できるようになる。 -~~~c++ -// 5バイトの生のメモリを確保 +~~~cpp +// 5バイトの生のメモリーを確保 void * ptr = std::malloc( 5 ) ; // 解放 std::free( ptr ) ; @@ -54,9 +54,9 @@ std::free( ptr ) ; ## operator new/operator delete -C++の追加した生のメモリを確保する方法が、`operator new`と`operator delete`だ。 +C++の追加した生のメモリーを確保する方法が、`operator new`と`operator delete`だ。 -~~~c++ +~~~cpp // グローバル名前空間 void * operator new ( std::size_t size ); void operator delete ( void * ptr ) ; @@ -64,15 +64,15 @@ void operator delete ( void * ptr ) ; 使い方は`malloc`とほぼ同じだ。`"operator new"`までが名前なので少し混乱するが、通常の関数呼び出しと同じだ。 -~~~c++ +~~~cpp void * ptr = ::operator new( 5 ) ; ~~~ グローバル名前空間であることを明示するために`::`を使っている。 -`operator new`で確保したメモリは、`operator delete`で解放するまで有効だ。 +`operator new`で確保したメモリーは、`operator delete`で解放するまで有効だ。 -~~~c++ +~~~cpp void * ptr = ::operator new( 5 ) ; ::operator delete ( ptr ) ; ~~~ @@ -81,7 +81,7 @@ void * ptr = ::operator new( 5 ) ; `int`や`double`のような基本的な型は、生のバイト列のポインターを型変換するだけで使える。 -1. 生のメモリを確保 +1. 生のメモリーを確保 2. ポインターを型変換 3. 値を代入 @@ -99,15 +99,15 @@ int main() } ~~~ -`int`型のサイズは`sizeof(int)`バイトなので、`sizeof(int)`バイトのメモリを確保する。`void *`型から`int *`型に型変換する。あとはポインターを経由して使うだけだ。 +`int`型のサイズは`sizeof(int)`バイトなので、`sizeof(int)`バイトのメモリーを確保する。`void *`型から`int *`型に型変換する。あとはポインターを経由して使うだけだ。 ポインターの文法がわかりにくい場合、リファレンスを使うこともできる。 -~~~c++ +~~~cpp int & int_ref = *int_ptr ; ~~~ -`malloc`や`operator new`が返すメモリの値は不定だ。なので、確保した生のメモリーへのポインターを、実際に使う型のポインターに型変換して、その値を参照しようとすると、結果は未定義だ。 +`malloc`や`operator new`が返すメモリーの値は不定だ。なので、確保した生のメモリーへのポインターを、実際に使う型のポインターに型変換して、その値を参照しようとすると、結果は未定義だ。 ~~~cpp int main() @@ -121,9 +121,9 @@ int main() このプログラムを実行した結果、何が起こるかはわからない。 -## メモリ確保の失敗 +## メモリー確保の失敗 -メモリ確保は失敗する可能性がある。現実のコンピューターは有限のリソースしか持たないために、メモリも当然有限のリソースだ。 +メモリー確保は失敗する可能性がある。現実のコンピューターは有限のリソースしか持たないために、メモリーも当然有限のリソースだ。 `malloc`が失敗すると、`nullptr`が返される。`malloc`が失敗したかどうかを調べるには、戻り値を`nullptr`と比較すればよい。 @@ -133,9 +133,9 @@ int main() void * ptr = std::malloc( 1 ) ; if ( ptr == nullptr ) { - // メモリ確保失敗 + // メモリー確保失敗 } else { - // メモリ確保成功 + // メモリー確保成功 } } ~~~ @@ -147,17 +147,17 @@ int main() { try { void * ptr = ::operator new( 1 ) ; - // メモリ確保成功 + // メモリー確保成功 } catch ( std::bad_alloc e ) { - // メモリ確保失敗 + // メモリー確保失敗 } } ~~~ -大抵の環境ではメモリ確保が失敗したときにできることは少ない。そのままプログラムを終了するのが最も適切な処理だ。というのも、ほとんどの処理にはメモリ確保が必要だからだ。 +たいていの環境ではメモリー確保が失敗したときにできることは少ない。そのままプログラムを終了するのが最も適切な処理だ。というのも、ほとんどの処理にはメモリー確保が必要だからだ。 -例外の場合、`catch`しなければプログラムは終了する。`malloc`の場合、自分でメモリ確保が失敗したかどうかを調べてプログラムを終了しなければならない。プログラムを途中で強制的に終了するには、`std::abort`が使える。 +例外の場合、`catch`しなければプログラムは終了する。`malloc`の場合、自分でメモリー確保が失敗したかどうかを調べてプログラムを終了しなければならない。プログラムを途中で強制的に終了するには、`std::abort`が使える。 ~~~cpp void f() @@ -174,21 +174,21 @@ void f() ## クラス型の値の構築 -動的に確保したメモリを`int`や`double`のような基本的な型の値として使うには以下のように書けばよいことはすでに学んだ。 +動的に確保したメモリーを`int`や`double`のような基本的な型の値として使うには以下のように書けばよいことはすでに学んだ。 -1. その型のサイズ分のメモリを確保 +1. その型のサイズ分のメモリーを確保 2. ポインターを型変換 3. 適切な値を代入 より汎用的にテンプレートを使って書くと以下のようになる。 ~~~cpp -// 動的確保したメモリをT型の値として使う +// 動的確保したメモリーをT型の値として使う template < typename T > void dynamic_allocate() { - // 1. その型のサイズ分のメモリを確保 + // 1. その型のサイズ分のメモリーを確保 void * ptr = ::operator new( sizeof(T) ) ; // 2. ポインターを型変換 T * T_ptr = static_cast( ptr ) ; @@ -206,14 +206,14 @@ int main() この方法は、ほとんどのクラスには使えない。例えば`std::vector`には使えない。 -~~~c++ +~~~cpp // エラー dynamic_allocate< std::vector >() ; ~~~ 「ほとんどのクラス」と書いたからには、使えるクラスもあるということだ。例えば以下のようなクラスでは使える。 -~~~c++ +~~~cpp struct Simple { int i ; @@ -229,15 +229,15 @@ int main() なぜ`Simple`のようなクラスでは使えるのだろうか。`std::vector`とはどう違うのか。この違いを厳密に解説するためには、とても長くて厳密なC++の標準規格の理解が必要だ。とても難しいため、本書では解説しない。 -クラスの値を使うためには、メモリ上にクラスのオブジェクトを構築する必要がある。クラスの構築にはコンストラクター呼び出し以外にも、そのメモリをクラスのオブジェクトとして使うのに必要な何らかの初期化が含まれる。 +クラスの値を使うためには、メモリー上にクラスのオブジェクトを構築する必要がある。クラスの構築にはコンストラクター呼び出し以外にも、そのメモリーをクラスのオブジェクトとして使うのに必要な何らかの初期化が含まれる。 ~~~cpp -// sizeof(std::vector)バイトのメモリを確保し -// そのメモリ上にクラスのオブジェクトを構築 +// sizeof(std::vector)バイトのメモリーを確保し +// そのメモリー上にクラスのオブジェクトを構築 std::vector v ; ~~~ -生のメモリ上にクラスのような複雑な型を構築するには、`newプレイスメント`を使う。 +生のメモリー上にクラスのような複雑な型を構築するには、`newプレイスメント`を使う。 ~~~c++ new ( 生のポインター ) 型 new初期化子 @@ -247,14 +247,14 @@ new ( 生のポインター ) 型 new初期化子 例えば`std::vector`型を構築するには以下のようにする。 -~~~c++ -// 生のメモリを動的確保 +~~~cpp +// 生のメモリーを動的確保 void * ptr = ::operator new ( sizeof( std::vector ) ) ; -// 生のメモリ上に型を構築 +// 生のメモリー上に型を構築 std::vector * vector_ptr = new (ptr) std::vector{} ; ~~~ -こうすればクラスが適切にメモリ上に構築され、コンストラクターも呼ばれる。コンストラクターが呼ばれることを確かめてみよう。 +こうすればクラスが適切にメモリー上に構築され、コンストラクターも呼ばれる。コンストラクターが呼ばれることを確かめてみよう。 ~~~cpp struct Logger @@ -278,7 +278,7 @@ int main() クラスのオブジェクトを適切に破棄するためには、デストラクターを呼ばなければならない。通常の変数ならば、変数が寿命を迎えたときに自動的にデストラクターが呼ばれてくれる。 -~~~c++ +~~~cpp int main() { Logger Alice("Alice"s) ; @@ -299,10 +299,10 @@ Bob is destructed. Alice is destructed. ~~~ -動的に確保されるメモリ上に構築されたオブジェクトは自動的に破棄されてくれない。クラスのオブジェクトの場合デストラクターを呼び出さなければならないが、動的メモリ確保したメモリ上に構築したクラスのオブジェクトの場合は、明示的に呼び出さなければならない。 +動的に確保されるメモリー上に構築されたオブジェクトは自動的に破棄されてくれない。クラスのオブジェクトの場合デストラクターを呼び出さなければならないが、動的メモリー確保したメモリー上に構築したクラスのオブジェクトの場合は、明示的に呼び出さなければならない。 -~~~c++ -// 動的メモリ確保 +~~~cpp +// 動的メモリー確保 void * raw_ptr = ::operator new( sizeof(Logger) ) ; // 構築 Logger * logger_ptr = new(raw_ptr) Logger{ "Alice"s } ; @@ -312,33 +312,33 @@ logger_ptr->~Logger() ; ::operator delete( raw_ptr ) ; ~~~ -このようにすれば、コンストラクター、デストラクターが適切に呼ばれる。また確保したメモリも解放される。 +このようにすれば、コンストラクター、デストラクターが適切に呼ばれる。また確保したメモリーも解放される。 ## new/delete クラスのオブジェクトを動的確保するのに、生の文字列の確保/解放と、クラスのオブジェクトの構築/破棄をすべて自前で行うのは面倒だ。幸い、確保と構築、破棄と解放を同時にやってくれる機能がある。`new式`と`delete式`だ。 -~~~ +~~~c++ new 型 new初期化子 delete ポインター ~~~ -`new式`は生のメモリを確保し、型のオブジェクトを構築し、型へのポインターを返す。 +`new式`は生のメモリーを確保し、型のオブジェクトを構築し、型へのポインターを返す。 ~~~cpp int * int_ptr = new int{123} ; std::vector * vector_ptr = new std::vector{} ; ~~~ -`delete式`は`new式`で返されたポインターの指し示すオブジェクトを破棄し、生のメモリを解放する。 +`delete式`は`new式`で返されたポインターの指し示すオブジェクトを破棄し、生のメモリーを解放する。 -~~~c++ +~~~cpp delete int_ptr ; delete vector_ptr ; ~~~ -`new式`がメモリの確保に失敗すると、`std::bad_alloc`例外を投げる。 +`new式`がメモリーの確保に失敗すると、`std::bad_alloc`例外を投げる。 ~~~cpp int main() @@ -357,13 +357,13 @@ int main() `new式`は配列型を動的確保することもできる。 -~~~c++ +~~~cpp int * int_array_ptr = new int[5]{1,2,3,4,5} ; ~~~ 配列型を`new式`で動的確保した場合、`delete式`は通常の`delete`ではなく、`delete[]`を使わなければならない。 -~~~c++ +~~~cpp delete [] int_array_ptr ; ~~~ @@ -398,9 +398,9 @@ int main() } ~~~ -このクラスは様々な点で実用的ではない。例えばこのクラスはコピーできてしまう。 +このクラスはさまざまな点で実用的ではない。例えばこのクラスはコピーできてしまう。 -~~~c++ +~~~cpp int main() { smart_ptr p1 ; @@ -411,6 +411,6 @@ int main() } ~~~ -このコードの何がまずいかというと、`smart_ptr::ptr`がコピーされてしまうということだ。`p2`が破棄されると、`delete ptr`が実行される。その後に`p1`が破棄されるのだが、もう一度`delete ptr`が実行されてしまうのだ。一度`delete`を呼び出したポインターはもう無効になっているので、それ以上`delete`を呼び出すことはできない。よってエラーになる。 +このコードの何がまずいかというと、`smart_ptr::ptr`がコピーされてしまうということだ。`p2`が破棄されると、`delete ptr`が実行される。そのあとに`p1`が破棄されるのだが、もう一度`delete ptr`が実行されてしまうのだ。一度`delete`を呼び出したポインターはもう無効になっているので、それ以上`delete`を呼び出すことはできない。よってエラーになる。 -この問題を解決するには、まだ学んでいないC++の機能がたくさん必要になる。この問題は必要な機能をすべて学び終えた後の章で、もう一度挑戦することにしよう。 +この問題を解決するには、まだ学んでいないC++の機能がたくさん必要になる。この問題は必要な機能をすべて学び終えたあとの章で、もう一度挑戦することにしよう。 diff --git a/033-vector-implementation.md b/033-vector-implementation.md index 47bdc42..e6ac951 100644 --- a/033-vector-implementation.md +++ b/033-vector-implementation.md @@ -1,10 +1,10 @@ # vectorの実装 : 基礎 -クラス、ポインター、メモリ確保を学んだので、とうとうコンテナーの中でも一番有名な`std::vector`を実装する用意ができた。しかしその前に、アロケーターについて学ぶ必要がある。 +クラス、ポインター、メモリー確保を学んだので、とうとうコンテナーの中でも一番有名な`std::vector`を実装する用意ができた。しかしその前に、アロケーターについて学ぶ必要がある。 `std::vector`は`std::vector`のように要素の型`T`を指定して使うので、以下のようになっていると思う読者もいるだろう。 -~~~c++ +~~~cpp namespace std { template < typename T > struct vector ; @@ -13,20 +13,20 @@ namespace std { 実際には以下のようになっている。 -~~~c++ +~~~cpp namespace std { template < typename T, typename allocator = allocator > struct vector ; } ~~~ -`std::allocator`というのは標準ライブラリのアロケーターだ。アロケーターは生のメモリの確保と解放をするライブラリだ。デフォルトで`std::allocator`が渡されるので、普段ユーザーはアロケーターを意識することはない。 +`std::allocator`というのは標準ライブラリのアロケーターだ。アロケーターは生のメモリーの確保と解放をするライブラリだ。デフォルトで`std::allocator`が渡されるので、普段ユーザーはアロケーターを意識することはない。 -`std::vector`は`malloc`や`operator new`を直接使わずアロケーターを使ってメモリ確保を行う。 +`std::vector`は`malloc`や`operator new`を直接使わずアロケーターを使ってメモリー確保を行う。 -アロケーターはテンプレートパラメーターで指定できる。何らかの理由で独自のメモリ確保を行いたい場合、独自のアロケーターを実装してコンテナーに渡すことができる。 +アロケーターはテンプレートパラメーターで指定できる。何らかの理由で独自のメモリー確保を行いたい場合、独自のアロケーターを実装してコンテナーに渡すことができる。 -~~~c++ +~~~cpp // 独自のアロケーター template < typename T > struct custom_allocator @@ -40,16 +40,16 @@ using custom_vector = std::vector< T, custom_allocator > ; int main() { custom_vector v ; - // 独自のアロケーターを使ったメモリ確保 + // 独自のアロケーターを使ったメモリー確保 v.push_back(0) ; } ~~~ ## `std::allocator`の概要 -`std::allocator`は`T`型を構築できる生のメモリを確保するための以下のようになっている。 +`std::allocator`は`T`型を構築できる生のメモリーを確保するための以下のようになっている。 -~~~c++ +~~~cpp namespace std { template class allocator { // ネストされた型名の宣言 @@ -77,36 +77,36 @@ template class allocator { `constexpr`というキーワードがあるが、ここでは気にする必要はない。あとで学ぶ。 -重要なのはメモリ確保をする`allocate`と、メモリ解放をする`deallocate`だ。 +重要なのはメモリー確保をする`allocate`と、メモリー解放をする`deallocate`だ。 ## `std::allocator`の使い方 -標準ライブラリのアロケーター、`std::allocator`は、T型を構築できる生のメモリの確保と解放をするライブラリだ。重要なメンバーは以下の通り。 +標準ライブラリのアロケーター、`std::allocator`は、`T`型を構築できる生のメモリーの確保と解放をするライブラリだ。重要なメンバーは以下のとおり。 ~~~cpp -// メモリ確保 +// メモリー確保 [[nodiscard]] T* allocate(size_t n); -// メモリ解放 +// メモリー解放 void deallocate(T* p, size_t n); ~~~ -`allocate(n)`はT型のn個の配列を構築できるだけの生のメモリを確保してその先頭へのポインターを返す。 +`allocate(n)`は`T`型の`n`個の配列を構築できるだけの生のメモリーを確保してその先頭へのポインターを返す。 -`deallocate(p, n)`は`allocate(n)`で確保されたメモリを解放する。 +`deallocate(p, n)`は`allocate(n)`で確保されたメモリーを解放する。 ~~~cpp int main() { std::allocator a ; - // 生のメモリ確保 - // std::string [1]分のメモリサイズ + // 生のメモリー確保 + // std::string [1]分のメモリーサイズ std::string * p = a.allocate(1) ; - // メモリ解放 + // メモリー解放 a.deallocate( p, 1 ) ; } ~~~ -`allocate`には`[[nodiscard]]`という属性がついている。これにより戻り値を無視すると警告が出る。 +`allocate`には`[[nodiscard]]`という属性が付いている。これにより戻り値を無視すると警告が出る。 ~~~cpp int main() @@ -120,20 +120,20 @@ int main() } ~~~ -確保されるのが生のメモリだということに注意したい。実際にT型の値として使うには、`new`による構築が必要だ。 +確保されるのが生のメモリーだということに注意したい。実際に`T`型の値として使うには、`new`による構築が必要だ。 ~~~cpp int main() { std::allocator a ; - // 生のメモリ確保 - // std::string [1]分のメモリサイズ + // 生のメモリー確保 + // std::string [1]分のメモリーサイズ std::string * p = a.allocate(1) ; // 構築 std::string * s = new(p) std::string("hello") ; // 明示的なデストラクター呼び出し s->basic_string() ; - // メモリ解放 + // メモリー解放 a.deallocate( p, 1 ) ; } ~~~ @@ -142,7 +142,7 @@ int main() 実は`std::string`は以下のようなクラステンプレートになっている。 -~~~c++ +~~~cpp namespace std { template < typename charT, @@ -161,7 +161,7 @@ class basic_string 普段は使っている`std::string`というのは、以下のようなエイリアスだ。 -~~~c++ +~~~cpp namespace std { using string = basic_string ; } @@ -182,7 +182,7 @@ void destroy_at( T * location ) このようにテンプレートで書くことによって、クラス名を意識せずに破棄ができる。 -~~~c++ +~~~cpp // 破棄 destroy_at( s ) ; ~~~ @@ -195,14 +195,14 @@ destroy_at( s ) ; `allocator_traits`はアロケーターの型`Alloc`を指定して使う。 -~~~c++ +~~~cpp std::allocator a ; int * p = a.allocate(1) ; ~~~ -と書くかわりに、 +と書く代わりに、 -~~~c++ +~~~cpp std::allocator a ; int * p = std::allocator_traits< std::allocator >::allocate( a, 1 ) ; ~~~ @@ -211,7 +211,7 @@ int * p = std::allocator_traits< std::allocator >::allocate( a, 1 ) ; これはとても使いづらいので、`allocator_traits`のエイリアスを書くとよい。 -~~~c++ +~~~cpp std::allocator a ; // エイリアス using traits = std::allocator_traits< std::allocator > ; @@ -220,7 +220,7 @@ int * p = traits::allocate( a, 1 ) ; これもまだ書きにくいので、`decltype`を使う。`decltype(expr)`は式`expr`の型として使える機能だ。 -~~~c++ +~~~cpp // int型 decltype(0) a ; // double型 @@ -233,14 +233,14 @@ decltype( "hello"s ) c ; `decltype`を使うと以下のように書ける。 -~~~c++ +~~~cpp std::allocator a ; // エイリアス using traits = std::allocator_traits< decltype(a) > ; int * p = traits::allocate( a, 1 ) ; ~~~ -`allocator_traits`はアロケーターを使った生のメモリの確保、解放と、そのメモリ上にオブジェクトを構築、破棄する機能を提供している。 +`allocator_traits`はアロケーターを使った生のメモリーの確保、解放と、そのメモリー上にオブジェクトを構築、破棄する機能を提供している。 ~~~cpp @@ -250,20 +250,20 @@ int main() // allocator_traits型 using traits = std::allocator_traits ; - // 生のメモリ確保 + // 生のメモリー確保 std::string * p = traits::allocate( a, 1 ) ; // 構築 std::string * s = traits::construct( a, p, "hello") ; // 破棄 traits::destroy( a, s ) ; - // メモリ解放 + // メモリー解放 traits::deallocate( a, p, 1 ) ; } ~~~ -`T`型の`N`個の配列を構築するには、まず`N`個の生のメモリを確保し、 +`T`型の`N`個の配列を構築するには、まず`N`個の生のメモリーを確保し、 -~~~c++ +~~~cpp std::allocator a ; using traits = std::allocator_traits ; std::string * p = traits::allocate( a, N ) ; @@ -271,7 +271,7 @@ std::string * p = traits::allocate( a, N ) ; `N`回の構築を行う。 -~~~c++ +~~~cpp for ( auto i = p, last = p + N ; i != last ; ++i ) { traits::construct( a, i, "hello" ) ; @@ -280,16 +280,16 @@ for ( auto i = p, last = p + N ; i != last ; ++i ) 破棄も`N`回行う。 -~~~c++ +~~~cpp for ( auto i = p + N, first = p ; i != first ; --i ) { traits::destroy( a, i ) ; } ~~~ -生のメモリを破棄する。 +生のメモリーを破棄する。 -~~~c++ +~~~cpp traits::deallocate( a, p, N ) ; ~~~ @@ -298,7 +298,7 @@ traits::deallocate( a, p, N ) ; 準備はできた。簡易的な`vector`を実装していこう。以下が本書で実装する簡易`vector`だ。 -~~~c++ +~~~cpp template < typename T, typename Allocator = std::allocator > class vector { @@ -329,7 +329,7 @@ public : 例えば要素数を定めて配列のようにアクセスできる。 -~~~c++ +~~~cpp vector v(100) ; for ( auto i = 0 ; i != 100 ; ++i ) v[i] = i ; @@ -337,14 +337,14 @@ for ( auto i = 0 ; i != 100 ; ++i ) イテレーターも使える。 -~~~c++ +~~~cpp std::for_each( std::begin(v), std::end(v), []( auto x ) { std::cout << x ; } ) ; ~~~ 要素を際限なく追加できる。 -~~~c++ +~~~cpp std::copy( std::istream_iterator(std::cin), std::istream_iterator(), std::back_inserter(v) ) ; @@ -354,7 +354,7 @@ std::copy( 簡易`vector`の概要では、まだ学んでいない機能が使われていた。`class`と`public`と`private`だ。 -C++のクラスにはアクセス指定がある。`public:`と`private:`だ。アクセス指定が書かれた後、別のアクセス指定が現れるまでの間のメンバーは、アクセス指定の影響を受ける。 +C++のクラスにはアクセス指定がある。`public:`と`private:`だ。アクセス指定が書かれたあと、別のアクセス指定が現れるまでの間のメンバーは、アクセス指定の影響を受ける。 ~~~cpp struct C @@ -395,7 +395,7 @@ int main() `private`メンバーはクラスの外から使うことができない。 -~~~c++ +~~~cpp struct C { private : @@ -415,7 +415,7 @@ int main() コンストラクターもアクセス指定の対象になる。 -~~~c++ +~~~cpp struct C { public : @@ -474,7 +474,7 @@ public : もし`dynamic_int::ptr`が`public`メンバーだった場合、以下のようなコードのコンパイルが通ってしまう。 -~~~c++ +~~~cpp int main() { dynamic_int i ; @@ -521,7 +521,7 @@ class bar ## ネストされた型名 -`std::vector`には様々なネストされた型名がある。 +`std::vector`にはさまざまなネストされた型名がある。 ~~~cpp int main() @@ -536,9 +536,9 @@ int main() 自作の簡易`vector`で`std::vector`と同じようにネストされた型名を書いていこう。 -要素型に関係するネストされた型名 +要素型に関係するネストされた型名。 -~~~c++ +~~~cpp template < typename T, typename Allocator = std::allocator > class vector { @@ -555,7 +555,7 @@ public : アロケーター型も`allocator_type`としてエイリアス宣言される。 -~~~c++ +~~~cpp template < typename T, typename Allocator = std::allocator > class vector { @@ -575,7 +575,7 @@ void f( std::vector & v ) 通常`std::size_t`が使われる。 -~~~c++ +~~~cpp size_type = std::size_t ; ~~~ @@ -594,13 +594,13 @@ void f( std::vector & v ) 通常`std::ptrdiff_t`が使われる。 -~~~c++ +~~~cpp difference_type = std::ptrdiff_t ; ~~~ イテレーターのエイリアス。 -~~~c++ +~~~cpp using iterator = pointer ; using const_iterator = const_pointer ; using reverse_iterator = std::reverse_iterator ; @@ -623,7 +623,7 @@ using const_reverse_iterator = std::reverse_iterator ; これを素直に考えると、ポインター1つ、整数2つ、アロケーター1つの4つのデータメンバーになる。 -~~~c++ +~~~cpp template < typename T, typename Allocator = std::allocator > class vector { @@ -641,14 +641,14 @@ private : 確かに`std::vector`はこのようなデータメンバーでも実装できる。しかし多くの実装では以下のようなポインター3つとアロケーター1つになっている。 -~~~c++ +~~~cpp template < typename T, typename Allocator = std::allocator > class vector { private : // 先頭の要素へのポインター pointer first ; - // 最後の要素のひとつ前方のポインター + // 最後の要素の1つ前方のポインター pointer last ; // 確保したストレージの終端 pointer reserved_last ; @@ -665,13 +665,13 @@ private : 簡易`vector`の簡単なメンバー関数を実装していく。ここでのサンプルコードはすべて簡易`vector`のクラス定義の中に書いたかのように扱う。例えば -~~~c++ +~~~cpp void f() { } ~~~ とある場合、これは、 -~~~c++ +~~~cpp template < typename T, typename Allocator = std::allocator > class vector { @@ -689,7 +689,7 @@ public : まず通常のイテレーター -~~~c++ +~~~cpp iterator begin() noexcept { return first ; } iterator end() noexcept @@ -720,7 +720,7 @@ int main() これを実現するには、メンバー関数を`const`修飾する。 -~~~c++ +~~~cpp struct Foo { // 非const版 @@ -746,7 +746,7 @@ int main() 簡易`vector`での実装は単に`const`修飾するだけだ。 -~~~c++ +~~~cpp iterator begin() const noexcept { return first ; } iterator end() const noexcept @@ -766,7 +766,7 @@ int main() この実装はメンバー関数名以外同じだ。 -~~~c++ +~~~cpp const_iterator cbegin() const noexcept { return first ; } const_iterator cend() const noexcept @@ -775,7 +775,7 @@ const_iterator cend() const noexcept `std::vector`にはリバースイテレーターを返すメンバー関数`rbegin`/`rend`と`crbegin`/`crend`がある。 -~~~c++ +~~~cpp int main() { std::vector v = {1,2,3,4,5} ; @@ -792,7 +792,7 @@ int main() `begin`に対する`rbegin`/`rend`の実装は以下のようになる。`crbegin`/`crend`は自分で実装してみよう。 -~~~c++ +~~~cpp reverse_iterator rbegin() noexcept { return reverse_iterator{ last } ; } reverse_iterator rend() noexcept @@ -820,13 +820,13 @@ class Number 例えば`Number`クラスを引数に取る関数があると、 -~~~c++ +~~~cpp void print_number( Number n ) ; ~~~ 変換コンストラクターの型の値を渡せる。 -~~~c++ +~~~cpp int main() { // int型から変換 @@ -842,7 +842,7 @@ int main() 戻り値として返すときにも変換できる。 -~~~c++ +~~~cpp // Number型のゼロを返す Number zero() { @@ -851,10 +851,9 @@ Number zero() } ~~~ -しかし、場合によってはこのような暗黙の型変換を行いたくないこともある。そういう場合、コンストラクターに`explicit`キーワードをつけると、暗黙の型変換を禁止させることができる。 - -~~~c++ +しかし、場合によってはこのような暗黙の型変換を行いたくないこともある。そういう場合、コンストラクターに`explicit`キーワードを付けると、暗黙の型変換を禁止させることができる。 +~~~cpp class Number { explicit Number( int i ) ; @@ -863,9 +862,9 @@ class Number } ; ~~~ -実は`std::reverse_iterator`のコンストラクターにも`explicit`キーワードがついている。 +実は`std::reverse_iterator`のコンストラクターにも`explicit`キーワードが付いている。 -~~~c++ +~~~cpp namespace std { template< typename Iterator > class reverse_iterator @@ -894,16 +893,16 @@ int main() bool b = v.empty() ; // 1、現在の要素数 auto s = v.size() ; - // 実装依存、追加の動的メモリ確保をせずに格納できる要素の最大数 + // 実装依存、追加の動的メモリー確保をせずに格納できる要素の最大数 auto c = v.capacity() ; } ~~~ -早速実装していこう。 +さっそく実装していこう。 `size`は要素数を返す。イテレーターの距離を求めればよい。 -~~~c++ +~~~cpp size_type size() const noexcept { return end() - begin() ; @@ -913,7 +912,7 @@ size_type size() const noexcept イテレーターライブラリを使ってもよい。本物の`std::vector`では以下のように実装されている。 -~~~c++ +~~~cpp size_type size() const noexcept { return std::distance( begin(), end() ) ; @@ -922,7 +921,7 @@ size_type size() const noexcept `empty`は空であれば`true`、そうでなければ`false`を返す。「空」というのは要素数がゼロという意味だ。 -~~~c++ +~~~cpp bool empty() const noexcept { return size() == 0 ; @@ -931,16 +930,16 @@ bool empty() const noexcept しかし`size() == 0`というのは、`begin() == end()`ということだ。なぜならば要素数が0であれば、イテレーターのペアはどちらも終端のイテレーターを差しているからだ。本物の`std::vector`では以下のように実装されている。 -~~~c++ +~~~cpp bool empty() const noexcept { return begin() == end() ; } ~~~ -`capacity`は、追加の動的メモリ確保をせずに追加できる要素の最大数を返す。これを計算するには、動的確保したストレージの末尾の1つ次のポインターであるデータメンバーである`reserved_last`を使う。最初の要素へのポインターである`first`から`reserved_last`までの距離が答えだ。ポインターの距離はイテレーターと同じく引き算する。 +`capacity`は、追加の動的メモリー確保をせずに追加できる要素の最大数を返す。これを計算するには、動的確保したストレージの末尾の1つ次のポインターであるデータメンバーである`reserved_last`を使う。最初の要素へのポインターである`first`から`reserved_last`までの距離が答えだ。ポインターの距離はイテレーターと同じく引き算する。 -~~~c++ +~~~cpp size_type capacity() const noexcept { return reserved_last - first ; @@ -964,7 +963,7 @@ int main() `operator []`は非`const`版と`const`版の2種類がある。 -~~~c++ +~~~cpp reference operator []( size_type i ) { return first[i] ; } const_reference operator []( size_type i ) const @@ -991,9 +990,9 @@ int main() } ~~~ -実装はインデックスを`size()`と比較して、範囲外であれば`std::out_of_range`をthrowする。`operator []`と同じく、非`const`版と`const`版がある。 +実装はインデックスを`size()`と比較して、範囲外であれば`std::out_of_range`を`throw`する。`operator []`と同じく、非`const`版と`const`版がある。 -~~~c++ +~~~cpp reference at( size_type i ) { if ( i >= size() ) @@ -1027,7 +1026,7 @@ int main() これにも`const`版と非`const`版がある。`vector`の`last`が最後の要素の次のポインターを指していることに注意。 -~~~c++ +~~~cpp reference front() { return first ; } const_reference front() const @@ -1053,7 +1052,7 @@ int main() 実装は`first`を返すだけだ。 -~~~c++ +~~~cpp pointer data() noexcept { return first ; } const_pointer data() const noexcept diff --git a/034-vector-memory-allocation.md b/034-vector-memory-allocation.md index acc362f..1fb2ef4 100644 --- a/034-vector-memory-allocation.md +++ b/034-vector-memory-allocation.md @@ -1,8 +1,8 @@ -# vectorの実装 : メモリ確保 +# vectorの実装 : メモリー確保 -## メモリ確保と解放の起こるタイミング +## メモリー確保と解放の起こるタイミング -`std::vector`はどこでメモリを確保と解放しているのだろうか。 +`std::vector`はどこでメモリーを確保と解放しているのだろうか。 デフォルト構築すると空になる。 @@ -24,7 +24,7 @@ int main() } ~~~ -すると`std::vector`は指定した要素数の有効な要素をもつ。 +すると`std::vector`は指定した要素数の有効な要素を持つ。 コンストラクターに要素数と初期値を渡すことができる。 @@ -40,7 +40,7 @@ int main() すると、指定した要素数で、要素の値はすべて初期値になる。 -`vector`のオブジェクトを構築した後でも、メンバー関数`resize(size)`で要素数を`size`個にできる。 +`vector`のオブジェクトを構築したあとでも、メンバー関数`resize(size)`で要素数を`size`個にできる。 ~~~cpp int main() @@ -92,22 +92,22 @@ int main() } ~~~ -`reserve(size)`は少なくとも`size`個の要素が追加の動的メモリ確保なしで追加できるようにメモリを予約する。 +`reserve(size)`は少なくとも`size`個の要素が追加の動的メモリー確保なしで追加できるようにメモリーを予約する。 ~~~cpp int main() { std::vector v ; - // 少なくとも3個の要素を追加できるように動的メモリ確保 + // 少なくとも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) ; } ~~~ @@ -129,7 +129,7 @@ int main() 簡易`vector`のデフォルトコンストラクターは何もしない。 -~~~c++ +~~~cpp vector( ) { } ~~~ @@ -137,7 +137,7 @@ vector( ) { } これで簡易`vector`の変数を作れるようになった。ただしまだ何もできない。 -~~~c++ +~~~cpp int main() { vector v ; @@ -164,15 +164,15 @@ int main() これを実装するには、アロケーターを取ってデータメンバーにコピーするコンストラクターを書く。 -~~~c++ +~~~cpp vector( const allocator_type & alloc ) noexcept alloc( alloc ) { } ~~~ -他のコンストラクターはこのコンストラクターにまずデリゲートすればよい。 +ほかのコンストラクターはこのコンストラクターにまずデリゲートすればよい。 -~~~c++ +~~~cpp vector() : vector( allocator_type() ) { } @@ -189,7 +189,7 @@ vector( size_type size, const_reference value, const allocator_type & alloc = al 要素数と初期値を取るコンストラクターは`resize`を使えば簡単に実装できる。 -~~~c++ +~~~cpp vector( size_type size, const allocator_type & alloc ) : vector( alloc ) { @@ -208,7 +208,7 @@ vector( size_type size, const_reference value, const allocator_type & alloc ) ここでは`vector`の実装を楽にするためのヘルパー関数をいくつか実装する。このヘルパー関数はユーザーから使うことは想定しないので、`private`メンバーにする。 -~~~c++ +~~~cpp // 例 struct vector { @@ -229,7 +229,7 @@ public : アロケーターは`allocator_traits`を経由して使う。実際のコードはとても冗長になる。 -~~~c++ +~~~cpp template < typename Allocator > void f( Allocator & alloc ) { @@ -239,7 +239,7 @@ void f( Allocator & alloc ) この問題はエイリアス名を使えば解決できる。 -~~~c++ +~~~cpp private : using traits = std::allocator_traits ; @@ -252,11 +252,11 @@ private : ### allocate/deallocate -`allocate(n)`はアロケーターから`n`個の要素を格納できる生のメモリの動的確保をして先頭要素へのポインターを返す。 +`allocate(n)`はアロケーターから`n`個の要素を格納できる生のメモリーの動的確保をして先頭要素へのポインターを返す。 `deallocate(ptr)`はポインター`ptr`を解放する。 -~~~c++ +~~~cpp private: pointer allocate( size_type n ) { return traits::allocate( alloc, n ) ; } @@ -266,11 +266,11 @@ private: ### construct/destroy -`construct(ptr)`は生のメモリへのポインター`ptr`に`vector`の`value_type`型の値をデフォルト構築する。 +`construct(ptr)`は生のメモリーへのポインター`ptr`に`vector`の`value_type`型の値をデフォルト構築する。 -`construct(ptr, value)`は生のメモリへのポインター`ptr`に値`value`のオブジェクトを構築する。 +`construct(ptr, value)`は生のメモリーへのポインター`ptr`に値`value`のオブジェクトを構築する。 -~~~c++ +~~~cpp void construct( pointer ptr ) { traits::construct( alloc, ptr ) ; } void construct( pointer ptr, const_reference value ) @@ -284,7 +284,7 @@ private: `destroy(ptr)`は`ptr`の指すオブジェクトを破棄する。 -~~~c++ +~~~cpp private : void destroy( pointer ptr ) { traits::destroy( alloc, ptr ) ; } @@ -294,7 +294,7 @@ private : `destroy_until(rend)`は、`vector`が保持する`rbegin()`からリバースイテレーター`rend`までの要素を破棄する。リバースイテレーターを使うので、要素の末尾から先頭に向けて順番に破棄される。なぜ末尾から先頭に向けて要素を破棄するかというと、C++では値の破棄は構築の逆順で行われるという原則があるからだ。 -~~~c++ +~~~cpp private : void destroy_until( reverse_iterator rend ) { @@ -313,14 +313,14 @@ private : `clear()`はすべての要素を破棄する。 -~~~c++ +~~~cpp void clear() noexcept { destroy_until( rend() ) ; } ~~~ -先程実装した`destroy_until(rend)`にリバースイテレーターの終端を渡せばすべての要素が破棄される。 +先ほど実装した`destroy_until(rend)`にリバースイテレーターの終端を渡せばすべての要素が破棄される。 ## デストラクター @@ -329,16 +329,16 @@ void clear() noexcept `std::vector`のデストラクターは、 1. 要素を末尾から先頭に向かう順番で破棄 -2. 生のメモリを解放する +2. 生のメモリーを解放する この2つの処理はすでに実装した。デストラクターの実装は単にヘルパー関数を並べて呼び出すだけでよい。 -~~~c++ +~~~cpp ~vector() { // 1. 要素を末尾から先頭に向かう順番で破棄 clear() ; - // 2. 生のメモリを解放する + // 2. 生のメモリーを解放する deallocate() ; } ~~~ @@ -346,7 +346,7 @@ void clear() noexcept ## reserveの実装 -`reserve`の実装は生の動的メモリを確保してデータメンバーを適切に設定する。 +`reserve`の実装は生の動的メモリーを確保してデータメンバーを適切に設定する。 ただし、いろいろと考慮すべきことが多い。 @@ -366,18 +366,18 @@ int main() すでに指定された要素数以上に予約されているからだ。 -動的メモリ確保が行われていない場合、単に動的メモリ確保をすればよい。 +動的メモリー確保が行われていない場合、単に動的メモリー確保をすればよい。 ~~~cpp int main() { std::vector v ; - // おそらく動的メモリ確保 + // おそらく動的メモリー確保 v.reserve( 10000 ) ; } ~~~ -「おそらく」というのは、C++の規格は`vector`のデフォルトコンストラクターが予約するストレージについて何も言及していないからだ。すでに要素数10000を超えるストレージが予約されている実装も規格準拠だ。本書で実装している`vector`は、デフォルトコンストラクターでは動的メモリ確保をしない実装になっている。 +「おそらく」というのは、C++の規格は`vector`のデフォルトコンストラクターが予約するストレージについて何も言及していないからだ。すでに要素数10000を超えるストレージが予約されている実装も規格準拠だ。本書で実装している`vector`は、デフォルトコンストラクターでは動的メモリー確保をしない実装になっている。 有効な要素が存在する場合、その要素の値は引き継がなければならない。 @@ -387,25 +387,25 @@ int main() { // 要素数3 std::vector v = {1,2,3} ; - // 1万個の要素を保持できるだけのメモリを予約 + // 1万個の要素を保持できるだけのメモリーを予約 v.reserve( 10000 ) ; // vは{1,2,3} } ~~~ -つまり動的メモリ確保をした後に、既存の要素を新しいストレージにコピーしなければならないということだ。 +つまり動的メモリー確保をしたあとに、既存の要素を新しいストレージにコピーしなければならないということだ。 まとめよう。 -1. すでに指定された要素数以上に予約されているなら何もしない -2. まだ動的メモリ確保が行われていなければ動的メモリ確保をする +1. すでに指定された要素数以上に予約されているなら何もしない。 +2. まだ動的メモリー確保が行われていなければ動的メモリー確保をする。 3. 有効な要素がある場合は新しいストレージにコピーする。 古いストレージから新しいストレージに要素をコピーするとき、古いストレージと新しいストレージが一時的に同時に存在しなければならない。 疑似コード風に記述すると以下のようになる。 -~~~c++ +~~~cpp template < typename T > void f() { @@ -423,7 +423,7 @@ void f() このとき、`T`型がコピーの最中に例外を投げると、後続の`delete`が実行されなくなる。この問題に対処して例外安全にするために、C++20に入る見込みの標準ライブラリ、`std::scope_exit`を使う。 -~~~c++ +~~~cpp template < typename T > void f() { @@ -445,14 +445,14 @@ void f() これを踏まえて`reserve`を実装する。 -~~~c++ +~~~cpp void reserve( size_type sz ) { // すでに指定された要素数以上に予約されているなら何もしない if ( sz <= capacity() ) return ; - // 動的メモリ確保をする + // 動的メモリー確保をする auto ptr = allocate( sz ) ; // 古いストレージの情報を保存 @@ -560,7 +560,7 @@ int main() このプログラムを実行すると、以下のように出力される。 -~~~c++ +~~~ destructed. destructed. destructed. @@ -584,13 +584,13 @@ int main() まとめると`resize`は以下のように動作する。 -1. 現在の要素数より少なくリサイズする場合、末尾から要素を破棄する -2. 現在の要素数より大きくリサイズする場合、末尾に要素を追加する +1. 現在の要素数より少なくリサイズする場合、末尾から要素を破棄する。 +2. 現在の要素数より大きくリサイズする場合、末尾に要素を追加する。 3. 現在の要素数と等しくリサイズする場合、何もしない。 実装しよう。 -~~~c++ +~~~cpp void resize( size_type sz ) { // 現在の要素数より少ない @@ -614,13 +614,13 @@ int main() 要素を破棄する場合、破棄する要素数だけ末尾から順番に破棄する。 -要素を増やす場合、`reserve`を呼び出してメモリを予約してから、追加の要素を構築する。 +要素を増やす場合、`reserve`を呼び出してメモリーを予約してから、追加の要素を構築する。 -`sz == size()`の場合は、どちらのif文の条件にも引っかからないので、何もしない。 +`sz == size()`の場合は、どちらの`if`文の条件にも引っかからないので、何もしない。 `size(sz, value)`は、追加の引数を取るほか、`construct( iter )`の部分が`constrcut( iter, value )`に変わるだけだ。 -~~~c++ +~~~cpp void resize( size_type sz, const_reference value ) { // ... @@ -631,7 +631,7 @@ void resize( size_type sz, const_reference value ) これで自作の`vector`はある程度使えるようになった。コンストラクターで要素数を指定できるし、リサイズもできる。 -~~~c++ +~~~cpp int main() { vector v(10, 1) ; @@ -657,18 +657,18 @@ int main() } ~~~ -`push_back`の実装は、末尾の予約された未使用のストレージに値を構築する。もし予約された未使用のストレージがない場合は、新しく動的メモリ確保する。 +`push_back`の実装は、末尾の予約された未使用のストレージに値を構築する。もし予約された未使用のストレージがない場合は、新しく動的メモリー確保する。 -追加の動的メモリ確保なしで保持できる要素の個数はすでに実装した`capacity()`で取得できる。`push_back`は要素をひとつ追加するので、`size() + 1 <= capacity()`ならば追加の動的メモリ確保はいらない。逆に、`size() + 1 > capacity()`ならば追加の動的メモリ確保をしなければならない。追加の動的メモリ確保はすでに実装した`reserve`を使えばよい。 +追加の動的メモリー確保なしで保持できる要素の個数はすでに実装した`capacity()`で取得できる。`push_back`は要素を1つ追加するので、`size() + 1 <= capacity()`ならば追加の動的メモリー確保はいらない。逆に、`size() + 1 > capacity()`ならば追加の動的メモリー確保をしなければならない。追加の動的メモリー確保はすでに実装した`reserve`を使えばよい。 -~~~c++ +~~~cpp void push_back( const_reference value ) { - // 予約メモリが足りなければ拡張 + // 予約メモリーが足りなければ拡張 if ( size() + 1 > capacity() ) { - // ひとつだけ増やす + // 1つだけ増やす reserve( size() + 1 ) ; } @@ -681,30 +681,30 @@ void push_back( const_reference value ) これは動く。ただし、効率的ではない。自作の`vector`を使った以下のような例を見てみよう。 -~~~c++ +~~~cpp int main() { // 要素数10000 vector v(1000) ; - // 10001個分のメモリを確保する + // 10001個分のメモリーを確保する // 10000個の既存の要素をコピーする v.push_back(0) ; - // 10002個分のメモリを確保する + // 10002個分のメモリーを確保する // 10001個の既存の要素をコピーする v.push_back(0) ; } ~~~ -たった1つの要素を追加するのに、毎回動的メモリ確保と既存の全要素のコピーをしている。これは無駄だ。 +たった1つの要素を追加するのに、毎回動的メモリー確保と既存の全要素のコピーをしている。これは無駄だ。 -`std::vector`は`push_back`で動的メモリ確保が必要な場合、`size()+1`よりも多くメモリを確保する。こうすると、`push_back`を呼び出すたびに毎回動的メモリ確保と全要素のコピーを行う必要がなくなるので、効率的になる。 +`std::vector`は`push_back`で動的メモリー確保が必要な場合、`size()+1`よりも多くメモリーを確保する。こうすると、`push_back`を呼び出すたびに毎回動的メモリー確保と全要素のコピーを行う必要がなくなるので、効率的になる。 ではどのくらい増やせばいいのか。10個ずつ増やす戦略は以下のようになる。 ~~~cpp void push_back( const_reference value ) { - // 予約メモリが足りなければ拡張 + // 予約メモリーが足りなければ拡張 if ( size() + 1 > capacity() ) { // 10個増やす @@ -728,7 +728,7 @@ int main() } ~~~ -10個ずつ増やす戦略では、この場合に1000回の動的メモリ確保と全要素のコピーが発生する。 +10個ずつ増やす戦略では、この場合に1000回の動的メモリー確保と全要素のコピーが発生する。 上のような場合、`vector`の利用者が事前に`v.reserve(10000)`とすれば効率的になる。しかし、コンパイル時に要素数がわからない場合、その手も使えない。 @@ -737,7 +737,7 @@ int main() { std::vector inputs ; // 要素数は実行時にしかわからない - // 10万個の入力が行われるかも知れない + // 10万個の入力が行われるかもしれない std::copy( std::ostream_iterator(std::cin), std::ostream_iterator(), @@ -750,7 +750,7 @@ int main() ~~~cpp void push_back( const_reference value ) { - // 予約メモリが足りなければ拡張 + // 予約メモリーが足りなければ拡張 if ( size() + 1 > capacity() ) { // 現在のストレージサイズ @@ -773,11 +773,11 @@ void push_back( const_reference value ) ### `shrink_to_fit` -`shrink_to_fit()`は`vector`が予約しているメモリのサイズを実サイズに近づけるメンバー関数だ。 +`shrink_to_fit()`は`vector`が予約しているメモリーのサイズを実サイズに近づけるメンバー関数だ。 -本書で実装してきた自作の`vector`は、`push_back`時に予約しているメモリがなければ、現在の要素数の2倍のメモリを予約する実装だった。すると以下のようなコードで、 +本書で実装してきた自作の`vector`は、`push_back`時に予約しているメモリーがなければ、現在の要素数の2倍のメモリーを予約する実装だった。すると以下のようなコードで、 -~~~c++ +~~~cpp int main() { vector v ; @@ -786,18 +786,18 @@ int main() } ~~~ -ユーザーが4万個の`int`型の値を入力した場合、65536個の`int`型の値を保持できるだけのメモリが確保されてしまい、差し引き`sizeof(int) * 25536`バイトのメモリが未使用のまま確保され続けてしまう。 +ユーザーが4万個の`int`型の値を入力した場合、65536個の`int`型の値を保持できるだけのメモリーが確保されてしまい、差し引き`sizeof(int) * 25536`バイトのメモリーが未使用のまま確保され続けてしまう。 -メモリ要件の厳しい環境ではこのようなメモリの浪費を避けたい。しかし、実行時にユーザーから任意の個数の入力を受けるプログラムを書く場合には、`push_back`を使いたい。 +メモリー要件の厳しい環境ではこのようなメモリーの浪費を避けたい。しかし、実行時にユーザーから任意の個数の入力を受けるプログラムを書く場合には、`push_back`を使いたい。 -こういうとき、`shrink_to_fit`は`vector`が予約するメモリを切り詰めて実サイズに近くする、かもしれない。「かもしれない」というのは、C++の標準規格は`shrink_to_fit`が必ずメモリの予約サイズを切り詰めるよう規定してはいないからだ。 +こういうとき、`shrink_to_fit`は`vector`が予約するメモリーを切り詰めて実サイズに近くする、かもしれない。「かもしれない」というのは、C++の標準規格は`shrink_to_fit`が必ずメモリーの予約サイズを切り詰めるよう規定してはいないからだ。 自作の`vector`では必ず切り詰める実装にしてみよう。 -まず予約するメモリを切り詰めるとはどういうことか。現在予約しているメモリで保持できる最大の要素数は`capacity()`で得られる。実際に保持している要素数を返すのは`size()`だ。すると`size() == capacity()`になるようにすればいい。 +まず予約するメモリーを切り詰めるとはどういうことか。現在予約しているメモリーで保持できる最大の要素数は`capacity()`で得られる。実際に保持している要素数を返すのは`size()`だ。すると`size() == capacity()`になるようにすればいい。 -~~~c++ +~~~cpp vector v ; // ... v.shrink_to_fit() ; @@ -806,9 +806,9 @@ v.size() == v.capacity() ; // trueにする `shrink_to_fit()`を呼んだとき、すでに`size() == capacity()`が`true`である場合は、何もしなくてもよい。 -それ以外の場合は、現在の有効な要素数分の新しいストレージを確保し、現在の値を新しいストレージにコピーし、古いメモリは破棄する。 +それ以外の場合は、現在の有効な要素数分の新しいストレージを確保し、現在の値を新しいストレージにコピーし、古いメモリーは破棄する。 -~~~c++ +~~~cpp void shrink_to_fit() { // 何もする必要がない diff --git a/035-copy.md b/035-copy.md index 9efc8fd..da1d42e 100644 --- a/035-copy.md +++ b/035-copy.md @@ -4,12 +4,12 @@ ## 普通のコピー -C++を書くユーザーは、クラス型のオブジェクトを使うとき、クラスが普通の型(regular type)のように振る舞うことを期待している。この普通には様々な意味がある。 +C++を書くユーザーは、クラス型のオブジェクトを使うとき、クラスが普通の型(regular type)のように振る舞うことを期待している。この普通にはさまざまな意味がある。 `int`型の変数をコピーするとき、コピー先の変数はコピー元の変数と等しくなる。 -~~~c++ +~~~cpp int source = 42 ; int destination = source ; ~~~ @@ -20,7 +20,7 @@ int destination = source ; 我々が普通にコピーと認識しているものは、C++の文法的にはコピー構築とコピー代入に分けることができる。 -~~~c++ +~~~cpp T source ; // コピー構築 T a = source ; @@ -47,7 +47,7 @@ int main() } ~~~ -これはまず`z = 0`が評価される。変数`z`の値は`0`になり、式を評価した結果の値は`z`へのlvalueリファレンスだ。なので、`y = z = 0`というのは、`y = (z=0)`となる。`z=0`については`z`であるので、`y = z`となる。ここでの`z`は`0`を代入された後の`z`なので、値は`0`だ。その結果変数`y`の値は`0`になる。変数`x`の場合も同様だ。 +これはまず`z = 0`が評価される。変数`z`の値は`0`になり、式を評価した結果の値は`z`へのlvalueリファレンスだ。なので、`y = z = 0`というのは、`y = (z=0)`となる。`z=0`については`z`であるので、`y = z`となる。ここでの`z`は`0`を代入されたあとの`z`なので、値は`0`だ。その結果変数`y`の値は`0`になる。変数`x`の場合も同様だ。 以下のような例も見てみよう。 @@ -59,13 +59,13 @@ int main() } ~~~ -これは`(x = 0)`の結果に`1`を代入している。`x=0`の結果は`x`なので、`x`には`0`が代入された後に`1`が代入される。結果として`x`の値は`1`になる。 +これは`(x = 0)`の結果に`1`を代入している。`x=0`の結果は`x`なので、`x`には`0`が代入されたあとに`1`が代入される。結果として`x`の値は`1`になる。 ## コピーコンストラクター コピー構築の場合、コピーコンストラクターが呼ばれる。 -~~~c++ +~~~cpp struct Value { // コピーコンストラクター @@ -83,7 +83,7 @@ int main() } ~~~ -コピーコンストラクターは`クラス型へのlvalueリファレンス型`を引数にとる`コンストラクター`だ。 +コピーコンストラクターは`クラス型へのlvalueリファレンス型`を引数に取る`コンストラクター`だ。 ~~~cpp struct X @@ -146,7 +146,7 @@ struct X コピー代入演算子の戻り値の型はクラス型への非`const`な`lvalue`リファレンスでなくてもよい。ただし、その場合もユーザーの期待にそぐわないことになる。 -~~~c++ +~~~cpp struct X { void operator = ( const X & source ) { } @@ -189,7 +189,7 @@ int main() 上記のコードは、以下のように書いたのと同じだ。 -~~~c++ +~~~cpp Point b{ a.x, a.y, a.z } ; Point c ; c.x = a.x ; @@ -234,7 +234,7 @@ int main() 自作の`vector`のコピーはどのように実装すればいいだろうか。デフォルトのコピーに任せてもいいのだろうか。デフォルトのコピーを使う場合、コピーコンストラクターは以下のように書いたものと同じだ。 -~~~c++ +~~~cpp template < typename T, typename Allocator = std::allocator > class vector { @@ -257,7 +257,7 @@ public : これは問題だ。以下のコードを考える。 -~~~c++ +~~~cpp int main() { vector v{1} ; @@ -278,7 +278,7 @@ int main() する。 -次に`v`が破棄される。`v`のデストラクターは`w`のデストラクターと全く同じことをする。ただし、ポインター`first`の指すオブジェクトはすでにデストラクターが呼び出されているし、ポインター`first`の指す生のストレージも解放されている。 +次に`v`が破棄される。`v`のデストラクターは`w`のデストラクターとまったく同じことをする。ただし、ポインター`first`の指すオブジェクトはすでにデストラクターが呼び出されているし、ポインター`first`の指す生のストレージも解放されている。 すでにデストラクターを呼び出したオブジェクトに対してもう一度デストラクターを呼び出した場合の挙動は未定義だ。すでに解放したストレージを指すポインターに対してもう一度ストレージの解放した場合の挙動は未定義だ。したがって、このプログラムの挙動は未定義となる。 @@ -293,7 +293,7 @@ int main() } ~~~ -変数`w`はまず要素を保持するためのメモリを動的確保する。その後、`w`に`v`が代入されるわけだが、このとき`w`が動的確保したメモリを指すポインターの値が上書きされてしまう。`w`が破棄されるとき、`w`が元々持っていた要素は破棄されなくなり、ストレージも解放されなくなる。 +変数`w`はまず要素を保持するためのメモリーを動的確保する。その後、`w`に`v`が代入されるわけだが、このとき`w`が動的確保したメモリーを指すポインターの値が上書きされてしまう。`w`が破棄されるとき、`w`がもともと持っていた要素は破棄されなくなり、ストレージも解放されなくなる。 ## 所有するクラス @@ -302,7 +302,7 @@ int main() 問題を簡単にするために、以下のようなクラスを考えよう。 -~~~ +~~~cpp template < typename T > class own { @@ -320,7 +320,7 @@ public : } ; ~~~ -このクラスはコンストラクターでテンプレートパラメーター`T`型のオブジェクトを動的メモリ確保をし、デストラクターでメモリの解放をする。 +このクラスはコンストラクターでテンプレートパラメーター`T`型のオブジェクトを動的メモリー確保をし、デストラクターでメモリーの解放をする。 コピーコンストラクターとコピー代入演算子は定義していないので、デフォルトのコピーが使われる。 @@ -342,19 +342,19 @@ public : この場合、クラスはポインターの参照するオブジェクトを所有していると考えることができる。ポインターの値をコピーするということは、所有権を共有するということだ。所有権を共有していることを考慮しないまま、クラスのオブジェクトが破棄されたときにポインターの参照先まで破棄してしまうと、所有したつもりになっているクラスのオブジェクトが出来上がってしまう。 -普通の型のように振る舞うコピーを実装するには、コピーの際に所有権を共有しない実装をする。具体的には、コピーのときに新しく動的メモリ確保し、値をコピーするのだ。 +普通の型のように振る舞うコピーを実装するには、コピーの際に所有権を共有しない実装をする。具体的には、コピーのときに新しく動的メモリー確保し、値をコピーするのだ。 コピーコンストラクターは以下のようになる。 -~~~c++ +~~~cpp own( const own & r ) : ptr( new T( *r.ptr ) ) { } ~~~ -今回の場合、コピー代入演算子で動的メモリ確保をする必要はない。なぜならば、コピー代入演算子が呼ばれたということは、いずれかのコンストラクターがすでに呼ばれていて、動的メモリ確保はされているからだ。 +今回の場合、コピー代入演算子で動的メモリー確保をする必要はない。なぜならば、コピー代入演算子が呼ばれたということは、いずれかのコンストラクターがすでに呼ばれていて、動的メモリー確保はされているからだ。 -~~~c++ +~~~cpp own & operator = ( const own & r ) { *ptr = *r.ptr ; @@ -364,7 +364,7 @@ own & operator = ( const own & r ) このコードには少し問題がある。変数は自分自身に代入ができるのだ。 -~~~c++ +~~~cpp // 1GBもの巨大なサイズのクラス struct one_giga_byte { std::byte storage[1'000'000'000] ; } @@ -412,7 +412,7 @@ int main() これと同じことを、`own`でやるにはどうすればいいのだろうか。つまり`own`から`own`への変換だ。 -~~~c++ +~~~cpp int main() { own a ; @@ -424,7 +424,7 @@ int main() 単に`own`からの変換だけであれば、`own`型から変換するコンストラクターを書けばよい。 -~~~c++ +~~~cpp template < typename T > class own { @@ -439,12 +439,11 @@ public : } ~~~ -このような自分自身以外の型の引数をひとつだけ取るコンストラクターのことを、`変換コンストラクター` -という。 +このような自分自身以外の型の引数を1つだけ取るコンストラクターのことを、`変換コンストラクター`という。 しかしこれでは`own`からの変換にしか対応できない。しかも`int`型から変換できない型を使うとエラーとなる。 -~~~c++ +~~~cpp // int型から変換できない型 struct I_hate_int { @@ -466,7 +465,7 @@ int main() この問題を解決するにはテンプレートを使う。 -~~~c++ +~~~cpp template < typename T > class own { @@ -485,7 +484,7 @@ public : しかし、上のクラス`I_hate_int`型は任意の型から変換できないので、この変換コンストラクターテンプレートの存在は問題にならならないのだろうか。心配御無用。テンプレートは具体的なテンプレート実引数が与えられて初めてコードが生成される。実際に使わない限りは問題にならない。 -~~~c++ +~~~cpp int main() { // 問題なし @@ -542,7 +541,7 @@ int main() コピーコンストラクターは簡単だ。コピー元と同じサイズの配列を動的確保し、要素をコピーすればいいだけだ。 -~~~c++ +~~~cpp dynamic_array( const dynamic_array & r ) : first( new T[r.size()]), last( first + r.size() ) { @@ -550,21 +549,21 @@ dynamic_array( const dynamic_array & r ) } ~~~ -コピー代入演算子でも、場合によっては動的メモリ確保が必要になる。現在所有しているメモリとは異なるサイズのオブジェクトからコピーする場合だ。 +コピー代入演算子でも、場合によっては動的メモリー確保が必要になる。現在所有しているメモリーとは異なるサイズのオブジェクトからコピーする場合だ。 ~~~cpp int main() { dynamic_array a(5) ; dynamic_array b(10) ; - // aの所有するメモリはサイズ不足 + // aの所有するメモリーはサイズ不足 a = b ; } ~~~ -コピー元よりコピー先のほうがメモリが多い場合、つまり`b = a`の場合は動的メモリ確保をしないという実装もできるが、その場合実際に確保したメモリサイズと、クラスが認識しているメモリサイズが異なることになる。今回はサイズが違う場合は必ず動的メモリ確保をすることにしよう。 +コピー元よりコピー先の方がメモリーが多い場合、つまり`b = a`の場合は動的メモリー確保をしないという実装もできるが、その場合実際に確保したメモリーサイズと、クラスが認識しているメモリーサイズが異なることになる。今回はサイズが違う場合は必ず動的メモリー確保をすることにしよう。 -~~~c++ +~~~cpp dynamic_array & operator == ( const dynamic_array & r ) { // 自分自身への代入ではない場合 @@ -578,16 +577,16 @@ dynamic_array & operator == ( const dynamic_array & r ) } ~~~ -`new`したメモリは`delete`しなければならない。そこで、コピー代入演算子はまず自分の所有するメモリを`delete`してから`new`し、値をコピーすることになる。 +`new`したメモリーは`delete`しなければならない。そこで、コピー代入演算子はまず自分の所有するメモリーを`delete`してから`new`し、値をコピーすることになる。 -~~~c++ +~~~cpp dynamic_array & operator == ( const dynamic_array & r ) { if ( this != &r && size() != r.size() ) { - // コピー先が所有しているメモリの解放 + // コピー先が所有しているメモリーの解放 delete first ; - // コピー元と同じサイズの動的メモリ確保 + // コピー元と同じサイズの動的メモリー確保 first = new T[r.size()] ; last = first + r.size() ; // コピー元の値をコピー @@ -605,7 +604,7 @@ dynamic_array & operator == ( const dynamic_array & r ) `std::vector`では、アロケーターのコピーだけがちょっと特殊になっている。コンテナーのコピーにあたってアロケーターをコピーすべきかどうかは、アロケーターの実装が選べるようになっている。このために、`std::allocator_traits::select_on_container_copy_construction(alloc)`を呼び出し、その戻り値でアロケーターを初期化する。`std::allocator_traits`という型については、すでに`traits`というエイリアスを宣言しているので、以下のようにする。 -~~~c++ +~~~cpp vector( const vector & r ) // アロケーターのコピー : alloc( traits::select_on_container_copy_construction(r.alloc) ) @@ -619,7 +618,7 @@ vector( const vector & r ) 1. コピー元の要素数を保持できるだけのストレージを確保 2. コピー元の要素をコピー構築 -~~~c++ +~~~cpp vector( const vector & r ) : alloc( traits::select_on_container_copy_construction( r.alloc ) ) { @@ -654,7 +653,7 @@ int main() これは単に以下のようなコードを実行したものと同じになる。 -~~~c++ +~~~cpp w[0] = v[0] ; w[1] = v[1] ; w[2] = v[2] ; @@ -699,11 +698,11 @@ int main() まとめよう。 1. 自分自身への代入であれば何もしない。 -2. 要素数が同じならば要素ごとにコピー代入 -3. それ以外の場合で、予約数が十分ならば有効な要素にはコピー代入、残りはコピー構築 -4. それ以外の場合で、予約数が不十分ならば、現在の要素は全て破棄して新たなストレージを確保してコピー構築 +2. 要素数が同じならば要素ごとにコピー代入。 +3. それ以外の場合で、予約数が十分ならば有効な要素にはコピー代入、残りはコピー構築。 +4. それ以外の場合で、予約数が不十分ならば、現在の要素はすべて破棄して新たなストレージを確保してコピー構築。 -~~~c++ +~~~cpp vector & operator = ( const vector & r ) { // 1. 自分自身への代入なら何もしない diff --git a/036-move.md b/036-move.md index fb6417c..e7bb5c1 100644 --- a/036-move.md +++ b/036-move.md @@ -6,7 +6,7 @@ コピーの仕方を振り返ってみよう。コピーにはコピー構築とコピー代入がある。 -~~~c++ +~~~cpp T source ; // コピー構築 T a = source ; @@ -20,7 +20,7 @@ e = source ; コピーにはコピー先とコピー元がある。 -~~~c++ +~~~cpp std::vector v = {1,2,3} ; std::vector destination = source ; // destinationは{1,2,3} @@ -31,7 +31,7 @@ std::vector destination = source ; ムーブはコピーと似ている。コピーをするときに、ムーブ元の変数を`source`を`std::move(source)`のように標準ライブラリ`std::move`に渡してその戻り値をコピー元の値とすることでムーブになる。ムーブにもコピーと同様にムーブ構築とムーブ代入がある。 -~~~c++ +~~~cpp T source ; // ムーブ構築 T a = std::move(source) ; @@ -45,7 +45,7 @@ e = std::move(source) ; ムーブにもムーブ先とムーブ元がある。 -~~~c++ +~~~cpp std::vector v = {1,2,3} ; // destinationはムーブ先 // sourceはムーブ元 @@ -58,7 +58,7 @@ std::vector destination = std::move(source) ; ムーブ後のムーブ元の値はわからない。なぜわからないかというと、値を移動しているからだ。 -ムーブのコストはコピーと全く同じか、コピーよりも低くなる。 +ムーブのコストはコピーとまったく同じか、コピーよりも低くなる。 ムーブはムーブ元の値をムーブ後に使わない場合に、コピーの代わりに使うことができる。 @@ -81,7 +81,7 @@ int main() 実際には、上記のコードはムーブ後に変数`w`を使っている。`main`関数のスコープを抜けるときに`w`が破棄されるが、そのときにデストラクターが実行される。 -C++の標準ライブラリーはムーブ後の状態について、その値は「妥当だが未規定の状態」になる。 +C++の標準ライブラリはムーブ後の状態について、その値は「妥当だが未規定の状態」になる。 なのでこの場合でもデストラクターを正常に呼び出すことはできる。このとき、`w.size()`が返す値はわからない。ただし、`w.resize(n)`を呼び出すと`n`個の要素を持つようになる。この結果、再び使うこともできるようになる。 @@ -100,11 +100,11 @@ int main() ## ムーブの中身 -ムーブは一体何をしているのか。ムーブの実装方法を理解するためには、`rvalueセマンティクス`と`値カテゴリー`とテンプレートの`フォワードリファレンス`という難しいC++の機能を理解しなければならない。この機能は次の章から解説するが、その機能を学ぶ動機づけにムーブが何をしているのかを具体的に学ぼう。 +ムーブはいったい何をしているのか。ムーブの実装方法を理解するためには、`rvalueセマンティクス`と`値カテゴリー`とテンプレートの`フォワードリファレンス`という難しいC++の機能を理解しなければならない。この機能は次の章から解説するが、その機能を学ぶ動機づけにムーブが何をしているのかを具体的に学ぼう。 `int`や`double`といった単なるバイト列で表現された値だけで表現できる基本型のオブジェクトの場合、ムーブというのはコピーと何ら変わらない。単に値を表現するバイト列をコピーするだけだ。 -~~~c++ +~~~cpp int a = 0 ; // コピー int b = a ; @@ -115,7 +115,7 @@ int c = std::move(a) ; そのため、`int`や`double`のムーブでは、ムーブ後もムーブ元のオブジェクトをそのまま使うことができるし、値も変わらない。 -~~~c++ +~~~cpp int a = 123 int b = std::move(a) ; // 123 @@ -125,7 +125,7 @@ a = 456 ; 生のポインターのムーブもコピーと同じだ。 -~~~c++ +~~~cpp int object { } ; int * source = object ; // 中身は単なるコピー @@ -155,7 +155,7 @@ int main() 以下のように書いたものとほぼ同じになる。 -~~~c++ +~~~cpp int main() { X a{1,2,3} ; @@ -168,7 +168,7 @@ int main() この場合のムーブは単なるコピーなので、実際には以下のように書くのと同じだ。 -~~~c++ +~~~cpp int main() { X a{1,2,3} ; @@ -184,11 +184,11 @@ C++の基本型とクラスのデフォルトのムーブの実装は、単な ムーブ後のオブジェクトは使えない状態になるということは、ムーブ後のオブジェクトの値はどうなってもいいということだ。 -`std::vector`のようなクラスは動的メモリ確保をしてポインターでストレージを参照している。自作のvectorにコピーを実装するときは、コピー先でも動的メモリ確保をして要素を1つずつコピーしなければならないことを学んだ。 +`std::vector`のようなクラスは動的メモリー確保をしてポインターでストレージを参照している。自作のvectorにコピーを実装するときは、コピー先でも動的メモリー確保をして要素を1つずつコピーしなければならないことを学んだ。 とても簡単な、`T`型の配列を確保する`dynamic_array`を考えてみよう。 -~~~c++ +~~~cpp template < typename T > class dynamic_array { @@ -209,7 +209,7 @@ public : このクラスのコピーコンストラクターの定義は以下のように書ける。 -~~~c++ +~~~cpp template < typename T > dynamic_array::dynamic_array( const dynamic_array & r ) : first( new T[r.size()] ), last( first + r.size() ) @@ -220,7 +220,7 @@ dynamic_array::dynamic_array( const dynamic_array & r ) これはコストがかかる。以下のようにすればコストがかからないがなぜできないのだろう。 -~~~c++ +~~~cpp < typename T > dynamic_array::dynamic_array( const dynamic_array & r ) : first( r.first ), last( r.last ) @@ -231,7 +231,7 @@ dynamic_array::dynamic_array( const dynamic_array & r ) コピーの章でも学んだように、この実装ではコピー先とコピー元が同じポインターを所有してしまうために、デストラクターが実行されるときに同じポインターが2回`delete`されてしまう。 -~~~c++ +~~~cpp int main() { dynamic_array source(10) ; @@ -244,7 +244,7 @@ int main() ならば、コピー元からポインターの所有権を奪ってしまえばいいのではないだろうか。 -~~~c++ +~~~cpp < typename T > dynamic_array::dynamic_array( dynamic_array & r ) : first( r.first ), last( r.last ) @@ -255,7 +255,7 @@ dynamic_array::dynamic_array( dynamic_array & r ) } ~~~ -引数がconstではないことに注目しよう。リファレンス型の引数を変更するには、constにはできない。 +引数が`const`ではないことに注目しよう。リファレンス型の引数を変更するには、`const`にはできない。 このコピーコンストラクターはコピー元を変更する。`delete式`は`nullptr`に対して適用した場合、何もしないことが保証されている。そのため、この場合にデストラクターでnullポインターのチェックは必要がない。 @@ -276,7 +276,7 @@ int main() C++ではコピーはコピー元を変更しないという慣習がある。このような慣習はすべてC++の標準規格で定められている。 -このため、C++はコピーの他にムーブを定めている。ムーブを使うにはムーブ元の変数`x`を`std::move(x)`のようにしてコピーする。`std::move`はこのコピーはコピーではなくムーブしてもよいというヒントになる。 +このため、C++はコピーのほかにムーブを定めている。ムーブを使うにはムーブ元の変数`x`を`std::move(x)`のようにしてコピーする。`std::move`はこのコピーはコピーではなくムーブしてもよいというヒントになる。 ムーブを実装するためには、まず基礎知識として次の章で学ぶr`value`リファレンス、値カテゴリー、テンプレートのフォワードリファレンスの深い理解が必要になる。 diff --git a/037-rvalue-reference.md b/037-rvalue-reference.md index eebe77a..f8a126d 100644 --- a/037-rvalue-reference.md +++ b/037-rvalue-reference.md @@ -2,7 +2,7 @@ ## 概要 -今まで使っているリファレンスは、正式には`lvalue`リファレンスという名前がついている。これは`lvalue`へのリファレンスという意味だ。`lvalue`へのリファレンスがあるからには、`lvalue`ではないリファレンスがあるということだ。C++には`rvalue`へのリファレンスがある。これを`rvalue`リファレンスという。 +いままで使っているリファレンスは、正式には`lvalue`リファレンスという名前がついている。これは`lvalue`へのリファレンスという意味だ。`lvalue`へのリファレンスがあるからには、`lvalue`ではないリファレンスがあるということだ。C++には`rvalue`へのリファレンスがある。これを`rvalue`リファレンスという。 この章で説明する内容はとても難しい。完全に理解するためには、何度も読み直す必要があるだろう。 @@ -10,13 +10,13 @@ `T`型への`lvalue`型リファレンス型は`T &`と書く。 -~~~c++ +~~~cpp T & lvalue_reference = ... ; ~~~ `T`型への`rvalue`リファレンス型は`T &&`と書く。 -~~~c++ +~~~cpp T && rvalue_reference = ... ; ~~~ @@ -38,7 +38,7 @@ int main() ここで、式`object`や式`f()`を評価した結果は`lvalue`だ。 -`rvalue`とは、名前無しのオブジェクトや計算結果の一時オブジェクト、戻り値の型としての`rvalue`リファレンスのことだ。 +`rvalue`とは、名前なしのオブジェクトや計算結果の一時オブジェクト、戻り値の型としての`rvalue`リファレンスのことだ。 ~~~cpp int && g() { return 0 ; } @@ -59,7 +59,7 @@ int main() `rvalue`リファレンスを`lvalue`で初期化することはできない。 -~~~c++ +~~~cpp int object { } ; int & f() { return object ; } @@ -73,7 +73,7 @@ int main() `lvalue`リファレンスを`rvalue`で初期化することはできない。 -~~~c++ +~~~cpp int && g() { return 0 ; } int h() { return 0 ; } @@ -103,9 +103,9 @@ int main() } ~~~ -`rvalue`リファレンス自体は`lvalue`だ。なぜならば`rvalue`リファレンスはオブジェクトに名前をつけて束縛するからだ。 +`rvalue`リファレンス自体は`lvalue`だ。なぜならば`rvalue`リファレンスはオブジェクトに名前を付けて束縛するからだ。 -~~~c++ +~~~cpp int main() { // rvalueリファレンス @@ -119,15 +119,15 @@ int main() ## 値カテゴリー -`lvalue`と`rvalue`とは何か。元々`lvalue`とは左辺値(left-hand value)、`rvalue`とは右辺値(right-hand value)という語源を持っている。これはまだC言語すらなかったはるか昔から存在する用語で、代入式の左辺に書くことができる値を`lvalue`、右辺に書くことができる値を`rvalue`と読んでいたことに由来する。 +`lvalue`と`rvalue`とは何か。もともと`lvalue`とは左辺値(left-hand value)、`rvalue`とは右辺値(right-hand value)という語源を持っている。これはまだC言語すらなかったはるか昔から存在する用語で、代入式の左辺に書くことができる値を`lvalue`、右辺に書くことができる値を`rvalue`と読んでいたことに由来する。 -~~~ +~~~cpp lvalue = rvalue ; ~~~ 例えば、`int`型の変数`x`は代入式の左辺に書くことができるから`lvalue`、整数リテラル`0`は右辺に書くことができるから`rvalue`といった具合だ。 -~~~c++ +~~~cpp int x ; x = 0 ; ~~~ @@ -136,41 +136,41 @@ C++では`lvalue`と`rvalue`をこのような意味では使っていない。 `lvalue`と`rvalue`を理解するには、値カテゴリーを理解しなければならない。 -1. 式(expression)とは`glvalue`か`rvalue`である -2. `glvalue`とは`lvalue`か`xvalue`である -3. `rvalue`とは`prvalue`か`xvalue`である +1. 式(expression)とは`glvalue`か`rvalue`である。 +2. `glvalue`とは`lvalue`か`xvalue`である。 +3. `rvalue`とは`prvalue`か`xvalue`である。 この関係を図示すると以下のようになる。 - - +~~~ TODO: 図示 -~~~ expression / \ glvalue rvalue / \ / \ lvalue xvalue prvalue ~~~ +fig/fig37-01.png + ### lvalue `lvalue`はすでに説明したとおり名前付きのオブジェクトのことだ。 -~~~c++ +~~~cpp // lvalue int object ; int & ref = object ; ~~~ -通常使う殆どのオブジェクトは`lvalue`になる。 +通常使うほとんどのオブジェクトは`lvalue`になる。 ### prvalue -`prvalue`は純粋なrvalue(pure rvalue)のことだ。つまり、名前無しのオブジェクトや計算結果の一時オブジェクトのことだ。 +`prvalue`は純粋なrvalue(pure rvalue)のことだ。つまり、名前なしのオブジェクトや計算結果の一時オブジェクトのことだ。 -~~~c++ +~~~cpp int f() { return 0 ; } // prvalue @@ -183,20 +183,20 @@ f() ; 関数の戻り値の型がリファレンスではない場合、一時オブジェクトが生成される。 -~~~c++ +~~~cpp struct X { } ; X f() ; ~~~ 演算子も関数の一種なので、 -~~~c++ +~~~cpp auto result = x + y + z ; ~~~ のような式がある場合、まず`x + y`が評価され、その結果が一時オブジェクトとして返される。その一時オブジェクトを仮に`temp`とすると、`temp + z`が評価され、また一時オブジェクトが生成され、変数`result`に代入される。 -式文全体を評価し終わった後に、一時オブジェクトは自動的に破棄される。 +式文全体を評価し終わったあとに、一時オブジェクトは自動的に破棄される。 一時オブジェクトは自動的に生成され、自動的に破棄される。ここがとても重要な点だ。これは次の章で説明するムーブセマンティクスに関わってくる。 @@ -231,7 +231,7 @@ int main() + `xvalue`配列への添字操作 -~~~c++ +~~~cpp int main() { int a[3] = {1,2,3} ; @@ -243,7 +243,7 @@ int main() + `xvalue`なクラスのオブジェクトへのリファレンスではない非`static`データメンバーへのアクセス -~~~c++ +~~~cpp struct X { int data_member ; } ; int main() @@ -255,7 +255,7 @@ int main() + 式`.*`で最初のオペランドが`xvalue`で次のオペランドがデータメンバーへのポインターの場合 -~~~c++ +~~~cpp struct X { int data_member ; } ; int main() @@ -275,7 +275,7 @@ int main() `lvalue`は`xvalue`に変換できるので、結果として`rvalue`に変換できることになる。 -~~~c++ +~~~cpp int main() { // lvalueなオブジェクト @@ -345,7 +345,7 @@ T && move( T & t ) noexcept この実装は`lvalue`を`xvalue`に変換することはできるが、`rvalue`(`prvalue`と`xvalue`)を`xvalue`に変換することはできない。 -~~~c++ +~~~cpp int main() { // エラー、prvalueを変換できない @@ -357,7 +357,7 @@ int main() } ~~~ -`rvalue`は`rvalue`リファレンスで受け取れるので、`lvalue`リファレンスを関数の引数として受け取る`move`の他に、`rvalue`リファレンスを関数の引数として受け取る`move`を書くとよい。 +`rvalue`は`rvalue`リファレンスで受け取れるので、`lvalue`リファレンスを関数の引数として受け取る`move`のほかに、`rvalue`リファレンスを関数の引数として受け取る`move`を書くとよい。 すると以下のように書けるだろうか。 @@ -391,14 +391,14 @@ void f( T && t ) ; このような関数テンプレートの仮引数`t`に実引数として`rvalue`を渡すと、`T`は`rvalue`の型となり、結果として`t`の型は`T &&`になる。 -~~~c++ +~~~cpp // Tはint f(0) ; ~~~ もし実引数として型`U`の`lvalue`を渡すと、テンプレートパラメーター`T`が`U &`となる。そして、テンプレートパラメーター`T`に対するリファレンス宣言子(`&`, `&&`)は単に無視される。 -~~~c++ +~~~cpp int lvalue{} ; // Tはint & // T &&はint & @@ -427,11 +427,11 @@ int main() `f(0)`は`prvalue`を渡している。この場合、`T`の型は`int`となる。`A`は`int &`、`B`は`int &&`となる。 -`f(lvalue)`は`lvalue`を渡している。この場合、`T`の型は`int &`となる。この場合の`T`に`&`や`&&`をつけても無視される。なので、`A`, `B`の型はどちらも`int &`になる。 +`f(lvalue)`は`lvalue`を渡している。この場合、`T`の型は`int &`となる。この場合の`T`に`&`や`&&`を付けても無視される。なので、`A`, `B`の型はどちらも`int &`になる。 したがって、以下のように書くと`move`は`lvalue`も`rvalue`も受け取ることができる。 -~~~c++ +~~~cpp // lvalueもrvalueも受け取ることができるmove template < typename T > T && move( T && t ) noexcept @@ -442,15 +442,15 @@ T && move( T && t ) noexcept ただし、この実装にはまだ問題がある。この`move`に`lvalue`を渡した場合、`lvalue`の型を`U`とすると、テンプレートパラメーター`T`は`U &`になる。 -~~~c++ +~~~cpp U lvalue{} ; // TはU & move( lvalue ) ; ~~~ -テンプレートパラメーター名`T`がリファレンスのとき、`T`にリファレンス宣言子`&&`をつけても単に無視されることを考えると、上の`move`に`int &`型の`lvalue`が実引数として渡されたときは、以下のように書いたものと等しくなる。 +テンプレートパラメーター名`T`がリファレンスのとき、`T`にリファレンス宣言子`&&`を付けても単に無視されることを考えると、上の`move`に`int &`型の`lvalue`が実引数として渡されたときは、以下のように書いたものと等しくなる。 -~~~c++ +~~~cpp int & move( int & t ) noexcept { return static_cast(t) ; @@ -559,7 +559,7 @@ int main() ここで`rvalue`を渡すのは簡単だ。`std::move`を使えばいい。 -~~~c++ +~~~cpp template < typename T > void f( T && t ) { @@ -571,7 +571,7 @@ void f( T && t ) `t`が`lvalue`ならば`lvalue`として、`rvalue`ならば`xvalue`として、渡された値カテゴリーのまま別の関数に渡したい場合、`std::forward(t)`が使える。 -~~~c++ +~~~cpp template < typename T > void f( T && t ) { @@ -582,9 +582,9 @@ void f( T && t ) `std::forward(t)`の`T`にはテンプレートパラメーター名を書く。こうすると、`t`が`lvalue`ならば`lvalue`リファレンス、`rvalue`ならば`rvalue`リファレンスが戻り値として返される。 -`std::forward`の実装は以下の通りだ。 +`std::forward`の実装は以下のとおりだ。 -~~~c++ +~~~cpp template constexpr T && diff --git a/038-move-semantics.md b/038-move-semantics.md index e917951..2bc53e9 100644 --- a/038-move-semantics.md +++ b/038-move-semantics.md @@ -12,20 +12,20 @@ 例えば、 -~~~c++ +~~~cpp template < typename T > struct S { T x ; } ; ~~~ があり、このクラス`S`のコンストラクターを続いて -~~~c++ +~~~cpp S( T const & x ) : x(x) { } ~~~ と書くことがある。これは実際には間違いで、正しくは以下のように書かなければならない。 -~~~c++ +~~~cpp template < typename T > struct S { @@ -115,7 +115,7 @@ int main() 特殊なルールとして、関数のローカル変数をオペランドに指定した`return`文はムーブをする可能性がある。 -~~~c++ +~~~cpp std::vector f() { std::vector v ; @@ -129,17 +129,17 @@ std::vector f() これは関数のローカル変数は`return`文が実行されたときには無効になるので、特別に存在するルールだ。そもそも、関数の`return`文はコピーもムーブもしない可能性がある。 -~~~c++ +~~~cpp int main() { - // さきほどの関数f + // 先ほどの関数f auto v = f() ; } ~~~ C++コンパイラーは以下のようにコードを変形することも許されているからだ。 -~~~c++ +~~~cpp int main() { std::vector v ; @@ -153,7 +153,7 @@ int main() 以下のようなクラスにムーブを実装しよう。 -~~~c++ +~~~cpp template < typename T > class dynamic_array { @@ -171,7 +171,7 @@ public : ムーブは所有権の移動だ。所有権の移動は、単にポインターをコピーするだけで済む。 -~~~c++ +~~~cpp dynamic_array source(10) ; // ムーブ dynamic_array destination = std::move(source) ; @@ -184,7 +184,7 @@ dynamic_array destination = std::move(source) ; となる。 -~~~c++ +~~~cpp // 1. ムーブ先へ所有権の移動 destination.first = source.first ; destination.last = source.last ; @@ -199,7 +199,7 @@ source.last = nullptr ; ムーブコンストラクターは以下のように実装できる。 -~~~c++ +~~~cpp dynamic_array( dynamic_array && r ) // ムーブ先へ所有権の移動 : first( r.first ), last( r.last ) @@ -214,7 +214,7 @@ dynamic_array( dynamic_array && r ) ムーブ代入の場合、すでにクラスのオブジェクトは構築されている。つまりムーブ先のクラスのオブジェクトはすでにストレージを所有しているかもしれない。 -~~~c++ +~~~cpp dynamic_array source(10) ; dynamic_array destination(10) ; // destinationはすでにストレージを所有 @@ -227,7 +227,7 @@ destination = std::move(source) ; 2. ムーブ先へ所有権の移動 3. ムーブ元の所有権の放棄 -~~~c++ +~~~cpp // 1. ムーブ先の所有権の解放 delete destination.first ; // 2. ムーブ先へ所有権の移動 @@ -240,7 +240,7 @@ source.last = nullptr ; ただし、この実装は自分自身へのムーブ代入に対応できない。 -~~~c++ +~~~cpp destination = std::move( destination ) ; ~~~ @@ -248,13 +248,13 @@ destination = std::move( destination ) ; 一般的なムーブ代入、つまり、 -~~~c++ +~~~cpp a = std::move(b) ; ~~~ -というコードでムーブが実行された場合、変数`b`はその後使えない状態になる。もし`b`が`a`とおなじである場合、`b`が使えない状態になるということは`a`も使えない状態になることはやむを得ないのが普通の挙動だ。 +というコードでムーブが実行された場合、変数`b`はその後使えない状態になる。もし`b`が`a`と同じである場合、`b`が使えない状態になるということは`a`も使えない状態になることはやむを得ないのが普通の挙動だ。 -普通の挙動がコピー代入と異なるのは、歴史的経緯やムーブという破壊的な操作の性質からくるものだ。 +普通の挙動がコピー代入と異なるのは、歴史的経緯やムーブという破壊的な操作の性質から来るものだ。 C++の標準ライブラリは自分自身へのムーブ代入後のオブジェクトの状態について、「有効だが未規定の状態」としている。 @@ -269,13 +269,13 @@ int main() } ~~~ -ムーブ代入でも、コピー代入のように何もしない実装にすることもできる。しかし、C++では様々な議論の結果、ムーブ代入は自己代入を積極的に何もしない挙動にはしないということになっている。 +ムーブ代入でも、コピー代入のように何もしない実装にすることもできる。しかし、C++ではさまざまな議論の結果、ムーブ代入は自己代入を積極的に何もしない挙動にはしないということになっている。 自分自身へのムーブ代入は誤りである。 自分自身へのムーブ代入がうっかり発生する場合は、エイリアシングによるものだ。 -~~~c++ +~~~cpp template < typename T > void moving( T & a, T & b ) { @@ -285,7 +285,7 @@ void moving( T & a, T & b ) このコードが以下のように呼ばれた場合、変数`a`, `b`ともに同じオブジェクトを指しているので、自分自身へのムーブ代入になる。 -~~~c++ +~~~cpp int main() { std::vector v = {1,2,3,} ; @@ -297,10 +297,10 @@ int main() そのための方法は2つある。 -ひとつはポインターを比較することだ。 +1つはポインターを比較することだ。 -~~~c++ +~~~cpp template < typename T > void moving( T & a, T & b ) { @@ -311,10 +311,10 @@ void moving( T & a, T & b ) ただしこれは追加の比較が入るのでパフォーマンスに影響を与える。 -もう一つは、ユーザーにエイリアシングを起こさないことを求めることだ。 +もう1つは、ユーザーにエイリアシングを起こさないことを求めることだ。 -~~~c++ +~~~cpp // 仕様 // この関数のa, bに同じオブジェクトを渡してはならない // 渡した場合の挙動は未定義 @@ -329,7 +329,7 @@ void moving( T & a, T & b ) ムーブ代入演算子は以下のように実装できる。 -~~~c++ +~~~cpp dynamic_array & operator = ( dynamic_array && r ) { // ムーブ先のストレージの解放 @@ -367,7 +367,7 @@ int main() デフォルトのムーブはクラスのメンバーをそれぞれムーブする。 -~~~c++ +~~~cpp b.i = std::move(a.i) ; b.v = std::move(a.v) ; ~~~ diff --git a/039-disable-copy.md b/039-disable-copy.md index 6028f64..89147f3 100644 --- a/039-disable-copy.md +++ b/039-disable-copy.md @@ -4,7 +4,7 @@ 例えばコピー不可能なシステムのリソースを扱うクラスだ。 -具体的にはファイル、スレッド、プロセス、ネットワークソケットといったリソースだ。このようなリソースを管理するクラスを作ったとして、一体コピーをどうすればいいのだろうか。 +具体的にはファイル、スレッド、プロセス、ネットワークソケットといったリソースだ。このようなリソースを管理するクラスを作ったとして、いったいコピーをどうすればいいのだろうか。 コピーできないクラスは`deleted定義`を使ってコピーコンストラクターとコピー代入演算子を消すことができる。 @@ -29,7 +29,7 @@ struct X このようなクラス`X`は、コピーできない。 -~~~c++ +~~~cpp int main() { // デフォルト構築できる @@ -76,11 +76,11 @@ C++には「5原則」という作法がある。 4. ムーブ代入演算子 5. デストラクター -このうちのひとつを独自に定義したならば、残りの4つも定義すべきである。 +このうちの1つを独自に定義したならば、残りの4つも定義すべきである。 というものだ。 -なぜか。コピーやムーブを独自に定義するということは、デフォルトのコピーやムーブでは足りない何らかの処理をしたいはずだ。その処理には、大抵の場合何らかの破棄の処理が必要で、するとデストラクターも定義しなければならない。 +なぜか。コピーやムーブを独自に定義するということは、デフォルトのコピーやムーブでは足りない何らかの処理をしたいはずだ。その処理には、たいていの場合何らかの破棄の処理が必要で、するとデストラクターも定義しなければならない。 同様に、デストラクターで何らかの独自の処理をするということは、コピーやムーブでも何らかの処理をしたいはずだ。 diff --git a/040-smart-pointer.md b/040-smart-pointer.md index fc5bc95..68bd358 100644 --- a/040-smart-pointer.md +++ b/040-smart-pointer.md @@ -27,7 +27,7 @@ void f() この何気ない一見問題のなさそうなコードには問題がある。もし`new int(1)`が失敗した場合、例外が投げられ、そのまま関数`f`の実行は終わってしまう。後続の`delete`は実行されない。 -そのような場合にスマートポインターが使える。スマートポインターはポインターの解放とムーブを変わりに行ってくれる便利なライブラリだ。 +そのような場合にスマートポインターが使える。スマートポインターはポインターの解放とムーブを代わりに行ってくれる便利なライブラリだ。 ## unique_ptr @@ -74,7 +74,7 @@ int main() } ~~~ -`unique_ptr`は大変に便利なのであらゆる箇所で生のポインターの代わりに使うべきだが、古い関数に生のポインターを渡さなければならない場合などは`unique_ptr`を渡せない。そのような場合のために`unique_ptr`、生のポインターを得る方法がある。メンバー関数`get`だ。 +`unique_ptr`はたいへん便利なのであらゆる箇所で生のポインターの代わりに使うべきだが、古い関数に生のポインターを渡さなければならない場合などは`unique_ptr`を渡せない。そのような場合のために`unique_ptr`、生のポインターを得る方法がある。メンバー関数`get`だ。 ~~~cpp // 古臭い時代遅れの生ポインターを引数に取る関数 @@ -124,7 +124,7 @@ int main() `unique_ptr`はコピーができない。 -~~~c++ +~~~cpp int main() { auto p = std::make_unique(0) ; @@ -138,7 +138,7 @@ int main() ムーブはできる。 -~~~c++ +~~~cpp int main() { auto p = std::make_unique(0) ; @@ -146,7 +146,7 @@ int main() } ~~~ -ムーブした後の変数`p`はポインターの所有権を持たない。 +ムーブしたあとの変数`p`はポインターの所有権を持たない。 `unique_ptr`の実装はとても簡単だ。例えば簡易的なものならば1ページに収まるほどのコード量で書ける。 @@ -185,7 +185,7 @@ public : コンストラクターでポインターを受け取り、デストラクターで破棄する。コピーは禁止。ムーブは所有権を移動。特に解説するまでもなくコードを読むだけでいいほどの単純な実装だ。 -現実の`unique_ptr`はもうすこし便利な機能を提供しているので、実装はもうすこし複雑になっているが、基本的な実装としては変わらない。ここで解説したのはとても基本的な使い方なので、より詳しい +現実の`unique_ptr`はもう少し便利な機能を提供しているので、実装はもう少し複雑になっているが、基本的な実装としては変わらない。ここで解説したのはとても基本的な使い方なので、より詳しい ## shared_ptr @@ -206,7 +206,7 @@ int main() `shared_ptr`はコピーができる。 -~~~c++ +~~~cpp auto p1 = std::make_shared(0) ; auto p2 = p1 ; auto p3 = p1 ; @@ -214,7 +214,7 @@ auto p3 = p1 ; しかも、コピーはすべて同じポインターを持っている。例えば以下のようにすると、 -~~~c++ +~~~cpp *p3 = 123 ; ~~~ @@ -256,9 +256,9 @@ int main() そのため、`shared_ptr`を使うときは、ポインターが有効なオブジェクトを指すかどうかを気にしなくてよい。そのポインターを所有する`shared_ptr`のオブジェクトが1つでも生き残っている限り、ポインターは有効になっている。 -`shared_ptr`はどうやって実装されているのだろうか。`shared_ptr`は`T`へのポインターの他に、現在何個の`shared_ptr`のオブジェクトがポインターを所有しているのかを数えるカウンターへのポインターを持っている。 +`shared_ptr`はどうやって実装されているのだろうか。`shared_ptr`は`T`へのポインターのほかに、現在何個の`shared_ptr`のオブジェクトがポインターを所有しているのかを数えるカウンターへのポインターを持っている。 -~~~c++ +~~~cpp template < typename T > class shared_ptr { @@ -269,7 +269,7 @@ class shared_ptr `shared_ptr`が初めて作られるとき、このカウンター用にストレージが動的確保され、値が`1`になる。 -~~~c++ +~~~cpp explicit shared_ptr( T * ptr ) : ptr( ptr ), count( new std::size_t(1) ) { } @@ -277,7 +277,7 @@ explicit shared_ptr( T * ptr ) コピーされるとき、カウンターがインクリメントされる。 -~~~c++ +~~~cpp shared_ptr( const shared_ptr & r ) : ptr( r.ptr ), count( r.count ) { @@ -287,7 +287,7 @@ shared_ptr( const shared_ptr & r ) デストラクターでは、カウンターがデクリメントされる。そしてカウンターがゼロの場合、ポインターが`delete`される。 -~~~c++ +~~~cpp ~shared_ptr() { // カウンターが妥当なポインターを指しているかどうか確認 @@ -309,7 +309,7 @@ shared_ptr( const shared_ptr & r ) 全体としては少し長いが、以下のようになる。 -~~~c++ +~~~cpp template < typename T > class shared_ptr { diff --git a/041-move-support.md b/041-move-support.md index 20741d9..73a1805 100644 --- a/041-move-support.md +++ b/041-move-support.md @@ -2,7 +2,7 @@ 自作の数値計算をするクラスを実装するとしよう。無限精度整数、ベクトル、行列など、自作のクラスで実装したい数値と演算は世の中にたくさんある。 -その時、数値の状態を表現するためにストレージを動的確保するとしよう。ここでは例のため、とても簡単な整数型を考える。 +そのとき、数値の状態を表現するためにストレージを動的確保するとしよう。ここでは例のため、とても簡単な整数型を考える。 ~~~cpp class Integer @@ -49,13 +49,13 @@ public : } ; ~~~ -コンストラクターは動的確保をする。デストラクターは解放する。コピーは動的確保をする。ムーブは所有権の移動をする。とても良くあるクラスの実装だ。 +コンストラクターは動的確保をする。デストラクターは解放する。コピーは動的確保をする。ムーブは所有権の移動をする。とてもよくあるクラスの実装だ。 実用的には`std::unique_ptr`を使うべきだが、低級な処理を説明するためにあえて生のポインターを使っている。 今回のコピー代入演算子は単に値をコピーしているが、コピー元とコピー先で確保したストレージのサイズが異なるような型、たとえば無限精度整数や動的なサイズのベクトルや行列などの場合は、コピー代入演算子でもコピー先のストレージを破棄してコピー元と同じサイズのストレージを確保するなどの処理が必要な場合もある。 -~~~c++ +~~~cpp // 行列クラス class matrix { @@ -93,7 +93,7 @@ public : クラス`Integer`の場合、演算の結果ストレージのサイズが変わるということはないので、愚直な実装で済む。 -~~~c++ +~~~cpp Integer & operator +=( const Integer & r ) { *ptr += *r.ptr ; @@ -113,7 +113,7 @@ Integer & operator -=( const Integer & r ) 演算を表現するクラスでオーバーロードしたい単行演算子には`operator +`と`operator -`がある。特に`operator -`は実用上の意味があるので実装してみよう。 -~~~c++ +~~~cpp Integer a(10) ; auto b = -a ; // これは二項演算子 operator +の結果に @@ -123,7 +123,7 @@ auto c = -(a + a) ; `*this`が`lvalue`の場合の単行演算子の実装は以下のようになる。 -~~~c++ +~~~cpp Integer operator -() const { Integer result( -*ptr ) ; @@ -136,28 +136,28 @@ Integer operator -() const 単項演算子`operator -`は`*this`を書き換えない。負数にした値のコピーを返す。 -変数`result`は`return文`の後は使われないので、`return std::move(result)` と書くこともできる。しかし、そのように書く必要はない。というのも`return文`は特別な扱いを受けているので、関数の中の変数を`return`した場合、自動でムーブが行われるからだ。もちろん、`std::move`を明示的に書いてもよい。 +変数`result`は`return文`のあとは使われないので、`return std::move(result)` と書くこともできる。しかし、そのように書く必要はない。というのも`return文`は特別な扱いを受けているので、関数の中の変数を`return`した場合、自動でムーブが行われるからだ。もちろん、`std::move`を明示的に書いてもよい。 -単項演算子`operator -`は`*this`が`lvalue`のときには上のように実装するしかない。しかしこの実装は非効率的だ。なぜならば、コードを読めばわかるように、追加の一時変数が生成され、追加の動的メモリ確保が行われるからだ。 +単項演算子`operator -`は`*this`が`lvalue`のときには上のように実装するしかない。しかしこの実装は非効率的だ。なぜならば、コードを読めばわかるように、追加の一時変数が生成され、追加の動的メモリー確保が行われるからだ。 そのため、もしクラス`Integer`がコピーしか実装していない場合、 -~~~c++ +~~~cpp Integer a ; auto b = -a ; ~~~ というコードは、 -~~~c++ +~~~cpp Integer a ; auto b = a ; b.make_it_negative() ; ~~~ -のような現在の値をそのまま負数にするメンバー関数`make_it_negative`を実装して使ったほうが効率がよくなる。 +のような現在の値をそのまま負数にするメンバー関数`make_it_negative`を実装して使った方が効率がよくなる。 -~~~c++ +~~~cpp class Integer { int * ptr ; @@ -171,7 +171,7 @@ public : 幸い、クラス`Integer`はムーブコンストラクターを実装しているので、 -~~~c++ +~~~cpp auto b = -a ; ~~~ @@ -179,21 +179,21 @@ auto b = -a ; しかし、 -~~~c++ +~~~cpp auto c = -(a + a) ; ~~~ というコードは依然として非効率的になる。まだ二項演算子`operator +`は実装していないが、これは、 -~~~c++ +~~~cpp auto temp1 = a + a ; auto temp2 = -temp1 ; auto c = temp2 ; ~~~ -になるからだ。すると以下のように書いたほうが効率が良くなる。 +になるからだ。すると以下のように書いた方が効率がよくなる。 -~~~c++ +~~~cpp Integer a ; auto c = a ; c += a ; @@ -204,7 +204,7 @@ c.make_it_negative() ; 単行演算子はクラスのメンバー関数として実装する。 -~~~c++ +~~~cpp class Integer { public ; @@ -214,13 +214,13 @@ public ; これが非メンバー関数ならば、単に`rvalue`リファレンスを取ればよい。 -~~~c++ +~~~cpp Integer negate( Integer && object ) ; ~~~ メンバー関数の場合、`object`に相当するのは`*this`だ。 -~~~c++ +~~~cpp class Integer { public : @@ -231,7 +231,7 @@ public : `this`がポインターになっているのは歴史的な都合で、本来はリファレンスになっているべきだった。メンバー関数は以下のような隠し引数があるものとして考えるとよい。 -~~~c++ +~~~cpp class Integer { public : @@ -247,7 +247,7 @@ public : メンバー関数を`const`修飾するというのは、 -~~~c++ +~~~cpp class Integer { public : @@ -257,7 +257,7 @@ public : この隠し引数を`const`修飾するのと同じだ。 -~~~c++ +~~~cpp class Integer { public : @@ -310,7 +310,7 @@ int main() これは実質的以下のような隠し引数があるものと考えてよい。もちろん隠し引数を使うことはできない。 -~~~c++ +~~~cpp struct X { // lvalueリファレンス @@ -353,7 +353,7 @@ struct X もしメンバー関数にリファレンス修飾子を書いた場合、同じ名前のすべてのメンバー関数にリファレンス修飾子を書かなければならない。 -~~~c++ +~~~cpp struct X { // エラー、リファレンス修飾子がない @@ -372,7 +372,7 @@ struct X リファレンス修飾子を使い、`*this`が`lvalue`と`rvalue`の場合で実装を分けることができる。 -~~~c++ +~~~cpp class Integer { int * ptr ; @@ -395,7 +395,7 @@ public : `rvalue`リファレンス修飾子を使った単行演算子`operator -`の実装は、`*this`自身が`rvalue`であるので、自分自身をムーブしている。ムーブ以降、`this->ptr`は`nullptr`になる。なぜならば、`Integer`のムーブ代入演算子がそのような実装になっているからだ。 -~~~c++ +~~~cpp /// 上で示したのと同じムーブ代入演算子の抜粋 Integer operator =( Integer && r ) { @@ -413,7 +413,7 @@ Integer operator =( Integer && r ) せっかく数値を表現するクラスなのだから二項演算子を使った演算がしたい。 -~~~c++ +~~~cpp int main() { Integer a(1) ; @@ -426,7 +426,7 @@ int main() 演算子のオーバーロードはメンバー関数による方法と、非メンバー関数による方法がある。 -~~~c++ +~~~cpp struct X { // メンバー関数 @@ -441,7 +441,7 @@ X operator +( const X & l, const X & r ) ; 例えば以下のようなコードで、 -~~~c++ +~~~cpp X a ; X b ; a + b ; @@ -457,7 +457,7 @@ a + b ; メンバー関数の場合の実装は以下のようになる。 -~~~c++ +~~~cpp class Integer { int * ptr ; @@ -471,7 +471,7 @@ public : 非メンバー関数の場合は、`Integer::ptr`が`private`メンバーであることが問題になる。 -~~~c++ +~~~cpp Integer operator + ( const Integer & l, const Integer & r ) { // エラー、Integer::ptrはprivateメンバー @@ -483,7 +483,7 @@ Integer operator + ( const Integer & l, const Integer & r ) 1. クラスのメンバー関数として処理を実装し、そのメンバー関数を呼び出す方法 -~~~c++ +~~~cpp class Integer { int * ptr ; @@ -519,7 +519,7 @@ int get_member( const X & obj ) これを使うと、以下のように`friend`宣言すれば、動かなかった非メンバー関数による`operator +`のオーバーロードが動くようになる。 -~~~c++ +~~~cpp class Integer { friend Integer operator +( const Integer &, const Integer & ) ; @@ -528,11 +528,11 @@ class Integer ### ムーブをしたくなる状況 -上の二項演算子の実装だけで、クラス`Integer`は加算ができるようになった。ただし、効率が良くない。 +上の二項演算子の実装だけで、クラス`Integer`は加算ができるようになった。ただし、効率がよくない。 例えば以下のようなコードを考えよう。 -~~~c++ +~~~cpp Integer a ; auto b = a + a + a ; ~~~ @@ -541,15 +541,15 @@ auto b = a + a + a ; 結果として、以下のようなコードと同じになる。 -~~~c++ +~~~cpp Integer a ; auto temp = a + a ; auto b = temp + a ; ~~~ -ムーブを実装していない場合、以下のように書いたほうが効率がよくなる。 +ムーブを実装していない場合、以下のように書いた方が効率がよくなる。 -~~~c++ +~~~cpp Integer a ; auto b = a ; b += a ; @@ -560,13 +560,13 @@ b += a ; 二項演算子は`operator +`だけではない。 -~~~c++ +~~~cpp auto result = a + b - c * d / e ; ~~~ のようなコードも書きたい。これを効率化のために、 -~~~c++ +~~~cpp auto result = a; a += b ; auto temp = c ; @@ -583,7 +583,7 @@ result -= temp ; 非メンバー関数で実装するには、以下のように宣言を書く。 -~~~c++ +~~~cpp class Integer { friend integer operator + ( const Integer & l, const Integer & r ) ; @@ -606,7 +606,7 @@ Integer operator + ( Integer && l, Integer && r ) ; 第一引数が`rvalue`の場合は、以下のようになる。 -~~~c++ +~~~cpp Integer operator + ( Integer && l, const Integer & r ) { auto result = std::move(l) ; @@ -615,14 +615,14 @@ Integer operator + ( Integer && l, const Integer & r ) } ~~~ -第一引数はrvalueなので、ムーブしてもよい。 +第一引数は`rvalue`なので、ムーブしてもよい。 -先程も説明したように、`'return文'`が関数のローカル変数を返すときは自動でムーブしてくれる。もちろん`'return std::move(result)'` と書いてもよい。 +先ほども説明したように、`'return文'`が関数のローカル変数を返すときは自動でムーブしてくれる。もちろん`'return std::move(result)'` と書いてもよい。 第二引数が`rvalue`の場合は、ムーブすべきオブジェクトが第二引数になる。 -~~~c++ +~~~cpp Integer operator + ( const Integer & l, Integer && r ) { auto result = std::move(r) ; @@ -631,7 +631,7 @@ Integer operator + ( const Integer & l, Integer && r ) } ~~~ -この実装は全てに使えるわけではない。加算の場合は、一般に交換法則を満たすことが期待できる。つまり、 +この実装はすべてに使えるわけではない。加算の場合は、一般に交換法則を満たすことが期待できる。つまり、 $$ a + b = b + a $$ @@ -641,7 +641,7 @@ $$ a + b = b + a $$ 第一引数、第二引数が両方共`rvalue`である場合というのは、例えば以下のような場合だ。 -~~~c++ +~~~cpp Integer a ; auto b = (a + a) + (a + a) ; ~~~ @@ -650,7 +650,7 @@ auto b = (a + a) + (a + a) ; もし、`rvalue + lvalue`と`lvalue + rvalue`に対応する演算子しかオーバーロードしていない場合、関数呼び出しが曖昧になってしまう。そこで、`rvalue + rvalue`の演算子オーバーロードも書く。 -~~~c++ +~~~cpp Integer operator +( Integer && l, Integer && r ) { return std::move(l) + r ; @@ -662,7 +662,7 @@ Integer operator +( Integer && l, Integer && r ) メンバー関数で実装する場合、二項演算子の第一引数は`*this`、第二引数がメンバー関数の第一引数になる。 -~~~c++ +~~~cpp class Integer { public : @@ -677,11 +677,11 @@ public : } ; ~~~ -`a + b`のとき、`*this`が`a`、`r`が`b`だ。後の実装は非メンバー関数の場合と変わらない。 +`a + b`のとき、`*this`が`a`、`r`が`b`だ。あとの実装は非メンバー関数の場合と変わらない。 例えばメンバー関数で`rvalue + lvalue`の実装は以下のようになる。 -~~~c++ +~~~cpp Integer Integer::operator +( const Integer & r ) && { auto result = std::move(*this) ; diff --git a/042-string-intro.md b/042-string-intro.md index 87c5459..2cc968a 100644 --- a/042-string-intro.md +++ b/042-string-intro.md @@ -25,7 +25,7 @@ C++では、基本ソース文字セットと呼ばれる文字がある。C++ 空白文字、水平タブ、垂直タブ、フォームフィード、改行の5文字と、印字可能な以下の91文字だ。 -~~~ +~~~c++ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 @@ -56,13 +56,13 @@ Unicode、もしくはISO/IEC 10646(Universal Coded Character Set, UCS)は文字 Unicodeは当初、16bitの符号なし整数値でコードポイントを表現する規格であった。この当時、1コードポイントは1文字であり16bitであった。 -そのような当初の目論見はすぐに破綻し、今では1コードポイントは21bit弱(U+0000からU+10FFFF)であり、1コードポイントは1文字を意味しないようになった。複数のコードポイントを組み合わせて1文字が表現されることもあるからだ。 +そのような当初の目論見はすぐに破綻し、いまでは1コードポイントは21bit弱(U+0000からU+10FFFF)であり、1コードポイントは1文字を意味しないようになった。複数のコードポイントを組み合わせて1文字が表現されることもあるからだ。 Unicodeはコードポイントについて定めた規格であり、バイト列で文字を表現する規格ではない。Unicodeを元にしたバイト列によって文字を表現するエンコード方式に、UTF-8, UTF-16, UTF-32が存在する。 #### UTF-16 -UTF-16は16bitの符号なし整数値によってUnicodeのコードポイントを表現するエンコード方式だ。まだUnicodeが16bitのコードポイントですべての文字を表現すると考えていた頃に考案されたUCS-2が元になっている。 +UTF-16は16bitの符号なし整数値によってUnicodeのコードポイントを表現するエンコード方式だ。まだUnicodeが16bitのコードポイントですべての文字を表現すると考えていたころに考案されたUCS-2が元になっている。 その後、Unicodeのコードポイントが21bit弱に拡張されたので、UCS-2からUTF-16が考案された。 @@ -120,7 +120,7 @@ int main() } ~~~ -筆者の環境では`"21"`と表示される。これはつまり、2つのバイトのうち、下位バイトのほうが先に配置されているということだ。 +筆者の環境では`"21"`と表示される。これはつまり、2つのバイトのうち、下位バイトの方が先に配置されているということだ。 世の中にはリトルエンディアン(Little Endian)とビッグエンディアン(Big Endian)がある。これは複数バイトの順序の違いだ。 @@ -158,7 +158,7 @@ C++プログラムが実行できるOSとしては以下のようなものがあ + Apple iOS + Microsoft Windows -この他にもOSは様々あるが、情報を得るだけでもNDAを結ぶ必要がある表に出てこないOSであったり、実験的すぎたりして、C++を学習する環境としては不適切だ。 +このほかにもOSはさまざまあるが、情報を得るだけでもNDAを結ぶ必要がある表に出てこないOSであったり、実験的すぎたりして、C++を学習する環境としては不適切だ。 このうち、Microsoft Windowsを除くOSはUTF-8を使用している。 @@ -172,7 +172,7 @@ Microsoft WindowsはUTF-16を使用している。ただし、この状況はMic ### 通常の文字リテラル -通常の文字リテラルは単一引用符でひとつの文字を囲む。 +通常の文字リテラルは単一引用符で1つの文字を囲む。 ~~~c++ `a` @@ -188,7 +188,7 @@ char b = 'b' ; char c = 'c' ; ~~~ -文字リテラルには以下のようなエスケープシーケンスがある。これは一部の印字不可能な文字や、文法上の理由で直接リテラルのなかに書くことができない文字を書けるようにするための代替手段だ。 +文字リテラルには以下のようなエスケープシーケンスがある。これは一部の印字不可能な文字や、文法上の理由で直接リテラルの中に書くことができない文字を書けるようにするための代替手段だ。 意味 リテラル @@ -221,7 +221,7 @@ char c = '\\' ; 通常の文字がどのような文字エンコードを使っているかは実装定義だ。 -その他にも文字の数値を直接指定するエスケープシーケンスとして、8進数エスケープシーケンスと16進数エスケープシーケンスがある。 +そのほかにも文字の数値を直接指定するエスケープシーケンスとして、8進数エスケープシーケンスと16進数エスケープシーケンスがある。 ~~~c++ char oct = '\101' ; @@ -255,7 +255,7 @@ char hex = "\x41" ; "This is a pen." ; ~~~ -通常の文字列リテラルの型はconstな文字型の配列になる。具体的な型としては`const char [n]`になる。`n`は文字列のサイズだ。通常の文字列リテラルの中の文字が基本実行文字だけであれば、書かれている文字数+1になる。しかし、この文字数というのも難しい。 +通常の文字列リテラルの型は`const`な文字型の配列になる。具体的な型としては`const char [n]`になる。`n`は文字列のサイズだ。通常の文字列リテラルの中の文字が基本実行文字だけであれば、書かれている文字数+1になる。しかし、この文字数というのも難しい。 文字列リテラルが連続している場合、1つにまとめられる。 @@ -282,7 +282,7 @@ auto s = "abcdef" ; 通常の文字列リテラルは末尾にnull文字(`\0`)が付与される。このために、配列のサイズは文字数+1になる。 -具体的な例では、`"abc"`という通常の文字列リテラルの型は'const char [4]'になる。これは以下のような配列に等しい。 +具体的な例では、`"abc"`という通常の文字列リテラルの型は``const char [4]`'になる。これは以下のような配列に等しい。 ~~~c++ const char s[4] = {'a', 'b', 'c', '\0'} ; @@ -300,7 +300,7 @@ const char s[6] = {'h', 'e', 'l', 'l', 'o', '\0' } ; char s[6] = "hello" ; ~~~ -配列の添字を書かない場合、文字列リテラルのサイズになるうう。 +配列の添字を書かない場合、文字列リテラルのサイズになる。 ~~~c++ // char [6] @@ -329,7 +329,7 @@ decltype(auto) reference = "hello" ; ## ワイド文字 -ワイド文字リテラルとワイド文字列リテラルはリテラルにエンコードプレフィクス`L`をつける。 +ワイド文字リテラルとワイド文字列リテラルはリテラルにエンコードプレフィクス`L`を付ける。 ~~~c++ // ワイド文字リテラル @@ -345,9 +345,9 @@ wchar_t c = L'A' ; const wchar_t (&ref)[6] = L"hello" ; ~~~ -ワイド文字は失敗した機能だ。まだUnicodeが16bitで世界中の文字を表現できるという妄想に囚われていた頃に提案された時代遅れの実装不可能な機能だ。 +ワイド文字は失敗した機能だ。まだUnicodeが16bitで世界中の文字を表現できるという妄想にとらわれていたころに提案された時代遅れの実装不可能な機能だ。 -C++の規格では、「ワイド文字は`wchar_t`型のオブジェクト1つがシステムがサポートする任意の1文字を表現可能である」と規定している。そのような文字エンコード方式は未だかつて存在していない。Unicodeの1コードポイントは1文字を意味しないので、UTF-32を使ってもワイド文字の規定を満たすことはできない。そのため、現在規格準拠の方法でワイド文字を実装しているC++コンパイラーは存在しない。 +C++の規格では、「ワイド文字は`wchar_t`型のオブジェクト1つがシステムがサポートする任意の1文字を表現可能である」と規定している。そのような文字エンコード方式はいまだかつて存在していない。Unicodeの1コードポイントは1文字を意味しないので、UTF-32を使ってもワイド文字の規定を満たすことはできない。そのため、現在規格準拠の方法でワイド文字を実装しているC++コンパイラーは存在しない。 Microsoft Windowsはワイド文字をUTF-16で表現している。それ以外の主要なOSはUTF-32を使っている。 @@ -380,7 +380,7 @@ char32_t s3[] = U"hello" ; `"いろは"`をそれぞれの文字列リテラルで表現すると以下のようになる。 -~~~c++ +~~~cpp // char8_t [10] char8_t s1[] = u8"いろは" ; // char16_t [4] @@ -391,7 +391,7 @@ char32_t s3[] = U"いろは" ; これは以下のように書くのと同じだ。 -~~~c++ +~~~cpp char8_t s1[10] = { 0xe3, 0x81, 0x84, 0xe3, 0x82, 0x8d, 0xe3, 0x81, 0xaf, 0x0 } ; char16_t s2[4] = { 0x3044, 0x308d, 0x306f, 0x0 } ; char32_t s3[4] = { 0x3044, 0x308d, 0x306f, 0x0 } ; @@ -403,7 +403,7 @@ char32_t s3[4] = { 0x3044, 0x308d, 0x306f, 0x0 } ; 以下のコードは、 -~~~c++ +~~~cpp char8_t s1[] = u8"𦥑" ; char16_t s2[] = u"𦥑" ; char32_t s3[] = U"𦥑" ; @@ -411,15 +411,15 @@ char32_t s3[] = U"𦥑" ; 以下のように解釈される。 -~~~c++ +~~~cpp char8_t s1[5] = { 0xf0, 0xa6, 0xa5, 0x91, 0x0 } ; char16_t s2[2] = { 0xd85a, 0xdd51, 0x0 } ; char32_t s3[2] = { 0x26951, 0x0 } ; ~~~ -文字`'が'`はUnicodeコードポイントでは結合済みコードポイントの`U+304C`で表現できるが、コードポイントU+304B(HIRAGANA LETTER KA)の後に直ちに続いて、コードポイントU+3099(COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK)を使って表現してもよい。 +文字`'が'`はUnicodeコードポイントでは結合済みコードポイントの`U+304C`で表現できるが、コードポイントU+304B(HIRAGANA LETTER KA)のあとに直ちに続いて、コードポイントU+3099(COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK)を使って表現してもよい。 -~~~c++ +~~~cpp // u8"\u304C" char8_t ga1[] = u8"が" ; // u8"\u304B\u3099" @@ -428,7 +428,7 @@ char8_t ga2[] = u8"か\u3099" ; これは以下のコードと等しい。 -~~~c++ +~~~cpp char8_t ga1[4] = { 0xe3, 0x81, 0x8c, 0x0 } ; char8_t ga2[7] = { 0xe3, 0x81, 0x8b, 0xe3, 0x82, 0x99, 0x0 } ; ~~~ @@ -437,7 +437,7 @@ char8_t ga2[7] = { 0xe3, 0x81, 0x8b, 0xe3, 0x82, 0x99, 0x0 } ; Apple macOSはUnicodeの正規化として一般的なNFC(Canonical Composition)ではなくNormalization Form D(NFD)を使っているので、濁点や半濁点は必ず分解される。Apple macOSでは`u8"\u304B\u3099"`が一般的な表現で、それ以外の環境では`u8"\u304C"`が一般的な表現だ。しかし、どちらも意味上は同じ表現だ。 -Unicodeの奇妙で面白い例は枚挙に暇がない。ここでは日本語を扱う際によくある注意点を説明したが、他にも絵文字、デーヴァナーガリー(ヒンディー語、マラーティー語、ネパール語)、モンゴル文字、アラビア文字、ヘブライ文字など扱いの難しい文字がたくさんある。 +Unicodeの奇妙で面白い例は枚挙に暇がない。ここでは日本語を扱う際によくある注意点を説明したが、ほかにも絵文字、デーヴァナーガリー(ヒンディー語、マラーティー語、ネパール語)、モンゴル文字、アラビア文字、ヘブライ文字など扱いの難しい文字がたくさんある。 重要な点をまとめると、 @@ -552,7 +552,7 @@ void process_string( const char * str ) `std::strlen`はポインターが指し示すnull終端された配列のnull文字を除くサイズを返す。以下のような実装だ。 -~~~c++ +~~~cpp std::size_t strlen( const char * s ) { auto i = s ; @@ -568,9 +568,9 @@ null終端文字列は文字型へのポインター1つだけなので取り回 ### std::basic_string -今まで文字列の型として使ってきた`std::string`は、実はクラステンプレートで実装されている。 +いままで文字列の型として使ってきた`std::string`は、実はクラステンプレートで実装されている。 -~~~c++ +~~~cpp namespace std { template< typename charT, @@ -585,7 +585,7 @@ namespace std { これに対し、以下のようなエイリアスが存在する。 -~~~c++ +~~~cpp namespace std { using string = basic_string ; using u8string = basic_string ; @@ -597,7 +597,7 @@ namespace std { それぞれの文字型に対応した`basic_string`のクラスだ。 -これに対して、ユーザー定義リテラルという機能を使い、文字列リテラルのサフィックスに`s`をつけることで、文字列リテラルを対応する`basic_string`のクラス型に変換できる。 +これに対して、ユーザー定義リテラルという機能を使い、文字列リテラルのサフィックスに`s`を付けることで、文字列リテラルを対応する`basic_string`のクラス型に変換できる。 ~~~cpp // string @@ -614,7 +614,7 @@ auto wstr = L"hello"s ; ユーザー定義リテラルの詳細については本書では詳しく説明しないが、演算子のオーバーロードと同じだ。演算子をオーバーロードするようにリテラル演算子をオーバーロードする。 -~~~c++ +~~~cpp std::string operator ""s( const char * ptr, std::size_t n ) { return std::string( ptr, n ) ; } std::u8string operator ""s( const char8_t * ptr, std::size_t n ) @@ -629,7 +629,7 @@ std::wstring operator ""s( const wchar_t * ptr, std::size_t n ) ユーザー定義リテラルを正しく実装するには複雑なルールがある。例えばユーザー定義のサフィックス名はアンダースコア1つから始まっていなければならないなどだ。 -~~~c++ +~~~cpp // OK int operator "" _abc( unsigned long long int ) ; // エラー、アンダースコア1つから始まっていない @@ -641,7 +641,7 @@ int operator ""abc( unsigned long long int ) ; `basic_string`による文字列の表現方法は、文字型配列の先頭要素へのポインター、文字型配列のサイズ、アロケーターだ。 -~~~c++ +~~~cpp template < typename charT, typename traits = char_traits, @@ -657,13 +657,13 @@ class basic_string あるいは、配列のサイズを表現するために、配列の最後の要素の1つ次のポインターを使っているかもしれない。 -~~~c++ +~~~cpp CharT * ptr ; CharT * last ; Allocator alloc ; ~~~ -`std::vector`と同じで、どちらのほうが効率がいいかはアーキテクチャにより異なる。 +`std::vector`と同じで、どちらの方が効率がいいかはアーキテクチャにより異なる。 `basic_string`は文字列を表現するためのストレージを所有するクラスだ。コンストラクターでストレージを動的確保し、デストラクターで解放する。 @@ -694,7 +694,7 @@ int main() `basic_string_view`はストレージを所有しないクラスだ。以下のような宣言になる。 -~~~c++ +~~~cpp namespace std { template < typename charT, @@ -704,9 +704,9 @@ namespace std { } ~~~ -その実装は文字型へのポインター2つか、文字型へのポインターひとつと配列のサイズを保持する整数型になる。 +その実装は文字型へのポインター2つか、文字型へのポインター1つと配列のサイズを保持する整数型になる。 -~~~c++ +~~~cpp charT * first ; charT * last ; ~~~ @@ -721,7 +721,7 @@ namespace std { `basic_string_view`には`basic_string`と対になる各文字型に対する特殊化がある。 -~~~c++ +~~~cpp namespace std { using string_view = basic_string_view ; using u8string_view = basic_string_view ; @@ -790,7 +790,7 @@ int main() } ~~~ -のように、どちらの文字列表現を使っても1つの関数を書くだけですむ。 +のように、どちらの文字列表現を使っても1つの関数を書くだけで済む。 `basic_string_view`はストレージを所有しないので関数の引数として使うときはリファレンスで取る必要はない。 @@ -805,7 +805,7 @@ void g( std::string_view obj ) ; ### null終端文字列の操作 -null終端文字列は文字列の先頭となる文字型へのポインター型のオブジェクトひとつで表現されるので、文字型の配列のサイズを取得するにも、いちいちnull文字が見つかるまでポインターをインクリメントしていく必要がある。この処理をやってくれるのが`std::strlen`だ。 +null終端文字列は文字列の先頭となる文字型へのポインター型のオブジェクト1つで表現されるので、文字型の配列のサイズを取得するにも、いちいちnull文字が見つかるまでポインターをインクリメントしていく必要がある。この処理をやってくれるのが`std::strlen`だ。 ~~~cpp void f( const char * ptr ) @@ -816,7 +816,7 @@ void f( const char * ptr ) 文字列リテラルの型は`const`な文字型の配列なので、文字列を変更することができない。 -~~~c++ +~~~cpp const char * ptr = "abc" ; // エラー ptr[0] = 'x' ; @@ -863,7 +863,7 @@ int main() ここで、配列`s`の要素数は7以上でなければならない。最終的なnull終端文字列を表現するには最低でも`char [7]`が必要だからだ。 -例えば2つのnull終端文字列を結合する場合で、どちらも`const`であったり、十分なサイズがなかった場合、2つの文字列を保持できるサイズのメモリを確保して、コピーしなければならない。 +例えば2つのnull終端文字列を結合する場合で、どちらも`const`であったり、十分なサイズがなかった場合、2つの文字列を保持できるサイズのメモリーを確保して、コピーしなければならない。 ~~~cpp // s1, s2を結合して使う関数 @@ -871,7 +871,7 @@ void concat_str( const char * s1, const char * s2 ) { // 2つの文字列のサイズの合計 + null文字 auto size = std::strlen( s1 ) + std::strlen( s2 ) + 1 ; - // 文字列を保持するメモリを確保する + // 文字列を保持するメモリーを確保する char * ptr = new char[size] ; char * i = ptr ; @@ -892,7 +892,7 @@ void concat_str( const char * s1, const char * s2 ) // 結合した文字列を使う - // 使い終わったのでメモリを解放する + // 使い終わったのでメモリーを解放する delete[] ptr ; } ~~~ @@ -1001,7 +1001,7 @@ int main() これは以下のようにも書ける。 -~~~c++ +~~~cpp for ( auto i = std::begin(s) ; i != std::end(s) ; ++i ) { std::cout << *i ; @@ -1027,7 +1027,7 @@ int main() } ~~~ -イテレーターを使うのは煩わしいが、C++20では`Range`ライブラリが追加され、以下のように書ける予定だ +イテレーターを使うのは煩わしいが、C++20では`Range`ライブラリが追加され、以下のように書ける予定だ。 ~~~c++ auto r = std::ranges::search( text, word ) ; @@ -1035,7 +1035,7 @@ if ( !std::ranges::empty(r) ) // ... ~~~ -名前空間を省くと、`!empty( search( text, word) )` になるが、これでもまだ分かりづらい。そこで`basic_string::find`がある。これは``の`std::find`とは別物で、文字列から部分文字列を探し、その部分文字列に一致する文字へのインデックスを返す。 +名前空間を省くと、`!empty( search( text, word) )` になるが、これでもまだわかりづらい。そこで`basic_string::find`がある。これは``の`std::find`とは別物で、文字列から部分文字列を探し、その部分文字列に一致する文字へのインデックスを返す。 ~~~cpp int main() @@ -1057,7 +1057,6 @@ int main() 例えば以下のコードを実行すると、 ~~~cpp - int main() { auto text = u8"すばしっこい茶色の狐がノロマな犬を飛び越した。"s ; @@ -1100,7 +1099,7 @@ int main() この場合、変数`text`に文字列`"abc"`はないので、`npos`が返る。`npos`が返ったかどうかは`npos`と比較すればわかる。`npos`は`-1`と等しいので、以下のようにも書ける。 -~~~c++ +~~~cpp if ( index != -1 ) // ... ~~~ @@ -1128,7 +1127,7 @@ C++20では、`starts_with/ends_with`という2つの便利なメンバー関数 `starts_with(str)`は文字列が部分文字列`str`で始まっている場合に`true`を返す。そうでない場合は`false`を返す。 -~~~c++ +~~~cpp int main() { auto text = "aa bb cc"s ; @@ -1142,11 +1141,11 @@ int main() bool b4 = text.starts_with("b"sv) ; bool b5 = text.starts_with("aaa"sv) ; } -~~ +~~~ `ends_with(str)`は文字列が部分文字列`str`で終わっている場合に`true`を返す。そうでない場合は`false`を返す。 -~~~c++ +~~~cpp int main() { auto text = "aa bb cc"s ; @@ -1197,7 +1196,7 @@ int main() 末尾への挿入は文字列の結合と同じ効果だ。 -インデックスで中間に挿入するのは以下の通り。 +インデックスで中間に挿入するのは以下のとおり。 ~~~cpp int main() @@ -1268,7 +1267,7 @@ int main() #### その他の推奨できない操作 -`basic_string`にはこの他に様々な、現代では推奨できない操作がある。 +`basic_string`にはこのほかにさまざまな、現代では推奨できない操作がある。 例えば`operator []`で文字列をインデックスでアクセスできる。これは基本実行文字セットに対しては動く。 @@ -1299,7 +1298,7 @@ int main() } ~~~ -`text`のインデックス`0`に当たる文字型の値は`u8'い'`ではない。UTF-8は文字「い」を文字型1つで表現できないからだ。`u8"いろは"`というUTF-8文字列リテラルはすでに学んだように、以下のように表現される。 +`text`のインデックス`0`にあたる文字型の値は`u8'い'`ではない。UTF-8は文字「い」を文字型1つで表現できないからだ。`u8"いろは"`というUTF-8文字列リテラルはすでに学んだように、以下のように表現される。 ~~~cpp // u8"いろは" @@ -1321,7 +1320,7 @@ int main() } ~~~ -`i`は`3`になる。なぜならば、`find_first_of("abc"sv)`はa, b, cのうちいずれかの文字である最初のインデックスを返すからだ。 +`i`は`3`になる。なぜならば、`find_first_of("abc"sv)`は`a`, `b`, `c`のうちいずれかの文字である最初のインデックスを返すからだ。 この機能はUnicodeでは使えない。というのも1文字型で1文字を表現できないからだ。 @@ -1334,7 +1333,7 @@ C++20では、文字列の先頭と末尾を指定したインデックス数分 先頭を削るには`remove_prefix(i)`を使う。 -~~~c++ +~~~cpp int main() { auto text = "quick brown fox jumps over the lazy dog." ; @@ -1347,7 +1346,7 @@ int main() 末尾を削るには`remove_suffix(i)`を使う。 -~~~c++ +~~~cpp int main() { auto text = "quick brown fox jumps over the lazy dog." ; diff --git a/043-random.md b/043-random.md index b943f13..9d5eca6 100644 --- a/043-random.md +++ b/043-random.md @@ -1,6 +1,6 @@ # 乱数 -乱数はプログラミングにおいてよく使う。例えば6面ダイスをプログラムで実装するには、1,2,3,4,5,6までのいずれかの目を出す。 +乱数はプログラミングにおいてよく使う。例えば6面ダイスをプログラムで実装するには、1, 2, 3, 4, 5, 6までのいずれかの目を出す。 ~~~ $ ./dice @@ -14,16 +14,16 @@ $ ./dice 5 1 6 6 1 6 6 2 4 2 ~~~ -このプログラム`dice`は標準入力から整数型の値`n`を取り、1,2,3,4,5,6のいずれかをそれぞれ$\frac{1}{6}$の確率で`n`個出力する。 +このプログラム`dice`は標準入力から整数型の値`n`を取り、1, 2, 3, 4, 5, 6のいずれかをそれぞれ$\frac{1}{6}$の確率で`n`個出力する。 まずこの`dice`プログラムを作ることを目標にC++の乱数アルゴリズムである``の使い方を学んでいく。 ## 疑似乱数 -コンピューターで使われる乱数のほとんどは疑似乱数と呼ばれる方法で生成されている。様々なアルゴリズムがあるが、とても簡単に理解できる疑似乱数のアルゴリズムに、線形合同法(Linear congruential generator)がある。 +コンピューターで使われる乱数のほとんどは疑似乱数と呼ばれる方法で生成されている。さまざまなアルゴリズムがあるが、とても簡単に理解できる疑似乱数のアルゴリズムに、線形合同法(Linear congruential generator)がある。 -線形合同法では今の乱数を$X_n$、次の乱数$X_{n+1}$とすると、$X_{n+1}$は以下のように求められる。 +線形合同法ではいまの乱数を$X_n$、次の乱数$X_{n+1}$とすると、$X_{n+1}$は以下のように求められる。 $$ X_{n+1} = (a \times X_{n} + c) \bmod m @@ -39,7 +39,7 @@ $$X_2 = 3 \times X_1 + 5 \bmod 2^{32}-1 = 20$$ $$X_3 = 3 \times X_2 + 5 \bmod 2^{32}-1 = 65$$ -「これは全然乱数ではない。予測可能じゃないか」と考えるかも知れない。しかし中でどのように乱数が生成されているかわからなければ、外部からは乱数のように見える。これが擬似乱数の考え方だ。 +「これはぜんぜん乱数ではない。予測可能じゃないか」と考えるかもしれない。しかし中でどのように乱数が生成されているかわからなければ、外部からは乱数のように見える。これが擬似乱数の考え方だ。 ## 乱数エンジン @@ -47,7 +47,7 @@ $$X_3 = 3 \times X_2 + 5 \bmod 2^{32}-1 = 65$$ 乱数エンジンはメンバー関数`min()`で最小値を、メンバー関数`max()`で最大値を、`operator()`で最小値から最大値の間の乱数を返す -~~~c++ +~~~cpp template < typename Engine > void f( Engine & e ) @@ -81,7 +81,7 @@ int main() } ~~~ -標準ライブラリの提供する乱数エンジンには様々なものがあるが、本書ではもう一つ、メルセンヌツイスターというアルゴリズムを実装した乱数エンジンを紹介する。`std::mt19937`だ。 +標準ライブラリの提供する乱数エンジンにはさまざまなものがあるが、本書ではもう1つ、メルセンヌツイスターというアルゴリズムを実装した乱数エンジンを紹介する。`std::mt19937`だ。 `std::mt19937`を使うには、`st::default_random_engine`を置き換えるだけでいい。 @@ -126,13 +126,13 @@ $ dice 3499211612 581869302 3890346734 3586334585 545404204 4161255391 3922919429 949333985 2715962298 1323567403 ~~~ -乱数エンジンで生成されるのは生の乱数だ。これは通常、32bit符号なし整数とか64bit符号なし整数で表現できる全範囲の値として生成される。これは実際に必要な乱数とは値の範囲が違う。実際に必要な乱数とは、例えば6面ダイスの場合は、`int`型で1,2,3,4,5,6のいずれかの値がそれぞれ$\frac{1}{6}$の確率で出てほしい。 +乱数エンジンで生成されるのは生の乱数だ。これは通常、32bit符号なし整数とか64bit符号なし整数で表現できる全範囲の値として生成される。これは実際に必要な乱数とは値の範囲が違う。実際に必要な乱数とは、例えば6面ダイスの場合は、`int`型で1, 2, 3, 4, 5, 6のいずれかの値がそれぞれ$\frac{1}{6}$の確率で出てほしい。 ## 乱数分布 `乱数分布`とは生の乱数を望みの範囲の乱数に加工するためのライブラリだ。クラスで実装されている。 -乱数分布ライブラリにも様々なものがあるが、6面ダイスのプログラムを実装するのに使うのは`std::uniform_int_distribution`だ。 +乱数分布ライブラリにもさまざまなものがあるが、6面ダイスのプログラムを実装するのに使うのは`std::uniform_int_distribution`だ。 この乱数分布ライブラリは、`T`にほしい乱数の整数型を指定する。コンストラクター引数を2つ取るので、1つ目の引数に最小値、2つ目の引数に最大値を指定する。 @@ -144,7 +144,7 @@ std::uniform_int_distribution d(a, b) ; 6面ダイスを作るには、`d(a, b)`を`d(1, 6)`にすればよい。 -~~~c++ +~~~cpp std::uniform_int_distribution d(1, 6) ; ~~~ @@ -184,7 +184,7 @@ int main() } ~~~ -早速実行してみよう。 +さっそく実行してみよう。 ~~~ $ ./dice @@ -200,7 +200,7 @@ $ ./dice この場合、乱数値は正の整数しか生成しないので、型を`int`ではなく`unsigned int`にすることもできる。 -~~~c++ +~~~cpp std::uniform_int_distribution d( 1, 6 ) ; ~~~ @@ -221,11 +221,11 @@ int main() もし2回目の出目の方が1回目の出目より大きかった場合、結果は負数になってしまうが、`unsigned int`型は負数を表現できない。 -そのため、通常は符号付きの整数型を使ったほうが安全だ。 +そのため、通常は符号付きの整数型を使った方が安全だ。 また、分布クラスのテンプレートパラメーターにはデフォルトテンプレート実引数が指定されているので、デフォルトでよければ省略することもできる。 -~~~c++ +~~~cpp // std::uniform_int_distributionと同じ std:uniform_int_distribution d( 1, 6 ) ; ~~~ @@ -234,13 +234,13 @@ std:uniform_int_distribution d( 1, 6 ) ; ## シード -線形合同法を思い出してみよう。線形合同法で次の乱数$X_{n+1}$を計算するには、今の乱数$X_{n}$に対して$X_{n+1} = (a \times X_{n} + c) \bmod m$という計算をする。 +線形合同法を思い出してみよう。線形合同法で次の乱数$X_{n+1}$を計算するには、いまの乱数$X_{n}$に対して$X_{n+1} = (a \times X_{n} + c) \bmod m$という計算をする。 線形合同法とは現在の乱数値を内部状態として持ち、そこに計算を加えることで次の乱数を作り出すのだ。 一般化すると、疑似乱数は内部状態$S_n$を持ち、計算を加える関数$f(x)$を適用することで、次の内部状態$S_{n+1}=f(S_n)$を作り出すのだ。単純な線形合同法の場合、内部状態がそのまま乱数の値になるが、複雑な疑似乱数アルゴリズムでは、内部状態から乱数を求めるのにさらに計算を加えるものもある。 -乱数エンジンをデフォルト初期化すると、この内部状態もデフォルト初期化される。そのため、今まで使っていた乱数は、プログラムの実行ごとに同じ乱数列を作り出すのだ。 +乱数エンジンをデフォルト初期化すると、この内部状態もデフォルト初期化される。そのため、いままで使っていた乱数は、プログラムの実行ごとに同じ乱数列を作り出すのだ。 疑似乱数の内部状態の初期状態を設定するための値をシード(seed)という。シードを設定するには、`std::seed_seq`というクラスのオブジェクトを乱数エンジンのコンストラクターに渡す。 @@ -322,7 +322,7 @@ int main() } ~~~ -内部状態を更新するのではない、本当に予測不可能な乱数を生成するには、ハードウェアの支援が必要だ。例えば放射性同位体がいつ放射性崩壊を起こすかは予測不可能だ。したがって放射線量を計測するガイガーカウンターの値は予測不可能だ。コンピューターにガイガーカウンターが取り付けられていれば、その値を読むことによって予測不可能な値を得ることができる。他にもコンピューターには様々な予測不可能な値を得る方法がある。`std::random_device`はそのような実装依存のコンピューターの支援を受け、予測不可能な乱数を生成する乱数エンジンだ。 +内部状態を更新するのではない、本当に予測不可能な乱数を生成するには、ハードウェアの支援が必要だ。例えば放射性同位体がいつ放射性崩壊を起こすかは予測不可能だ。したがって放射線量を計測するガイガーカウンターの値は予測不可能だ。コンピューターにガイガーカウンターが取り付けられていれば、その値を読むことによって予測不可能な値を得ることができる。ほかにもコンピューターにはさまざまな予測不可能な値を得る方法がある。`std::random_device`はそのような実装依存のコンピューターの支援を受け、予測不可能な乱数を生成する乱数エンジンだ。 ~~~cpp int main() @@ -336,7 +336,7 @@ int main() `std::random_device`を使えば、`std::seed_seq`を予測不可能な値で初期化できる。 -~~~c++ +~~~cpp // 予測不可能な乱数エンジン std::random_device rd ; // シード値 @@ -369,15 +369,15 @@ int main() `std::random_device`は`unsigned int`型の乱数を返す。筆者の環境では`sizeof(unsigned int) == 4`になる。すると$5000 \div 4 = 1250$ となる。とすると安全のためには、`std::seed_seq`には`std::random_device`の乱数を1250個渡すべきだろう。 -~~~c++ +~~~cpp std::random_device rd ; -std::seed_seq s = { rd(), rd(), rd(), ... /*のこり1247個のrd()*/ } ; +std::seed_seq s = { rd(), rd(), rd(), ... /*残り1247個のrd()*/ } ; std::mt19937 e( s ) ; ~~~ -筆者の環境では`sizeof(std::default_random_engine) == 8`であった。すると2個で良いことになる。 +筆者の環境では`sizeof(std::default_random_engine) == 8`であった。すると2個でよいことになる。 -~~~c++ +~~~cpp std::random_device rd ; std::seed_seq s = { rd(), rd() } ; std::default_random_engine e(s) ; @@ -418,9 +418,9 @@ int dice( Engine & e ) 剰余を使って値を$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$だ。 +生の乱数`r`には3ビット以上の情報が必要だ。コンピューターは整数をビット列で表現するのですべてのビットが等しく乱数の場合、$n$ビットの乱数値は$2^n$個の状態を持つ。これを$0 \leq r \leq 2^n-1$に割り振った符号なし整数にしたとする。`r`が3ビットの場合、その値の範囲は$0 \leq r \leq 7$だ。 -上のコードでは、0から5まではそのまま1から6になる。剰余のため、6と7はそれぞれ1と2になる。すべての取りうる乱数を書き出してみよう。 +上のコードでは、0から5まではそのまま1から6になる。剰余のため、6と7はそれぞれ1と2になる。すべての取り得る乱数を書き出してみよう。 生の乱数 ダイス目 --------- ------ @@ -441,7 +441,7 @@ int dice( Engine & e ) では$1 \leq n \leq 6$までの乱数を得るにはどうするのかというと以下のようなアルゴリズムで分布を行う。 1. 3bitの生の乱数rを得る -2. `r`が$0 \leq r \leq 5$なら'r+1'が分布された乱数 +2. `r`が$0 \leq r \leq 5$なら``r`+1'が分布された乱数 3. それ以外の場合、1に戻る これを実装すると以下のようになる。 @@ -506,7 +506,7 @@ void f( Distribution & d ) 例えば、`std::uniform_int_distribution( a, b )`の場合、構築時に渡した`a`, `b`を引数の名前でメンバー関数として取得できる。 -~~~c++ +~~~cpp std::uniform_int_distribution d( 1, 6 ) ; d.a() ; // 1 d.b() ; // 6 @@ -527,7 +527,7 @@ int main() std::mt19937 x ; // 乱数を生成 - // aは内部に乱数をキャッシュするかも知れない。 + // aは内部に乱数をキャッシュするかもしれない。 a( x ) ; // yはxと同じ内部状態を持つ @@ -544,7 +544,7 @@ int main() このような場合に、内部状態をリセットするメンバー関数`reset`を呼び出せば、同じ内部状態になることが保証される。 -~~~c++ +~~~cpp // 内部状態をリセット a.reset() ; // true @@ -572,7 +572,7 @@ void f( Distribution & d ) ## 一様分布(Uniform Distribution) -一様分布とは乱数の取りうる状態がすべて等しい確率で出現する乱数のことだ。 +一様分布とは乱数の取り得る状態がすべて等しい確率で出現する乱数のことだ。 ### 整数の一様分布(`std::uniform_int_distribution`) @@ -588,9 +588,9 @@ $$ std::uniform_int_distribution d( a, b ) ; ~~~ -`IntType`は整数型でデフォルトは`int`、`a`は最小値、`b`は最大値。ただし$a \leq b$ +`IntType`は整数型でデフォルトは`int`、`a`は最小値、`b`は最大値。ただし$a \leq b$。 -エンジンも含めた使い方は以下の通り +エンジンも含めた使い方は以下のとおり。 ~~~cpp template < typename Engine > @@ -607,11 +607,11 @@ void f( Engine & e ) 値の範囲には負数も使える。 -~~~c++ +~~~cpp std::uniform_int_distribution d( -3, 3 ) ; ~~~ -この分布は、-3,-2,-1,0,1,2,3のいずれかをそれぞれ$\frac{1}{7}$の等しい確率で返す。 +この分布は、$-3$, $-2$, $-1$, 0, 1, 2, 3のいずれかをそれぞれ$\frac{1}{7}$の等しい確率で返す。 ### 浮動小数点数の一様分布(`uniform_real_distribution`) @@ -630,9 +630,9 @@ $$ std::uniform_real_distribution d( a, b ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`double`、`a`は最小値、`b`は最大値。値の範囲は$a \leq b$ かつ $b - a \leq \text{RealType型の最大値}$ +`RealType`は浮動小数点数型でデフォルトは`double`、`a`は最小値、`b`は最大値。値の範囲は$a \leq b$ かつ $b - a \leq \text{`RealType`型の最大値}$。 -エンジンも含めた使い方は以下の通り。 +エンジンも含めた使い方は以下のとおり。 ~~~cpp template < typename Engine > @@ -653,13 +653,13 @@ void f( Engine & e ) ベルヌーイ分布(bernoulli distribution)とは、ベルヌーイ試行(bernoulli trial)に関する分布だ。 -ベルヌーイとは数学者ヤコブ・ベルヌーイ(Jacob Bernoulli, 1655-1705)に由来する。ヤコブ・ベルヌーイは西洋数学史上、ジェロラモ・カルダーノ(Gerolamo Cardano, 1501-1576)に続く二人目の、数学的に乱数をまともに文章に書き残した数学者で、現在の統計の基礎を切り開いた人物だ。西洋数学史において乱数と統計の研究は遅れた。この理由は主に宗教的なもので、運命とは神の決定したもうことであり、人の子の及ぶところではないとする考え方が一般的だった。そのため、まともな数学者は乱数を研究しなかった。ヤコブ・ベルヌーイの乱数に関する論文も、結局本人は完成させることができず、論文完成に息子が着手しようとするも、これまた乱数はまともな数学者の取り組むべきところではないという周囲の圧力のために断念するなどの興味深い歴史がある。 +ベルヌーイとは数学者ヤコブ・ベルヌーイ(Jacob Bernoulli, 1655-1705)に由来する。ヤコブ・ベルヌーイは西洋数学史上、ジェロラモ・カルダーノ(Gerolamo Cardano, 1501-1576)に続く二人目の、数学的に乱数をまともに文章に書き残した数学者で、現在の統計の基礎を切り開いた人物だ。西洋数学史において乱数と統計の研究は遅れた。この理由は主に宗教的なもので、運命とは神の決定したもうことであり、人の子の及ぶところではないとする考え方が一般的だった。そのため、まともな数学者は乱数を研究しなかった。ヤコブ・ベルヌーイの乱数に関する論文も、けっきょく本人は完成させることができず、論文完成に息子が着手しようとするも、これまた乱数はまともな数学者の取り組むべきところではないという周囲の圧力のために断念するなどの興味深い歴史がある。 ### ベルヌーイ試行 ベルヌーイ試行とは、独立した試行で結果が2種類のものだ。 -「独立した試行」というのは、試行結果が前回の試行に影響されないことを言う。例えばコイントスの結果は表と裏だが、前回のコイントスの結果は今回のコイントスに影響しない。 +「独立した試行」というのは、試行結果が前回の試行に影響されないことをいう。例えばコイントスの結果は表と裏だが、前回のコイントスの結果は今回のコイントスに影響しない。 結果が2種類というのは、試行をした結果、2種類の結果のうちのどちらか一方が出ることを言う。成功/失敗、表/裏、勝ち/負け、`true`/`false`など、なんでもいい。数学的には成功/失敗を使うが、C++では`true`/`false`で表現する。 @@ -676,13 +676,13 @@ void f( Engine & e ) + 6面ダイスを振って6がでるか、6以外が出るか + 6面ダイスを振って5,6がでるか、1,2,3,4が出るか + 確率1%で当たるくじ引きの結果がアタリか、ハズレか -+ 赤玉と白玉が多数入ったツボの中身をよくかき混ぜ、玉をひとつだけ取り出し、戻す。引いた玉の色が赤か、白か ++ 赤玉と白玉が多数入ったツボの中身をよくかき混ぜ、玉を1つだけ取り出し、戻す。引いた玉の色が赤か、白か このような結果を2種類に分けることができ、そのうちのどちらか一方だけが結果として出る、かつ1回1回が独立した試行をベルヌーイ試行と呼ぶ。 ベルヌーイ分布を使うと、一様分布ではない2値(`true`/`false`)の確率的な結果について乱数で得ることができる。例えば、ビデオゲームで宝箱を開けると32%の確率でアイテムが入っており、68%の確率で空っぽであるとする。これを一様分布で実装すると、以下のようになる。 -~~~c++ +~~~cpp // 宝箱にアイテムが入っている場合trueを返す template < typename Engine > bool open_chest( Engine & e ) @@ -699,7 +699,7 @@ bool open_chest( Engine & e ) ### ベルヌーイ分布(std::bernoulli_distribution) -ベルヌーイ分布(bernoulli distribution)は一回のベルヌーイ試行の結果を乱数として返す。 +ベルヌーイ分布(bernoulli distribution)は1回のベルヌーイ試行の結果を乱数として返す。 `std::bernoulli_distribution`は`bool`型の乱数$b$を以下の離散確率関数に従って分布する。 @@ -809,7 +809,7 @@ $p=1.0$ならば常に`true`, $p=0.0$なら常に`false`、$p=0.5$ならば`true 二項分布(binomial distribution)は確率$p$で成功するベルヌーイ試行を$t$回行ったときに成功した回数$i$を乱数として返す。 具体的に例えると、100回コイントスをした結果出た表の数だ。コイントスは表と裏とそれぞれ -50%ずつの確率で出す。表を成功(`true`)とすると、つまり、$p=0.5$のベルヌーイ試行だ。100回コイントスをするというのは$t=100$だ。つまり、100回コイントスをした結果でた表の数というのは、100回ベルヌーイ試行した結果の成功数になる。この結果は、期待値としては50だが、ここで生成するのは乱数なので、50回出るわけではない。運が悪ければ1回も表が出ないこともあり得る。 +50%ずつの確率で出す。表を成功(`true`)とすると、つまり、$p=0.5$のベルヌーイ試行だ。100回コイントスをするというのは$t=100$だ。つまり、100回コイントスをした結果出た表の数というのは、100回ベルヌーイ試行した結果の成功数になる。この結果は、期待値としては50だが、ここで生成するのは乱数なので、50回出るわけではない。運が悪ければ1回も表が出ないこともあり得る。 6面ダイスを60回振った結果出た1の目の回数もそうだ。この場合、$p=\frac{1}{6}$のベルヌーイ試行を$t=60$回行うことになる。期待値は$10$だ。成功を2,3の目が出た回数と考えることもできる。この場合期待値は$20$だ。 @@ -863,7 +863,7 @@ auto coinflips100( Engine & e ) これを10回ぐらい呼んでみよう。 -~~~c++ +~~~cpp int main() { std::mt19937 e ; @@ -892,7 +892,7 @@ auto roll_for_one( Engine & e ) } ~~~ -60回のベルヌーイ試行をするので$t=60$で、ベルヌーイ試行の確率は6面ダイスの1の目が出る確率なので、$$p=\frac{1}{6}$になる。 +60回のベルヌーイ試行をするので$t=60$で、ベルヌーイ試行の確率は6面ダイスの1の目が出る確率なので、$p=\frac{1}{6}$になる。 確率1%で当たるくじを100回引いた結果アタリの回数を返す関数`lootbox`は以下のように実装できる。 @@ -913,11 +913,11 @@ auto lootbox( Engine & e ) 1, 0, 2, 1, 0, 0, 0, 0, 1, 3, ~~~ -確率1%で当たるくじを100回引くと、複数回当たることもあれば、1回も当たらないこともある。期待値は1だが、期待値というのは平均的に期待できる結果でしかない。読者諸君もくじ引きをするときは確率に気をつけよう。たとえくじが毎回公平であったとしても、確率は無記憶性なのだ。「もう90回くじを引いたから後10回引けば当たるはず」という考え方は通用しない。 +確率1%で当たるくじを100回引くと、複数回当たることもあれば、1回も当たらないこともある。期待値は1だが、期待値というのは平均的に期待できる結果でしかない。読者諸君もくじ引きをするときは確率に気を付けよう。たとえくじが毎回公平であったとしても、確率は無記憶性なのだ。「もう90回くじを引いたからあと10回引けば当たるはず」という考え方は通用しない。 ### 幾何分布(std::geometric_distribution) -幾何分布(geometric distribution)とは、確率$p$で成功するベルヌーイ試行をはじめて成功するまで行った回数を乱数として分布する。 +幾何分布(geometric distribution)とは、確率$p$で成功するベルヌーイ試行を初めて成功するまで行った回数を乱数として分布する。 具体的な例で例えると、 @@ -925,7 +925,7 @@ auto lootbox( Engine & e ) + 6面ダイスを1の目が出るまで振った回数 + 確率1%で当たるくじ引きをアタリが出るまで引いた回数 -コイントスの例で考えよう。コイントス一回をベルヌーイ試行とし、成功を表とする。表が出るまでコイントスをしてみよう。コイントスを何回する必要があるだろうか。運がよければ1回で表がでるので1回だ。運が悪ければ、5回コイントスをしても全部裏なこともあるだろう。100回コイントスをして表が一度も出ないことは、確率的にはありえる。ただしその確率は$\frac{1}{2^{100}}$なので、およそありえない確率ではある。 +コイントスの例で考えよう。コイントス1回をベルヌーイ試行とし、成功を表とする。表が出るまでコイントスをしてみよう。コイントスを何回する必要があるだろうか。運がよければ1回で表がでるので1回だ。運が悪ければ、5回コイントスをしても全部裏なこともあるだろう。100回コイントスをして表が一度も出ないことは、確率的にはありえる。ただしその確率は$\frac{1}{2^{100}}$なので、およそありえない確率ではある。 `std::geometric_distribution`は`IntType`型の乱数$i$, $i \geq 0$を以下の離散確率関数に従って分布する。 @@ -939,7 +939,7 @@ $$ std::geometric_distribution d( p ) ; ~~~ -`IntType`は整数型でデフォルトは`int`、`p`は確率で値の範囲は$0 < p < 1$だ。`p`の値の範囲に注意すること。0と1であってはならない。幾何分布は成功するまでベルヌーイ試行した回数をかえすので、$p=0$の場合、必ず失敗するベルヌーイ試行になり意味がない。$p=1$のときは必ず成功するベルヌーイ試行であり、やはり意味がない。 +`IntType`は整数型でデフォルトは`int`、`p`は確率で値の範囲は$0 < p < 1$だ。`p`の値の範囲に注意すること。0と1であってはならない。幾何分布は成功するまでベルヌーイ試行した回数を返すので、$p=0$の場合、必ず失敗するベルヌーイ試行になり意味がない。$p=1$のときは必ず成功するベルヌーイ試行であり、やはり意味がない。 `geometric_distribution`の生成する乱数の範囲にも注意が必要だ。生成される乱数$i$の範囲は$i \geq 0$だ。0もあり得る。0ということは、最初のベルヌーイ試行が成功したということだ。1は2回めのベルヌーイ試行が成功したということだ。幾何分布はベルヌーイ試行が初めて成功するまでのベルヌーイ試行の回数を返すので、成功したベルヌーイ試行は回数に含めない。 @@ -997,7 +997,7 @@ auto try_rolls( Engine & e ) 6面ダイスを振ると、運がよければ1回で1の目が出るが、運が悪いと何十回も振る必要がある。 -確率1%のくじをはじめて当たるまで引き続け、くじを引いた回数を返す関数`try_lootboxes`も書いてみよう。 +確率1%のくじを初めて当たるまで引き続け、くじを引いた回数を返す関数`try_lootboxes`も書いてみよう。 ~~~cpp template < typename Engine > @@ -1018,7 +1018,7 @@ unsigned int try_lootboxes( Engine & e ) ### 負の二項分布(std::negative_binomial_distribution) -負の二項分布(negative binomial distribution)は幾何分布に似ている。幾何分布がベルヌーイ試行が1回成功するまでに行ったベルヌーイ試行の回数を乱数として分布するのに対し、負の二項分布はベルヌーイ試行が`k`回成功するまでに行ったベルヌーイ試行の回数を乱数として分布する。 +負の二項分布(negative binomial distribution)は幾何分布に似ている。幾何分布がベルヌーイ試行が1回成功するまでに行ったベルヌーイ試行の回数を乱数として分布するのに対し、負の二項分布はベルヌーイ試行が$k$回成功するまでに行ったベルヌーイ試行の回数を乱数として分布する。 負の二項分布を具体的な例で考えよう @@ -1040,7 +1040,7 @@ $$ $p = 1$のときの$P(i\,|\,k,p)$は未定義だ。 -変数の宣言は以下の通り。 +変数の宣言は以下のとおり。 ~~~c++ std::negative_binomial_distribution d( k, p ) ; @@ -1094,7 +1094,7 @@ auto count_failed_coinflips_until_10_heads( Engine & e ) } ~~~ -参考までに、`n`回表が出るまでに行ったコイントスの回数を乱数で返す関数`count_n_coinflips`は以下の通り。 +参考までに、`n`回表が出るまでに行ったコイントスの回数を乱数で返す関数`count_n_coinflips`は以下のとおり。 ~~~cpp template < typename Engine > @@ -1105,7 +1105,7 @@ auto count_n_coinflips( unsigned int n, Engine & e ) } ~~~ -6面ダイスを10回、1の目が出るまで振った回数を乱数で返す関数`"count_10_rolls"`は以下の通り。 +6面ダイスを10回、1の目が出るまで振った回数を乱数で返す関数`"count_10_rolls"`は以下のとおり。 ~~~cpp template < typename Engine > @@ -1116,7 +1116,7 @@ auto count_10_rolls( Engine & e ) } ~~~ -確率1%のくじを10回当てるまでくじを引いた回数を返す関数`count_10_lootbox`は以下の通り。 +確率1%のくじを10回当てるまでくじを引いた回数を返す関数`count_10_lootbox`は以下のとおり。 ~~~cpp template < typename Engine > diff --git a/044-random-part2.md b/044-random-part2.md index c623ca8..cd3435b 100644 --- a/044-random-part2.md +++ b/044-random-part2.md @@ -11,9 +11,9 @@ + 1時間である放射性同位体が放射性崩壊する回数 -冤罪や交通事故の発生件数は乱数ではないように考えるかも知れない。しかし、結果的にみれば乱数のように振る舞っている。一ヶ月に発生する交通事故が10件であったとする。これは平均すると約3日に1回交通事故が起こっていることになるが、実際に3日に一回交通事故が起こったわけではない。交通事故の発生は離散的で、1日に複数件起こることもあれば、一週間無事故のときもある。なので3日に一回交通規制を敷いても交通事故を防ぐことはできない。 +冤罪や交通事故の発生件数は乱数ではないように考えるかもしれない。しかし、結果的にみれば乱数のように振る舞っている。1ヶ月に発生する交通事故が10件であったとする。これは平均すると約3日に1回交通事故が起こっていることになるが、実際に3日に1回交通事故が起こったわけではない。交通事故の発生は離散的で、1日に複数件起こることもあれば、1週間無事故のときもある。なので3日に1回交通規制を敷いても交通事故を防ぐことはできない。 -ポアソン分布に従う乱数の特徴としてもう一つ、無記憶性というものがある。3日間に1件の割合で交通事故が起こっているとしよう。その場合、常に今から3日以内に1件の交通事故が起きることが期待できるだけであって、3日以内に必ず起こるわけではない。そして、2日待ったから明日交通事故が起こるというわけでもない。交通事故が起こる確率は常に今から3日間につき1件だ。ポアソン分布は無記憶性なので、すでに2日間待っているという過去は未来に影響しない。 +ポアソン分布に従う乱数の特徴としてもう1つ、無記憶性というものがある。3日間に1件の割合で交通事故が起こっているとしよう。その場合、常にいまから3日以内に1件の交通事故が起きることが期待できるだけであって、3日以内に必ず起こるわけではない。そして、2日待ったから明日交通事故が起こるというわけでもない。交通事故が起こる確率は常にいまから3日間につき1件だ。ポアソン分布は無記憶性なので、すでに2日間待っているという過去は未来に影響しない。 具体的なC++ライブラリのポアソン分布の使い方としては、ある所定の時間に平均して起こる事象の回数`mean`を指定すると、その所定の時間に起こった事象が乱数で返される。 @@ -28,7 +28,7 @@ $$ ここで$\mu$を`mean`とする。$\mu > 0$ではない場合未定義だ。 -変数の宣言は以下の通り。 +変数の宣言は以下のとおり。 ~~~c++ std::poisson_distribution d( mean ) ; @@ -80,9 +80,9 @@ auto traffic_accidents( Engine & e ) ポアソン分布による乱数は例えば、 -+ 一ヶ月に平均して10件発生する交通事故がある一ヶ月に発生した件数 ++ 1ヶ月に平均して10件発生する交通事故がある1ヶ月に発生した件数 -が乱数だった。一ヶ月が時間間隔で、交通事故が事象だ。10件が平均だ。 +が乱数だった。1ヶ月が時間間隔で、交通事故が事象だ。10件が平均だ。 抽象的に書くと、 @@ -92,9 +92,9 @@ auto traffic_accidents( Engine & e ) 指数分布では具体的には以下のようになる。 -+ 一ヶ月に平均して10件発生する交通事故が発生してから、次の交通事故が発生するまでの時間間隔 ++ 1ヶ月に平均して10件発生する交通事故が発生してから、次の交通事故が発生するまでの時間間隔 -ポアソン分布に従う離散的な事象のある時間間隔における平均の発生回数が与えられているとする。例えば上の場合、交通事故が事象で、「一ヶ月」が時間間隔で、「平均して10件」が平均の発生回数だ。平均して約3日に1件ほど発生していることになる。ところで今まさに交通事故が発生したとする。このとき、次の交通事故が発生するまでの時間間隔はどのくらいだろうか。平均すると約3日に1件だが、交通事故は離散的な事象なので、1時間後にまた起きるかも知れないし、一週間交通事故が起こらないかも知れない。長期的に統計を取ると月に平均して10件発生している場合、次の交通事故が発生するまでの時間間隔を集計して平均すると約3日に1件発生する確率になる。 +ポアソン分布に従う離散的な事象のある時間間隔における平均の発生回数が与えられているとする。例えば上の場合、交通事故が事象で、「1ヶ月」が時間間隔で、「平均して10件」が平均の発生回数だ。平均して約3日に1件ほど発生していることになる。ところでいままさに交通事故が発生したとする。このとき、次の交通事故が発生するまでの時間間隔はどのくらいだろうか。平均すると約3日に1件だが、交通事故は離散的な事象なので、1時間後にまた起きるかもしれないし、1週間交通事故が起こらないかもしれない。長期的に統計を取ると月に平均して10件発生している場合、次の交通事故が発生するまでの時間間隔を集計して平均すると約3日に1件発生する確率になる。 指数分布が扱うのはこの次の交通事故が発生するまでの時間間隔だ。抽象的にもう一度書くと、ポアソン分布に従う離散的な事象の平均回数が与えられている場合に、ある事象から次の事象が発生するまでの時間間隔を分布する。 @@ -104,7 +104,7 @@ $$ p(x\,|\,\lambda) = \lambda e^{-\lambda x} \text{ .} $$ -変数の宣言方法は以下の通り。 +変数の宣言方法は以下のとおり。 ~~~c++ std::exponential_distribution d( lambda ) ; @@ -127,7 +127,7 @@ int main() } ~~~ -一ヶ月に10件の交通事故がポアソン分布に従って発生する場合に、ある交通事故から次の交通事故までの時間間隔の乱数を日数で得る関数`until_next_traffic_accident`は以下のように書く。 +1ヶ月に10件の交通事故がポアソン分布に従って発生する場合に、ある交通事故から次の交通事故までの時間間隔の乱数を日数で得る関数`until_next_traffic_accident`は以下のように書く。 ~~~cpp template < typename Engine > @@ -138,7 +138,7 @@ auto until_next_traffic_accident( Engine & e ) } ~~~ -ある時間間隔に10回起こるので、`lambda`には`10.0`を指定する。ここでは簡単のために一ヶ月を30日とする。結果の乱数は1.0がある時間間隔に等しいので、つまり1.0は30日に等しい。結果に`30.0`をかけることで日数を計算する。 +ある時間間隔に10回起こるので、`lambda`には`10.0`を指定する。ここでは簡単のために1ヶ月を30日とする。結果の乱数は1.0がある時間間隔に等しいので、つまり1.0は30日に等しい。結果に`30.0`を掛けることで日数を計算する。 この関数を10回呼び出すと以下のようになった。 @@ -160,13 +160,13 @@ $$ $\alpha$を`alpha`、$\beta$を`beta`とする。 -変数の宣言は以下の通り。 +変数の宣言は以下のとおり。 ~~~c++ std::gamma_distribution d( alpha, beta ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`double`。`alpha`, `beta`は`RealType`型。値の範囲は$0 < alpha$, $0 < beta$ +`RealType`は浮動小数点数型でデフォルトは`double`。`alpha`, `beta`は`RealType`型。値の範囲は$0 < alpha$, $0 < beta$。 使い方。 @@ -193,13 +193,13 @@ p(x\,|\,a,b) = \frac{a}{b} \text{ .} $$ -変数の宣言は以下の通り。 +変数の宣言は以下のとおり。 ~~~c++ std::weibull_distribution d( a, b ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`double`。`a`, `b`は`RealType`型。値の範囲は$0 < a$, $0 < b$ +`RealType`は浮動小数点数型でデフォルトは`double`。`a`, `b`は`RealType`型。値の範囲は$0 < a$, $0 < b$。 使い方。 @@ -228,13 +228,13 @@ $$ 極値分布(extreme value distribution)は、ガンベルI型(Gumbel Type I)、対数ウェイブル(log-Weibull)、フィッシャー=ティペットI型(Fisher-Tippett Type I)という名前の分布と呼ばれることもある。 -変数の宣言は以下の通り。 +変数の宣言は以下のとおり。 ~~~c++ std::extreme_value_distribution d( a, b ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`double`。`a`, `b`は`RealType`型。値の範囲は$0 < b$ +`RealType`は浮動小数点数型でデフォルトは`double`。`a`, `b`は`RealType`型。値の範囲は$0 < b$。 使い方。 diff --git a/045-random-part3.md b/045-random-part3.md index ffc259d..d832030 100644 --- a/045-random-part3.md +++ b/045-random-part3.md @@ -2,8 +2,7 @@ ### 正規分布(`std::normal_distribution`) -`std::normal_distribution`は浮動小数点数型の乱数$x$ -以下の確率密度関数に従って分布する。 +`std::normal_distribution`は浮動小数点数型の乱数$x$を以下の確率密度関数に従って分布する。 $$ (x\,|\,\mu,\sigma) @@ -25,7 +24,7 @@ $$ std::normal_distribution d( mean, stddev ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`double`。`mean`, `stddev`は浮動小数点数型。`mean`は平均。`stddev`は標準偏差で値の範囲は$0 < \text{stddev}$ +`RealType`は浮動小数点数型でデフォルトは`double`。`mean`, `stddev`は浮動小数点数型。`mean`は平均。`stddev`は標準偏差で値の範囲は$0 < \text{stddev}$。 使い方。 @@ -93,7 +92,7 @@ $$ std::chi_squared_distribution d( n ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`double`。`n`は`RealType`型。値の範囲は$0 < n$ +`RealType`は浮動小数点数型でデフォルトは`double`。`n`は`RealType`型。値の範囲は$0 < n$。 使い方。 @@ -125,7 +124,7 @@ $$ std::cauchy_distribution d( a, b ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`double`。`a`, `b`は`RealType`型。値の範囲は$0 < b$ +`RealType`は浮動小数点数型でデフォルトは`double`。`a`, `b`は`RealType`型。値の範囲は$0 < b$。 使い方。 @@ -144,9 +143,9 @@ int main() } ~~~ -### フィッシャーのF分布(`std::fisher_f_distribution`) +### フィッシャーの$F$分布(`std::fisher_f_distribution`) -フィッシャーのF分布(Fisher's F-distribution)の名前は数学者サー・ロナルド・エイルマー・フィッシャー(Sir Ronald Aylmer Fisher)に由来する。 +フィッシャーの$F$分布(Fisher's $F$-distribution)の名前は数学者サー・ロナルド・エイルマー・フィッシャー(Sir Ronald Aylmer Fisher)に由来する。 `std::fisher_f_distribution`は浮動小数点数の乱数$x > 0$を以下の関数密度関数に従って分布する。 @@ -164,7 +163,7 @@ $$ std::fisher_f_distribution d( m, n ) ; ~~~ -`RealType`は浮動小数点数型でデフォルトは`dobule`。`m`, `n`は`RealType`型。値の範囲は$0 < m$ かつ $0 < n$ +`RealType`は浮動小数点数型でデフォルトは`dobule`。`m`, `n`は`RealType`型。値の範囲は$0 < m$ かつ $0 < n$。 使い方。 @@ -179,11 +178,11 @@ int main() } ~~~ -### スチューデントのt分布(`std::student_t_distribution`) +### スチューデントの$t$分布(`std::student_t_distribution`) -スチューデントのt分布(Student's t-distribution)はウィリアム・シーリー・ゴセットによって考案された。当時、ウィリアムはギネス醸造所で働いていたが、ギネスは従業員に科学論文を発表することを禁じていたために、ウィリアムはスチューデントという偽名で発表した。 +スチューデントの$t$分布(Student's $t$-distribution)はウィリアム・シーリー・ゴセットによって考案された。当時、ウィリアムはギネス醸造所で働いていたが、ギネスは従業員に科学論文を発表することを禁じていたために、ウィリアムはスチューデントという偽名で発表した。 -`std::student_t_distribution`は浮動小数点数型の乱数$x$お以下の確率密度関数に従って分布する。 +`std::student_t_distribution`は浮動小数点数型の乱数$x$を以下の確率密度関数に従って分布する。 $$ p(x\,|\,n) = \frac{1}{\sqrt{n \pi}} diff --git a/200-cpp.md b/200-cpp.md index 5732cc5..ff864a0 100644 --- a/200-cpp.md +++ b/200-cpp.md @@ -2,7 +2,7 @@ CプリプロセッサーはC++がC言語から受け継いだ機能だ。CプリプロセッサーはソースコードをC++としてパースする前に、テキストをトークン単位で変形する処理のことだ。この処理はソースファイルをC++としてパースする前処理として行われる。CプリプロセッサーはC++ではなく別言語として認識すべきで、そもそもプログラミング言語ではなくマクロ言語だ。 -C++ではCプリプロセッサーが広く使われており、今後もしばらくは使われるだろう。読者がC++で書かれた既存のコードを読む時、Cプリプロセッサーは避けて通れない。Cプリプロセッサーはいずれ廃止したい機能ではあるが、C++は未だに廃止できていない。 +C++ではCプリプロセッサーが広く使われており、今後もしばらくは使われるだろう。読者がC++で書かれた既存のコードを読むとき、Cプリプロセッサーは避けて通れない。Cプリプロセッサーはいずれ廃止したい機能ではあるが、C++はいまだに廃止できていない。 Cプリプロセッサーはプリプロセッシングディレクティブ(preprocessing directive)を認識し、トークン列を処理する。ディレクティブはソースファイルの文頭に文字`#`から始まり、改行文字で終わる。`#`とディレクティブの間に空白文字を入れてもよい。 @@ -26,14 +26,14 @@ Cプリプロセッサーはプリプロセッシングディレクティブ(pre 例えば、以下のようなヘッダーファイル`foo.h`があり、 -~~~c++ +~~~cpp // foo.h foo foo foo ~~~ 以下のようなソースファイル`bar.cpp`がある場合、 -~~~c++ +~~~cpp // bar.cpp #include "foo.h" @@ -43,7 +43,7 @@ foo foo foo `bar.cpp`をCプリプロセッサーにかけると、以下のようなソースファイルが出力される -~~~c++ +~~~cpp // bar.cpp // foo.h @@ -56,9 +56,9 @@ foo foo foo 冒頭で述べたように、`#include`の本質はコンパイラーによるコピペである。あるテキストファイルの内容をその場に挿入するコピペ機能を提供する。 -`#include`は、他の言語でモジュール、importなどと呼ばれている機能を簡易的に提供する。C++の標準ライブラリを使うには、``や``や``のようなヘッダーファイルを`#include`する必要がある。 +`#include`は、ほかの言語でモジュール、importなどと呼ばれている機能を簡易的に提供する。C++の標準ライブラリを使うには、``や``や``のようなヘッダーファイルを`#include`する必要がある。 -~~~c++ +~~~cpp // iostreamライブラリを使う #include // stringライブラリを使う @@ -73,27 +73,27 @@ int main() } ~~~ -すでに述べたように`#include`はファイルの内容をその場に挿入するだけであり、他の言語にあるモジュールのための高級な機能ではない。本書を執筆時点で規格策定中のC++20では、より高級なモジュール機能を追加する予定がある。 +すでに述べたように`#include`はファイルの内容をその場に挿入するだけであり、ほかの言語にあるモジュールのための高級な機能ではない。本書を執筆時点で規格策定中のC++20では、より高級なモジュール機能を追加する予定がある。 同じヘッダーファイルを複数回`#include`すると、当然複数回挿入される。 以下のような`val.h`を、 -~~~c++ +~~~cpp // val.h inline int val ; ~~~ 以下のように複数回`#include`すると、 -~~~c++ +~~~cpp #include "val.h" #include "val.h" ~~~ 以下のように置換される。 -~~~c++ +~~~cpp // val.h inline int val ; // val.h @@ -102,9 +102,9 @@ inline int val ; これは`val`の定義が重複しているためエラーとなる。 -しかし、ヘッダーファイルを一度しか`#include`しないようにするのは困難だ。なぜならば、ヘッダーファイルは他のヘッダーファイルから間接的に`#include`されることもあるからだ。 +しかし、ヘッダーファイルを一度しか`#include`しないようにするのは困難だ。なぜならば、ヘッダーファイルはほかのヘッダーファイルから間接的に`#include`されることもあるからだ。 -~~~c++ +~~~cpp // lib_f.h #include "val.h" @@ -112,7 +112,7 @@ inline int val ; int f() ; ~~~ -~~~c++ +~~~cpp // lib_g.h #include "val.h" @@ -120,7 +120,7 @@ int f() ; int g() ; ~~~ -~~~c++ +~~~cpp // main.cpp #include "lib_f.h" @@ -134,7 +134,7 @@ int main() この`main.cpp`をCプリプロセッサーにかけると以下のように置換される。 -~~~c++ +~~~cpp // main.cpp // lib_f.h @@ -162,7 +162,7 @@ int main() この問題に対処するためには、複数回`#include`されると困るヘッダーファイルでは、インクルードガード(include guard)と呼ばれている方法を使う。 -~~~c++ +~~~cpp // val.h #ifndef INCLUDE_GUARD_HEADER_VAL_H @@ -194,15 +194,15 @@ inline int val ; ### オブジェクト風マクロ -オブジェクト風マクロの文法は以下の通り +オブジェクト風マクロの文法は以下のとおり。 -~~~ +~~~c++ #define マクロ名 置換リスト 改行文字 ~~~ -`#define`以降の行では、マクロ名が置換リストに置き換わる +`#define`以降の行では、マクロ名が置換リストに置き換わる。 -~~~c++ +~~~cpp #define ONE 1 #define ONE_PLUS_ONE ONE + ONE #define GNU GNU's is NOT UNIX @@ -213,7 +213,7 @@ ONE_PLUS_ONE これをプリプロセスすると以下のソースコードになる。 -~~~c++ +~~~cpp 1 1 + 1 ~~~ @@ -224,7 +224,7 @@ ONE_PLUS_ONE あるマクロ名を置換した結果、そのマクロ名が現れても再帰的に置換されることはない。 -~~~c++ +~~~cpp #define GNU GNU's NOT UNIX! GNU @@ -232,7 +232,7 @@ GNU これは以下のように置換される。 -~~~c++ +~~~cpp GNU's NOT UNIX! ~~~ @@ -240,15 +240,15 @@ GNU's NOT UNIX! ### 関数風マクロ -関数風マクロの文法は以下の通り。 +関数風マクロの文法は以下のとおり。 -~~~ +~~~c++ #define マクロ名( 識別子リスト ) 置換リスト 改行文字 ~~~ 関数風マクロはあたかも関数のように記述できる。関数風マクロに実引数として渡したトークン列は、置換リスト内で仮引数としての識別子で参照できる。 -~~~c++ +~~~cpp #define NO_ARGUMENT() No argument #define ONE_ARGUMENT( ARG ) begin ARG end #define MAKE_IT_DOUBLE( ARG ) ONE_ARGUMENT( ARG ARG ) @@ -260,7 +260,7 @@ MAKE_IT_DOUBLE( foo bar ) これは以下のように置換される。 -~~~c++ +~~~cpp No argument begin foo bar end begin foo bar foo bar end @@ -268,7 +268,7 @@ begin foo bar foo bar end 複数の引数を取るマクロへの実引数は、カンマで区切られたトークン列を渡す。 -~~~c++ +~~~cpp #define TWO( A, B ) A B #define THREE( A, B, C ) C B A @@ -278,14 +278,14 @@ THREE( 1, 2, 3 ) これは以下のように置換される。 -~~~c++ +~~~cpp 1 2 3 4 3 2 1 ~~~ ただし、括弧で囲まれたトークン列の中にあるカンマは、マクロの実引数の区切りとはみなされない。 -~~~c++ +~~~cpp #define MACRO( A ) A MACRO( (a,b) ) @@ -293,15 +293,15 @@ MACRO( (a,b) ) これは以下のように置換される。 -~~~ +~~~cpp (a,b) ~~~ ### `__VA_ARGS__`(可変長引数マクロ) -`#define`の識別子リストを`...`だけにしたマクロは、可変長引数マクロになる。このときマクロの実引数のトークン列は、置換リストのなかで`__VA_ARGS__`として参照できる。 +`#define`の識別子リストを`...`だけにしたマクロは、可変長引数マクロになる。このときマクロの実引数のトークン列は、置換リストの中で`__VA_ARGS__`として参照できる。 -~~~c++ +~~~cpp #define MACRO(...) __VA_ARGS__ MACRO( You can write , and ,, or even ,,,, ) @@ -309,7 +309,7 @@ MACRO( You can write , and ,, or even ,,,, ) これは以下のように置換される。 -~~~ +~~~cpp You can write , and ,, or even ,,,, ~~~ @@ -317,7 +317,7 @@ You can write , and ,, or even ,,,, 可変長引数マクロの識別子リストに仮引数と`...`を書いたマクロの置換リストでは、仮引数の数だけの実引数は仮引数で参照され、残りが`__VA_ARGS__`で参照される。 -~~~c++ +~~~cpp #define MACRO( X, Y, Z, ... ) X Y Z and __VA_ARGS__ MACRO( 1,2,3,4,5,6 ) @@ -325,11 +325,11 @@ MACRO( 1,2,3,4,5,6 ) これは以下のように置換される -~~~ +~~~cpp 1 2 3 and 4,5,6 ~~~ -X, Y, Zにそれぞれ1, 2, 3が入り、`__VA_ARGS__`には`4,5,6`が入る。 +`X`, `Y`, `Z`にそれぞれ`1`, `2`, `3`が入り、`__VA_ARGS__`には`4`, `5`, `6`が入る。 ### `__VA_OPT__` @@ -337,7 +337,7 @@ X, Y, Zにそれぞれ1, 2, 3が入り、`__VA_ARGS__`には`4,5,6`が入る。 `__VA_OPT__`は可変引数マクロの置換リストでのみ使える。`__VA_OPT__(content)`は`__VA_ARGS__`にトークンがない場合はトークンなしに置換され、トークンがある場合はトークン列`content`に置換される。 -~~~c++ +~~~cpp #define MACRO( X, ... ) f( X __VA_OPT__(,) __VA_ARGS__ ) MACRO(1) @@ -346,14 +346,14 @@ MACRO(1,2) これは以下のように置換される。 -~~~ +~~~cpp f( 1 ) f( 1, 2 ) ~~~ `MACRO(1)`は`X`が`1`になり、`__VA_ARGS__`にはトークンがないので、`__VA_OPT__(,)`は空に置換される。結果として`f(1)`となる。 -`MACRO(1,2)`は、Xが1になり、`__VA_ARGS__`にはトークン`2`が入るので、`__VA_OPT__(,)`は`,`に置換される。結果として`f(1,2)`となる。 +`MACRO(1,2)`は、`X`が`1`になり、`__VA_ARGS__`にはトークン`2`が入るので、`__VA_OPT__(,)`は`,`に置換される。結果として`f(1,2)`となる。 `__VA_OPT__`は`__VA_ARGS__`に実引数となるトークン列がなければ空に置換されるので、このようにトークン列の有無によってカンマなどの文法上必須のトークン列の有無を切り替えたい場合に使うことができる。 @@ -363,7 +363,7 @@ f( 1, 2 ) `#`は関数風マクロの置換リストの中のみで使うことができる。`#`は関数風マクロの仮引数の識別子の直前に書くことができる。`#`が直前に書かれた識別子は、マクロ実引数のトークン列の文字列リテラルになる。 -~~~c++ +~~~cpp #define STRING( X ) # X STRING( hello ) @@ -372,14 +372,14 @@ STRING( hello world ) これは以下のように置換される。 -~~~ +~~~cpp "hello" "hello world" ~~~ また、可変長マクロと組み合わせた場合、 -~~~c++ +~~~cpp #define STRING( ... ) # __VA_ARGS__ STRING() @@ -388,7 +388,7 @@ STRING( hello,world ) 以下のように置換される。 -~~~c++ +~~~cpp "" "hello,world" ~~~ @@ -399,7 +399,7 @@ STRING( hello,world ) `##`は関数風マクロの置換リストの中にしか書けない。`##`は両端にマクロの仮引数の識別子を書かなければならない。`##`は両端の識別子の参照するマクロ実引数のトークン列を結合した置換を行う。 -~~~c++ +~~~cpp #define CONCAT( A, B ) A ## B CONCAT( foo, bar ) @@ -408,14 +408,14 @@ CONCAT( aaa bbb, ccc ddd) これは以下のように置換される。 -~~~ +~~~cpp foobar aaa bbbccc ddd ~~~ -結合した結果のトークンは更にマクロ置換の対象となる。 +結合した結果のトークンはさらにマクロ置換の対象となる。 -~~~c++ +~~~cpp #define CONCAT( A, B ) A ## B #define FOOBAR hello @@ -424,7 +424,7 @@ CONCAT( FOO, BAR ) これは以下のように置換される。 -~~~ +~~~cpp hello ~~~ @@ -436,7 +436,7 @@ hello しかし、関数やクラスを生成するような複雑なマクロは、複数行に分けて書きたい。 -~~~c++ +~~~cpp #define LIST_NAME2( PREFIX, TYPE ) PREFIX ## TYPE #define LIST_NAME( TYPE ) LIST_NAME2( list_, TYPE ) @@ -450,7 +450,7 @@ DEFINE_LIST(double) 上の例は以下のように、プリプロセッサーとしては比較的わかりやすく書くことができる。 -~~~c++ +~~~cpp #define LIST_NAME2( PREFIX, TYPE ) PREFIX ## TYPE #define LIST_NAME( TYPE ) LIST_NAME2( list_, TYPE ) @@ -473,7 +473,7 @@ C++ではテンプレートがあるために、このようなマクロを書 `#undef`はそれ以前に定義されたマクロを削除する。 -~~~c++ +~~~cpp #define FOO BAR FOO #undef FOO @@ -482,7 +482,7 @@ FOO これは以下のように置換される。 -~~~ +~~~cpp BAR FOO ~~~ @@ -493,25 +493,25 @@ FOO ### プリプロセッサーの定数式 -プリプロセッサーで使える条件式は、C++の条件式とは比べてだいぶ制限がある。基本的には整数定数式で、`true`, `false`が使える他、`123`, `1+1`, `1 == 1`, `1 < 1`のような式も使える。ただし、識別子はすべてマクロ名として置換できるものは置換され、置換できない識別子は、`true`, `false`以外はキーワードも含めてすべて`0`に置換される。 +プリプロセッサーで使える条件式は、C++の条件式とは比べてだいぶ制限がある。基本的には整数定数式で、`true`, `false`が使えるほか、`123`, `1+1`, `1 == 1`, `1 < 1`のような式も使える。ただし、識別子はすべてマクロ名として置換できるものは置換され、置換できない識別子は、`true`, `false`以外はキーワードも含めてすべて`0`に置換される。 したがって、プリプロセッサーで以下のように書くと、 -~~~c++ +~~~cpp #if UNDEFINED #endif ~~~ 以下のように書いたものと同じになる。 -~~~c++ +~~~cpp #if 0 #endif ~~~ プリプロセッサーであるので、C++としての`constexpr`変数や`constexpr`関数も使えない。 -~~~c++ +~~~cpp constexpr int x = 1 ; #if x @@ -521,7 +521,7 @@ hello これは以下のように置換される。 -~~~ +~~~cpp constexpr int x = 1 ; ~~~ @@ -529,7 +529,7 @@ constexpr int x = 1 ; 以下の例はエラーになる。 -~~~c++ +~~~cpp constexpr int f() { return 1 ; } #if f() @@ -540,16 +540,16 @@ constexpr int f() { return 1 ; } プリプロセッサーの定数式では、特殊なマクロ風の式を使うことができる。`defined`と`__has_include`だ。 -`defined`は以下の文法を持つ +`defined`は以下の文法を持つ。 -~~~ +~~~c++ defined 識別子 defined ( 識別子 ) ~~~ `defined`は識別子がそれ以前の行で`#define`でマクロとして定義されていて`#undef`で取り消されていない場合`1`になり、それ以外の場合`0`になる。 -~~~c++ +~~~cpp // #if 0 #if defined MACRO #endif @@ -569,7 +569,7 @@ defined ( 識別子 ) `__has_include`は以下の文法を持つ。 -~~~c++ +~~~cpp __has_include ( < ヘッダーファイル名 > ) __has_include ( " ヘッダーファイル名 " ) __has_include ( 文字列リテラル ) @@ -578,7 +578,7 @@ __has_include ( < マクロ > ) 1番目と2番目は、指定されたヘッダーファイル名がシステムに存在する場合`1`に、そうでない場合`0`になる。 -~~~c++ +~~~cpp // の存在を確認してから#includeする #if __has_include() # include @@ -592,7 +592,7 @@ __has_include ( < マクロ > ) 3番目と4番目は、1番目と2番目が適用できない場合に初めて考慮される。その場合、まず通常通りにプリプロセッサーのマクロ置換が行われる。 -~~~c++ +~~~cpp #define STDIO "stdio.h" #if __has_include( STDIO ) @@ -608,7 +608,7 @@ __has_include ( < マクロ > ) `#if`ディレクティブは以下の文法を持つ。 -~~~ +~~~c++ #if 定数式 改行文字 #endif @@ -616,7 +616,7 @@ __has_include ( < マクロ > ) もし定数式がゼロの場合、`#if`と`#endif`で囲まれたトークン列は処理されない。定数式が非ゼロの場合、処理される。 -~~~c++ +~~~cpp #if 0 This line will be skipped. #endif @@ -628,7 +628,7 @@ This line will be processed. これをプリプロセスすると以下のようになる。 -~~~c++ +~~~cpp This line will be processed. ~~~ @@ -639,7 +639,6 @@ This line will be processed. `#elif`ディレクティブは、C++でいう`else if`に相当する。 ~~~c++ - #elif 定数式 改行文字 ~~~ @@ -647,7 +646,7 @@ This line will be processed. 以下の例は、すべて`YES`のトークンがある行のみ処理される。 -~~~c++ +~~~cpp #if 1 YES #elif 1 @@ -677,9 +676,9 @@ YES #endif ~~~ -プリプロセスした結果は以下の通り、 +プリプロセスした結果は以下のとおり。 -~~~ +~~~cpp YES YES YES @@ -694,7 +693,7 @@ YES 以下の例は、`YES`のトークンがある行のみ処理される。 -~~~c++ +~~~cpp #if 1 YES #else @@ -718,21 +717,21 @@ NO ### \#ifdef, \#ifndefディレクティブ -~~~ +~~~c++ #ifdef 識別子 #ifndef 識別子 ~~~ は、それぞれ以下と同じ意味になる。 -~~~ +~~~c++ #if defined 識別子 #if !defined 識別子 ~~~ 例、 -~~~c++ +~~~cpp #ifdef MACRO #endif @@ -755,7 +754,7 @@ NO 以下の文法の`#line`ディレクティブは、`#line`ディレクティブの次の行の行番号をあたかも数値で指定した行番号であるかのように振る舞わせる。 -~~~ +~~~c++ #line 数値 改行文字 ~~~ @@ -763,7 +762,7 @@ NO 以下の例はコンパイルエラーになるが、コンパイルエラーメッセージはあたかも102行目に問題があるかのように表示される。 -~~~c++ +~~~cpp // 1行目 // 2行目 #line 100 // 3行目 @@ -785,7 +784,7 @@ int main() 以下の文法の`#line`ディレクティブは、次の行の行番号を数値にした上で、ソースファイル名をソースファイル名にする。 -~~~v++ +~~~c++ #line 数値 "ソースファイル名" 改行文字 ~~~ @@ -812,7 +811,7 @@ int main() `#error`ディレクティブはコンパイルエラーを引き起こす。 -~~~ +~~~c++ #error 改行文字 #error トークン列 改行文字 ~~~ @@ -821,7 +820,7 @@ int main() `#error`の利用例としては、`#if`と組み合わせるものがある。以下の例は`CHAR_BIT`が8でなければコンパイルエラーになるソースファイルだ。 -~~~c++ +~~~cpp #include #if CHAR_BIT != 8 @@ -835,9 +834,9 @@ int main() `#pragma`ディレクティブは実装依存の処理を行う。`#pragma`はコンパイラー独自の拡張機能を追加する文法として使われている。 -文法は以下の通り。 +文法は以下のとおり。 -~~~ +~~~c++ #pragma プリプロセッサートークン列 改行文字 ~~~ @@ -847,7 +846,7 @@ C++では属性が追加されたために、`#pragma`を使う必要はほと `Null`ディレクティブとは何もしないプリプロセッサーディレクティブだ。 -~~~ +~~~c++ # 改行文字 ~~~ @@ -855,7 +854,7 @@ C++では属性が追加されたために、`#pragma`を使う必要はほと ## 定義済みマクロ名 -いくつかのマクロ名がプリプロセッサーによって予め定義されている。 +いくつかのマクロ名がプリプロセッサーによってあらかじめ定義されている。 ---------------------------------------------------------------------------- マクロ名 値 意味 diff --git a/300-multiple-source-files.md b/300-multiple-source-files.md index 4f97e7e..02841fe 100644 --- a/300-multiple-source-files.md +++ b/300-multiple-source-files.md @@ -1,15 +1,15 @@ # 分割コンパイル -これまで、プログラムはひとつのソースファイルから作っていた。プログラムは複数のソースファイルから作ることもできる。ソースファイルを複数に分割することで、ソースファイルの管理がしやすくなったり、プログラムのビルド時間の短縮にもつながる。 +これまで、プログラムは1つのソースファイルから作っていた。プログラムは複数のソースファイルから作ることもできる。ソースファイルを複数に分割することで、ソースファイルの管理がしやすくなったり、プログラムのビルド時間の短縮にもつながる。 ### ソースファイルとコンパイル -ソースファイルを分割すると、C++の書き方にも注意が必要になる。だがその前に、複数のソースファイルをコンパイルしてひとつのプログラムにする方法を学ぶ。 +ソースファイルを分割すると、C++の書き方にも注意が必要になる。だがその前に、複数のソースファイルをコンパイルして1つのプログラムにする方法を学ぶ。 ### 単一のソースファイルのコンパイル -C++のソースファイルをコンパイルして実行可能ファイルを作る方法を今一度おさらいをしよう。 +C++のソースファイルをコンパイルして実行可能ファイルを作る方法をいま一度おさらいをしよう。 `source.cpp`という名前のソースファイルがあるとき、ここから`program`という名前の実行可能ファイルを作るには、 @@ -19,25 +19,25 @@ $ g++ -o program source.file としていた。毎回このコマンドを入力するのは面倒なので、`Makefile`を以下のように書いていた。 -~~~ +~~~makefile program: source.cpp g++ $< -o $@ ~~~ ### ヘッダーファイルはコピペ -すでに、ソースファイルの他にヘッダーファイルというファイルも使っている。ヘッダーファイルはソースファイルではない。コンパイル前にソースファイルにコピペされるだけのものだ。 +すでに、ソースファイルのほかにヘッダーファイルというファイルも使っている。ヘッダーファイルはソースファイルではない。コンパイル前にソースファイルにコピペされるだけのものだ。 例えば以下のような内容の`header.h`というヘッダーファイルがあるとして、 -~~~c++ +~~~cpp // header.h ++i ; ~~~ `source.cpp`が以下のようであるとき、 -~~~c++ +~~~cpp int main() { int i = 0 ; @@ -50,8 +50,7 @@ int main() `source.cpp`をコンパイルすると、まずヘッダーファイルが以下のように展開される。 -~~~c++ - +~~~cpp int main() { int i = 0 ; @@ -79,7 +78,7 @@ $ g++ -o program foo.cpp bar.cpp ## オブジェクトファイル -単にソースファイルを分割したいだけならば、GCCに分割したソースファイルをすべて指定すればよい。しかしその場合、複数あるソースファイルのひとつだけを編集した場合でも、すべてのソースファイルをコンパイルしなければならない。 +単にソースファイルを分割したいだけならば、GCCに分割したソースファイルをすべて指定すればよい。しかしその場合、複数あるソースファイルの1つだけを編集した場合でも、すべてのソースファイルをコンパイルしなければならない。 C++では伝統的に、ソースファイルを部分的にコンパイルしてオブジェクトファイルを生成し、オブジェクトファイルをリンクしてプログラムを生成する方法がある。 @@ -89,6 +88,7 @@ C++では伝統的に、ソースファイルを部分的にコンパイルし TODO: 図示 ソースファイル→(コンパイラー)→オブジェクトファイル→(リンカー)→プログラム ~~~ +fig/fig300-01.png GCCではC++コンパイラーの名前は`g++`で、リンカーの名前は`ld`だ。ただし、C++のオブジェクトファイルをリンクするのにリンカーを直接使うことはない。`g++`は`ld`を適切に呼び出してくれるからだ。 @@ -128,9 +128,9 @@ $ ls bar.cpp bar.o foo.cpp foo.o program ~~~ -こうすることによって、ひとつのソースファイルを編集しただけで、すべてのソースファイルをコンパイルする必要がなくなる。 +こうすることによって、1つのソースファイルを編集しただけで、すべてのソースファイルをコンパイルする必要がなくなる。 -これをMakefileで書くには、出力するファイルと依存するファイルを考える。 +これを`Makefile`で書くには、出力するファイルと依存するファイルを考える。 + `program`は`foo.o`と`bar.o`に依存する + `foo.o`は`foo.cpp`に依存する @@ -154,7 +154,7 @@ bar.o : bar.cpp ## 複数のソースファイルの書き方 -C++のひとつのソースファイルは、1つの`翻訳単位`(translation unit)として扱われる。別の翻訳単位の定義を使うには、様々な制約がある。具体的な例で学ぼう。 +C++の1つのソースファイルは、1つの`翻訳単位`(translation unit)として扱われる。別の翻訳単位の定義を使うには、さまざまな制約がある。具体的な例で学ぼう。 ### 関数 @@ -196,7 +196,7 @@ $ g++ -c print_int.cpp すると残りのソースファイルを`main.cpp`とすると以下のようになる。 -~~~c++ +~~~cpp // main.cpp int main() { @@ -208,7 +208,7 @@ int main() 関数を宣言するには、関数の本体以外の部分を書き、セミコロンで終端する。 -~~~c++ +~~~cpp // main.cpp void print_int( int ) ; @@ -227,7 +227,7 @@ $ g++ -o program main.o print_int.o このとき、`main.cpp`で関数`print_int`を定義することはできない。 -~~~c++ +~~~cpp // エラー、print_int.cppでも定義されている void print_int( int ) { } @@ -237,9 +237,9 @@ int main() } ~~~ -C++では定義は全翻訳単位にひとつしか書くことができないルール、ODR(One Definition Rule、単一定義原則)があるからだ。 +C++では定義は全翻訳単位に1つしか書くことができないルール、ODR(One Definition Rule、単一定義原則)があるからだ。 -~~~c++ +~~~cpp // 宣言 void f() ; @@ -254,9 +254,9 @@ void f() { } void f() { } ~~~ -なぜODRがあるのか。なぜ定義はひとつしか書けないのか。理由は簡単だ。もし定義が複数書けるならば、異なる定義を書くことができてしまうからだ。 +なぜODRがあるのか。なぜ定義は1つしか書けないのか。理由は簡単だ。もし定義が複数書けるならば、異なる定義を書くことができてしまうからだ。 -~~~c++ +~~~cpp bool f() { return true ; } bool f() { return false ; } ~~~ @@ -265,11 +265,11 @@ bool f() { return false ; } この問題を防ぐために、C++にはODRがある。 -複数のソースファイル、つまり複数の翻訳単位からなるプログラムの場合でもODRは適用される。定義はすべての翻訳単位内でひとつでなければならない。 +複数のソースファイル、つまり複数の翻訳単位からなるプログラムの場合でもODRは適用される。定義はすべての翻訳単位内で1つでなければならない。 引数リストが違う関数は別の関数で、別の定義になる。 -~~~c++ +~~~cpp // 定義 void f() { } @@ -282,7 +282,7 @@ void f( double ) { } 名前は使う前に宣言が必要だが、肝心の定義は別のソースファイルに書いてある。宣言と定義を間違えてしまった場合はエラーになる。 -~~~c++ +~~~cpp // print_int.hpp // 失敗状態を返す bool print_int( int x ) @@ -302,7 +302,8 @@ int main() ~~~ このような間違いを防ぐためのお作法として、宣言はヘッダーファイルに書いて`#include`する。 -~~~c++ + +~~~cpp // print_int.h bool print_int( int x ) ; @@ -320,14 +321,14 @@ int main() 変数にも宣言と定義がある。通常、変数の宣言は定義を兼ねる。 -~~~cpp +~~~c++ // 宣言かつ定義 int variable ; ~~~ そのため、別の翻訳単位の変数を使うために変数を書くと、定義が重複してしまい、ODR違反になる。 -~~~c++ +~~~cpp // global.cpp int variable ; @@ -343,7 +344,7 @@ int main() 変数を定義せずに宣言だけしたい場合は、`extern`キーワードを使う。 -~~~pp +~~~cpp // global.cpp int variable ; @@ -363,7 +364,7 @@ int main() 変数の場合も、間違いを防ぐためにヘッダーファイルに書いて`#include`するとよい。 -~~~c++ +~~~cpp // global.h extern int variable ; @@ -380,7 +381,7 @@ int main() 変数や関数の定義はODRにより重複できない。ということはヘッダーファイルに書いて複数の翻訳単位で`#include`できないということだ。 -~~~c++ +~~~cpp // library.h std::string delimiter{"\n"} ; @@ -398,7 +399,7 @@ void print_int( int x ) `library.h`には宣言だけを書いて、別途翻訳単位となるソースファイル、例えば`library.cpp`を用意しなければならない。 -~~~c++ +~~~cpp // library.h void print_int( int x ) ; @@ -412,7 +413,7 @@ void print_int( int x ) 小さなライブラリの場合、この制約は煩わしい。できればヘッダーファイルだけで済ませてしまいたい。このためにC++には特別なODRを例外的に回避する方法がある。 -キーワード`inline`をつけて定義した関数と変数は、インライン関数、インライン変数となる。 +キーワード`inline`を付けて定義した関数と変数は、インライン関数、インライン変数となる。 ~~~cpp // library.h @@ -425,7 +426,7 @@ inline void print_int( int x ) インライン関数とインライン変数は、複数の翻訳単位で重複して定義できる。 -~~~c++ +~~~cpp // foo.cpp #include "library.h" @@ -439,8 +440,8 @@ inline void print_int( int x ) 同じ翻訳単位の中で重複することはできない。 -~~~c++ -// ひとつの翻訳単位 +~~~cpp +// 1つの翻訳単位 inline int variable ; // エラー、再定義 inline int variable ; @@ -452,7 +453,7 @@ inline int variable ; たとえば以下はトークン列が違う。 -~~~c++ +~~~cpp inline int f( int x ) { return x ; } inline int f( int y ) { return y ; } ~~~ @@ -463,7 +464,7 @@ inline int f( int y ) { return y ; } 同じトークン列でも意味が異なることがある。 -~~~c++ +~~~cpp // foo.cpp void f( int ) { } inline bool g( ) @@ -481,7 +482,7 @@ inline bool g() `foo.cpp`のインライン関数`g`は`f(int)`を呼び出すが、`bar.cpp`のインライン関数`g`は`f(double)`を呼び出す。インライン関数`g`のトークン列はどちらも同じだが、意味が異なる。 -ODRの例外的な回避の怖いところは、間違えてしまってもコンパイラーがエラーメッセージを出してくれる保証がないところだ。上の同じトークン列で違う意味のような関数は、そのままコンパイルが通ってリンクされ、実行可能なプログラムが生成されてしまうかも知れない。そのようなプログラムの挙動がどうなるかはわからない。この理由は、ODR違反を完全に発見するコンパイラーの実装が技術的に困難だからだ。ODR違反をしないのはユーザーの責任だ。 +ODRの例外的な回避の怖いところは、間違えてしまってもコンパイラーがエラーメッセージを出してくれる保証がないところだ。上の同じトークン列で違う意味のような関数は、そのままコンパイルが通ってリンクされ、実行可能なプログラムが生成されてしまうかもしれない。そのようなプログラムの挙動がどうなるかはわからない。この理由は、ODR違反を完全に発見するコンパイラーの実装が技術的に困難だからだ。ODR違反をしないのはユーザーの責任だ。 インライン変数とインライン関数はわざわざ翻訳単位を分けて分割コンパイルするまでもないライブラリに使うとよい。 @@ -501,7 +502,7 @@ struct Foo } ; ~~~ -クラスを複数の翻訳単位で使うには、関数と同じように宣言と定義に分ければよいと考えるかも知れないが、残念ながらクラスの宣言だけでできることは少ない。 +クラスを複数の翻訳単位で使うには、関数と同じように宣言と定義に分ければよいと考えるかもしれないが、残念ながらクラスの宣言だけでできることは少ない。 クラスの宣言だけでできることは、クラス名を型名として使うとか、クラスのポインター型を作るぐらいのものだ。 @@ -513,7 +514,7 @@ Foo * ptr = nullptr ; 宣言だけされたクラスのオブジェクトを作ることはできないし、ポインターの演算もできない。 -~~~c++ +~~~cpp struct Foo ; int main() @@ -533,7 +534,7 @@ int main() ODR違反を起こさないために、クラス定義はインクルードファイルに書いて`#include`するのがお作法だ。 -~~~ +~~~cpp // Foo.h // クラス定義 struct Foo @@ -598,7 +599,7 @@ void main() ~~~ ##### staticメンバー -クラスのメンバーは非`static`メンバーと`static`メンバーにわけることができる。`static`メンバーは`static`キーワードをつけて宣言する。 +クラスのメンバーは非`static`メンバーと`static`メンバーにわけることができる。`static`メンバーは`static`キーワードを付けて宣言する。 ~~~cpp @@ -674,7 +675,7 @@ int main() } ~~~ -複数の翻訳単位からなるプログラムの場合、ODRにより定義はひとつしか書けないので、どこか1つのソースファイルだけに定義を書くことになる。 +複数の翻訳単位からなるプログラムの場合、ODRにより定義は1つしか書けないので、どこか1つのソースファイルだけに定義を書くことになる。 ~~~cpp // S.h @@ -705,7 +706,7 @@ struct S `static`メンバーはクラススコープの下に関数と変数というだけで、その実態は名前空間スコープ内の関数と変数と同じだ。 -~~~c++ +~~~cpp // 名前空間 namespace A { int variable ; diff --git a/400-gdb.md b/400-gdb.md index 219fe0d..2c2c8a1 100644 --- a/400-gdb.md +++ b/400-gdb.md @@ -14,25 +14,25 @@ int main() しかし実際に実行して見ると、`1`から`9`までの整数しか標準出力しない。なぜだろうか。 -読者の中にはコード中の問題のある箇所に気がついた人もいるだろう。これはたったの5行のコードで、問題の箇所も一箇所だ。これが数百行、数千行になり、関数やクラスを複雑に使い、問題の原因は複数の箇所のコードの実行が組み合わさった結果で、しかも自分で書いたコードなので正しく書いたはずだという先入観がある場合、たとえコードとしてはささいな間違いであったとしても、発見は難しい。 +読者の中にはコード中の問題のある箇所に気が付いた人もいるだろう。これはたったの5行のコードで、問題の箇所も1箇所だ。これが数百行、数千行になり、関数やクラスを複雑に使い、問題の原因は複数の箇所のコードの実行が組み合わさった結果で、しかも自分で書いたコードなので正しく書いたはずだという先入観がある場合、たとえコードとしてはささいな間違いであったとしても、発見は難しい。 -こういうとき、実際にコードを一行ずつ実行したり、ある時点でプログラムの実行を停止させて変数の値を見たりしたいものだ。 +こういうとき、実際にコードを1行ずつ実行したり、ある時点でプログラムの実行を停止させて変数の値を見たりしたいものだ。 そんな夢を実現するのがデバッガーだ。この章ではデバッガーとしてGDB(GNUプロジェクトデバッガー)の使い方を学ぶ。 -GDBで快適にデバッグするには、プログラムをコンパイルするときにデバッグ情報を出力する必要がある。そのためには、GCCに`-g`オプションをつけてプログラムをコンパイルする。 +GDBで快適にデバッグするには、プログラムをコンパイルするときにデバッグ情報を出力する必要がある。そのためには、GCCに`-g`オプションを付けてプログラムをコンパイルする。 ~~~ $ g++ -g -o program program.cpp ~~~ -本書のはじめに作った入門用の`Makefile`を使う場合は、`$gcc_options`に`-g`を加えることになる。 +本書の始めに作った入門用の`Makefile`を使う場合は、`$gcc_options`に`-g`を加えることになる。 ~~~ gcc_options = -std=c++17 -Wall --pedantic-error -g ~~~ -コンパイラーのオプションを変更した後は、`make clean`を実行してコンパイル済みヘッダーファイルを生成し直す必要がある。 +コンパイラーのオプションを変更したあとは、`make clean`を実行してコンパイル済みヘッダーファイルを生成し直す必要がある。 ~~~ $ make clean @@ -102,7 +102,7 @@ Reading symbols from program...done. (gdb) help ~~~ -ヘルプメッセージが表示される。あるコマンドのヘルプを見たい場合は`"help コマンド"`と入力する。今から使う予定のコマンドである`"list"`のヘルプを見てみよう。 +ヘルプメッセージが表示される。あるコマンドのヘルプを見たい場合は`"help コマンド"`と入力する。いまから使う予定のコマンドである`"list"`のヘルプを見てみよう。 ~~~ (gdb) help list @@ -147,7 +147,7 @@ Starting program: 実行可能ファイルへのパス (gdb) break 5 ~~~ -これで、`main`関数、4行目、5行目にブレイクポイントを設定した。早速もう一度最初から実行してみよう。 +これで、`main`関数、4行目、5行目にブレイクポイントを設定した。さっそくもう一度最初から実行してみよう。 ~~~ (gdb) run @@ -204,7 +204,7 @@ $3 = 0 $4 = 10 ~~~ -現在、プログラムは5行目を実行する手前で止まっている。このまま`"continue"`コマンドを使うとプログラムの終了まで実行されてしまう。もう一度1行だけ実行するには`"break 6"`で6行目にブレイクポイントを設定すればよいのだが、次の一行だけ実行したいときにいちいちブレイクポイントを設定するのは面倒だ。 +現在、プログラムは5行目を実行する手前で止まっている。このまま`"continue"`コマンドを使うとプログラムの終了まで実行されてしまう。もう一度1行だけ実行するには`"break 6"`で6行目にブレイクポイントを設定すればよいのだが、次の1行だけ実行したいときにいちいちブレイクポイントを設定するのは面倒だ。 そこで使うのが`"step"`だ。次の5行目を実行すると、変数`val`の値は`11`になっているはずだ。 @@ -243,7 +243,7 @@ $ gdb program ## プログラムの停止方法 -デバッガーの機能として一番わかり易いのが、実行中のプログラムを一時停止させる機能だ。 +デバッガーの機能として一番わかりやすいのが、実行中のプログラムを一時停止させる機能だ。 ### ブレイクポイント @@ -298,13 +298,13 @@ Num Type Disp Enb Address What 3 breakpoint keep y 0x000000000000115b in main() at main.cpp:7 ~~~ -これは5,6,7行目にそれぞれブレイクポイントを設定した後の`"info breakpoints"`の結果だ。 +これは5,6,7行目にそれぞれブレイクポイントを設定したあとの`"info breakpoints"`の結果だ。 この表の意味は、左から番号(Num, Number)、種類(Type)、中断後の処理(Disposition), 有効/無効(Enb, Enable/Disable)、アドレス(Address), 内容(What)となっている。 ブレイクポイントには作成された順番に番号が振られる。ブレイクポイントの設定を変えるには、この番号でブレイクポイントを参照する。 -ブレイクポイントには3種類ある。普通のブレイクポイントである`breakpoint`の他に、特殊なブレイクポイントであるウォッチポイント(watchpoint)、キャッチポイント(catchpoint)がある。 +ブレイクポイントには3種類ある。普通のブレイクポイントである`breakpoint`のほかに、特殊なブレイクポイントであるウォッチポイント(watchpoint)、キャッチポイント(catchpoint)がある。 中断後の処理と有効/無効の切り替えはあとで説明する。 @@ -345,7 +345,7 @@ No breakpoints or watchpoints. (gdb) enable 1 ~~~ -ブレイクポイントは発動した後に自動で無効化させることができる。 +ブレイクポイントは発動したあとに自動で無効化させることができる。 `"enable [breakpoints] once"`コマンドで、ブレイクポイントが一度発動すると自動的に無効化されるブレイクポイントを設定できる。 @@ -355,7 +355,7 @@ No breakpoints or watchpoints. このコマンドは、ブレイクポイント番号1が一度発動したら自動的に無効化する設定をする。 -ブレイクポイントは$n$回発動した後に自動的に無効化することもできる。そのためのコマンドは`"enable [breakpoints] count n"`だ。 +ブレイクポイントは$n$回発動したあとに自動的に無効化することもできる。そのためのコマンドは`"enable [breakpoints] count n"`だ。 ~~~ (gdb) enable 1 count 10 @@ -366,7 +366,7 @@ No breakpoints or watchpoints. #### 関数名へのブレイクポイント -ブレイクポイントの場所として関数名を書くと、その関数名が呼び出された後、関数の本体の一行目が実行されるところにブレイクポイントが設定される。 +ブレイクポイントの場所として関数名を書くと、その関数名が呼び出されたあと、関数の本体の1行目が実行されるところにブレイクポイントが設定される。 @@ -501,7 +501,7 @@ int main() } ~~~ -このプログラムを`main`関数から1行づつ実行してその挙動を確かめたい。その場合に、すべての行にブレイクポイントを設定するのは面倒だ。GDBではこのような場合に、現在中断している場所から一行だけ実行する方法がある。 +このプログラムを`main`関数から1行ずつ実行してその挙動を確かめたい。その場合に、すべての行にブレイクポイントを設定するのは面倒だ。GDBではこのような場合に、現在中断している場所から1行だけ実行する方法がある。 ### 実行再開(continue) @@ -525,7 +525,7 @@ int main() `step`コマンドは省略して`s`でもよい。 -先程のソースファイルで、まず`main`関数にブレイクポイントを設定して実行すると、 +先ほどのソースファイルで、まず`main`関数にブレイクポイントを設定して実行すると、 ~~~ (gdb) break main @@ -574,13 +574,13 @@ int main() このまま`step`コマンドを続けていくと、また`main`関数に戻り、また次の行が実行され、また関数`f`が実行される。 -1行づつ実行するのではなく$n$行実行したい場合は、`step`コマンドに$n$を指定する +1行ずつ実行するのではなく$n$行実行したい場合は、`step`コマンドに$n$を指定する ~~~ (gdb) step n ~~~ -するとソースファイルのn行分実行される。例えば以下のように書くと、 +するとソースファイルの$n$行分実行される。例えば以下のように書くと、 ~~~ (gdb) step 3 @@ -659,7 +659,7 @@ int main() } ~~~ -ここで関数`f`に注目してみよう。関数`f`は様々な関数から呼ばれる。関数`main`から呼ばれるし、関数`apple`や`banana`からも呼ばれる。特に、関数`cherry`は関数`apple`を呼び出すので、間接的に関数`f`を呼ぶ。 +ここで関数`f`に注目してみよう。関数`f`はさまざまな関数から呼ばれる。関数`main`から呼ばれるし、関数`apple`や`banana`からも呼ばれる。特に、関数`cherry`は関数`apple`を呼び出すので、間接的に関数`f`を呼ぶ。 関数`f`にブレイクポイントを仕掛けて実行してみよう。 @@ -686,7 +686,7 @@ int main() #1 0x0000555555556310 in main () at main.cpp:10 ~~~ -`#0`がバックトレースの最も深い現在のスタックフレームだ。これは関数fでソースファイル`main.cpp`の2行目だ。`#1`が`"#0`の上のスタックフレームで、これは関数`main`で10行目にある。 +`#0`がバックトレースの最も深い現在のスタックフレームだ。これは関数`f`でソースファイル`main.cpp`の2行目だ。`#1`が`"#0`の上のスタックフレームで、これは関数`main`で10行目にある。 実行を再開して、次の関数`f`のバックトレースを見よう。 @@ -700,7 +700,7 @@ int main() 今回はスタックフレームが3つある。最も外側のスタックフレームは関数`main`で、そこから関数`apple`が呼び出され、そして関数`f`が呼び出される。 -更に進めよう。 +さらに進めよう。 ~~~ (gdb) continue @@ -758,7 +758,7 @@ int main() $1 = 1 ~~~ -1行づつ実行して値を見ていこう。 +1行ずつ実行して値を見ていこう。 ~~~ (gdb) step @@ -801,11 +801,11 @@ int main() { } ## シグナルによるプログラムの中断 -プログラムは様々な理由によりシグナルを出して実行を強制的に終了する。このシグナルはGDBによってトラップされ、ブレイクポイントと同じくプログラムの中断として扱われる。 +プログラムはさまざまな理由によりシグナルを出して実行を強制的に終了する。このシグナルはGDBによってトラップされ、ブレイクポイントと同じくプログラムの中断として扱われる。 -プログラムが実行を終了するようなシグナルは、プログラムの不具合によって生じる。具体的な不具合は実行環境に依存するが、大抵の環境で動く不具合は、nullポインターを経由した間接アクセスと、ゼロ除算だ。 +プログラムが実行を終了するようなシグナルは、プログラムの不具合によって生じる。具体的な不具合は実行環境に依存するが、たいていの環境で動く不具合は、nullポインターを経由した間接アクセスと、ゼロ除算だ。 -~~~c++ +~~~cpp // nullポインターを経由した間接アクセス int * ptr = nullptr ; *ptr = 0 ; @@ -815,7 +815,7 @@ int * ptr = nullptr ; 実際にそのようなプログラムを作ってGDBで実行し、プログラムが中断されることを確認してみよう。 -~~~c++ +~~~cpp int main() { int x { } ; @@ -839,7 +839,7 @@ Program received signal SIGFPE, Arithmetic exception. ちょうどゼロ除算を起こした箇所でプログラムの実行が中断する。 -このとき中断した状態でプログラムの様々な状態を観測できる。例えばバックトレースを表示したり、変数の値を確認したりできる。 +このとき中断した状態でプログラムのさまざまな状態を観測できる。例えばバックトレースを表示したり、変数の値を確認したりできる。 ## コアダンプを使ったプログラムの状態の確認 @@ -853,9 +853,9 @@ $ program Floating point exception (core dumped) ~~~ -`core dumped`という文字が気になる。プログラムはシグナルで強制的に実行を終了するときコアファイルをダンプする。このファイル名は通常`core`だ。通常はカレントディレクトリに`core`という名前のファイルが生成されているはずだ。 +`core dumped`という文字が気になる。プログラムはシグナルで強制的に実行を終了するときコアファイルをダンプする。このファイル名は通常`core`だ。通常はカレントディレクトリーに`core`という名前のファイルが生成されているはずだ。 -もしカレントディレクトリに`core`という名前のファイルがない場合、以下のコマンドを実行する。 +もしカレントディレクトリーに`core`という名前のファイルがない場合、以下のコマンドを実行する。 ~~~ $ ulimit -c unlimited @@ -891,4 +891,4 @@ Program terminated with signal SIGFPE, Arithmetic exception. $1 = 0 ~~~ -デバッガーはとても役に立つ。本書では少しだけしか解説しなかったが、この他にも強力な機能がたくさんある。 +デバッガーはとても役に立つ。本書では少しだけしか解説しなかったが、このほかにも強力な機能がたくさんある。