江添亮のC++入門

江添 亮

2018-02-27

本書はプログラミングの経験はあるがC++は知らない読者を対象にしたC++を学ぶための本である。本書はすでに学んだことのみを使って次の知識を説明する手法で書かれた。C++コンパイラーをC++で書く場合、C++コンパイラーのソースコードをコンパイルする最初のC++コンパイラーをどうするかというブートストラップ問題がある。本書はいわばC++における知識のブートストラップを目指した本だ。これにより読者は本を先頭から読んでいけば、まだ学んでいない概念が突如として無説明のまま使われて混乱することなく読み進むことができるだろう。

C++知識のブートストラップを意識した入門書の執筆はなかなかに難しかった。ある機能Xを教えたいが、そのためには機能Yを知っていなければならず、機能Yを理解するためには機能Zの理解が必要といった具合に、C++の機能の依存関係の解決をしなければならなかったからだ。著者自身も苦しい思いをしながらできるだけ今までに説明した知識のみを使って次の知識を教えるように書き進めていった結果、意外な再発見をした。ポインターを教えた後はC++のほとんどの機能を教えることに苦労しなくなったのだ。けっきょくC++ではいまだにポインターの機能はさまざまな機能の土台になっているのだろう。

本書の執筆時点でC++は現在、C++20の規格制定に向けて大詰めを迎えている。C++20では#includeに変わるモジュール、軽量な実行媒体であるコルーチン、高級なassert機能としてのコントラクトに加え、とうとうコンセプトが入る。ライブラリとしてもコンセプトを活用したレンジ、spanflat_mapなどさまざまなライブラリが追加される。その詳細は、次に本を出す機会があるならば『江添亮の詳説C++17』と似たようなC++20の参考書を書くことになるだろう。C++はまだまだ時代に合わせて進化する言語だ。

本書の執筆はGitHub上で公開した状態で行われた。

https://github.com/EzoeRyou/cpp-intro

本書のライセンスはGPLv3である。ただし、本書の著者近影はGPLv3ではなく撮影者が著作権を保持している。

本書の著者近影の撮影は、著者の古くからの友人でありプロのカメラマンである三浦大に撮影してもらった。

三浦大のWebサイト: http://www.masarumiura.jp/

江添亮

C++の概要

C++とは何か。C++の原作者にして最初の実装者であるBjarne Stroustrupは、以下のように簡潔にまとめている。

C++は、Simulaのプログラム構造化のための機構と、Cのシステムプログラミング用の効率性と柔軟性を提供するために設計された。C++は半年ほどで現場で使えることを見込んでいた。結果として成功した。

Bjarne Stroustrup, A History of C++: 1979-1991, HOPL2

プログラミング言語史に詳しくない読者は、Simulaというプログラミング言語について知らないことだろう。Simulaというのは、初めてオブジェクト指向プログラミングを取り入れたプログラミング言語だ。当時と言えばまだ高級なプログラミング言語はほとんどなく、if else, whileなどのIBMの提唱した構造化プログラミングを可能にする文法を提供しているプログラミング言語すら、多くは研究段階であった。いわんやオブジェクト指向など、当時はまだアカデミックにおいて可能性の1つとして研究されている程度の地に足のついていない夢の機能であった。そのような粗野な時代において、Simulaは先進的なオブジェクト指向プログラミングを実現していた。

オブジェクト指向は現代のプログラミング言語ではすっかり普通になった。データの集合とそのデータに適用する関数を関連付けることができる便利なシンタックスシュガー、つまりプログラミング言語の文法上の機能として定着した。しかし、当時のオブジェクト指向というのはもっと抽象度の高い概念であった。本来のオブジェクト指向をプログラミング言語に落とし込んだ最初の言語として、SimulaとSmalltalkがある。

Simulaではクラスのオブジェクト1つ1つが、あたかも並列実行しているかのように振る舞った。Smalltalkでは同一プログラム内のオブジェクトごとのデータのやり取りですらあたかもネットワーク越しに通信をするかのようなメッセージパッシングで行われた。

問題は、そのような抽象度の高すぎるSimulaやSmalltalkのようなプログラミング言語の設計と実装では実行速度が遅く、大規模なプログラムを開発するには適さなかった。

Cの効率性と柔軟性というのは、要するに実行速度が速いとかメモリー消費量が少ないということだ。ではなぜCはほかの言語に比べて効率と柔軟に優れているのか。これには2つの理由がある。

1つ、Cのコードは直接ハードウェアがサポートする命令にまでマッピング可能であるということ。現実のハードウェアにはストレージがあり、メモリーがあり、キャッシュがあり、レジスターがあり、命令は投機的に並列実行される泥臭い計算機能を提供している。

1つ、使わない機能のコストを支払う必要がないというゼロオーバーヘッドの原則。例えばあらゆるメモリー利用がGC(ガベージコレクション)によって管理されている言語では、たとえメモリーをすべて明示的に管理していたとしても、GCのコストを支払わなければならない。GCではプログラマーは確保したメモリーの解放処理を明示的に書く必要はない。定期的に全メモリーを調べて、どこからも使われていないメモリーを解放する。この処理には余計なコストがかかる。しかし、いつメモリーを解放すべきかがコンパイル時に決定できる場合では、GCは必要ない。GCが存在する言語では、たとえGCが必要なかったとしても、そのコストを支払う必要がある。また実行時にメモリーレイアウトを判定して実行時に分岐処理ができる言語では、たとえコンパイル時にメモリーレイアウトが決定されていたとしても、実行時にメモリーレイアウトを判定して条件分岐するコストを支払わなければならない。

C++は、「アセンブリ言語をおいて、C++より下に言語を置かない」と宣言するほど、ハードウェア機能への直接マッピングとゼロオーバーヘッドの原則を重視している。

C++のほかの特徴としては、委員会方式による国際標準規格を定めていることがある。特定の一個人や一法人が所有する言語は、個人や法人の意思で簡単に仕様が変わってしまう。短期的な利益を追求するために長期的に問題となる変更をしたり、単一の実装が仕様だと言わんばかりの振る舞いをする。特定の個人や法人に所有されていないこと、実装が従うべき標準規格があること、独立した実装が複数あること、言語に利害関係を持つ関係者が議論して投票で変更を可決すること、これがC++が長期に渡って使われてきた理由でもある。

委員会方式の規格制定では、下位互換性の破壊は忌避される。なぜならば、既存の動いているコードを壊すということは、それまで存在していた資産の価値を毀損することであり、利害関係を持つ委員が反対するからだ。

下位互換性を壊した結果何が起こるかというと、単に言語が新旧2つに分断される。Python 2とPython 3がその最たる例だ。

C++には今日の最新で高級な言語からみれば古風な制約が数多く残っているが、いずれも理由がある。下位互換性を壊すことができないという理由。効率的な実装方法が存在しないという理由。仮に効率的な実装が存在するにしても、さまざまな環境で実装可能でなければ規格化はできないという理由。

C++には善しあしがある。Bjarne StroustrupはC++への批判にこう答えている。

言語には2種類ある。文句を言われる言語と、誰も使わない言語。

C++は文句を言われる方の言語だ。

C++の実行

プログラミング言語を学ぶには、まず書いたソースコードをプログラムとして実行できるようになることが重要だ。自分が正しく理解しているかどうかを確認するために書いたコードが期待どおりに動くことを確かめてこそ、正しい理解が確認できる。

C++の実行の仕組み

C++は慣習的に、ソースファイルをコンパイルしてオブジェクトファイルを生成し、オブジェクトファイルをリンクして実行可能ファイルを生成し、実行可能ファイルを直接実行することで実行する言語だ。

ほかの言語では、ソースファイルをそのままパースし、解釈して実行するインタープリター形式の言語が多い。もっとも、いまとなってはソースファイルから中間言語に変換して、VM(Virtual Machine)と呼ばれる中間言語を解釈して実行するソフトウェア上で実行するとか、JIT(Just-In-Time)コンパイルしてネイティブコードを生成して実行するといった実装もあるため、昔のように単純にインタープリター型の言語ということはできなくなっている事情はある。ただし、最終的にJITコンパイルされてネイティブコードが実行される言語でも、コンパイルやコード生成はプログラマーが意識しない形で行われるため、プログラマーはコンパイラーを直接使う必要のない言語も多い。

C++はプログラマーが直接コンパイラーを使い、ソースファイルをプログラムに変換する言語だ。

簡単な1つのソースファイルからなるプログラムの実行

ここでは、典型的なC++のソースファイルをどのようにコンパイルし実行するか、一連の流れを学ぶ。

サンプルコード

以下のC++のソースファイルは標準出力にhelloと出力するものだ。

#include <iostream>

int main()
{
    std::cout << "hello" ;
}

コードの詳細な意味はさておくとして、このサンプルコードを使ってC++の実行までの流れを見ていこう。

まずは端末から作業用の適当な名前のディレクトリーを作る。ここではcppとしておこう。ディレクトリーの作成はmkdirコマンドで行える。

$ mkdir cpp
$ cd cpp

好きなテキストエディターを使って上のサンプルコードをテキストファイルとして記述する。ファイル名はhello.cppとしておこう。

$ vim hello.cpp

C++のソースファイルの名前は何でもよいが、慣習で使われている拡張子がいくつかある。本書では.cppを使う。

無事にソースファイルが作成できたかどうか確認してみよう。現在のカレントディレクトリー下のファイルの一覧を表示するにはls、ファイルの内容を表示するにはcatを使う。

$ ls
hello.cpp
$ cat hello.cpp
#include <iostream>

int main()
{
    std::cout << "hello" ;
}

コンパイル

さて、ソースファイルが用意できたならば、いよいよコンパイルだ。

C++のソースファイルから、実行可能ファイルを生成するソフトウェアをC++コンパイラーという。C++コンパイラーとしては、GCC(GNU Compiler Collection)とClang(クラン)がある。使い方はどちらもほぼ同じだ。

GCCを使って先ほどのhello.cppをコンパイルするには以下のようにする。

$ g++ -o hello hello.cpp

GCCという名前のC++コンパイラーなのにg++なのは、gccはC言語コンパイラーの名前としてすでに使われているからだ。この慣習はClangも引き継いでいて、ClangのC++コンパイラーはclang++だ。

サンプルコードを間違いなくタイプしていれば、カレントディレクトリーにhelloという実行可能ファイルが作成されるはずだ。確認してみよう。

$ ls
hello hello.cpp

実行

さて、いよいよ実行だ。通常のOSではカレントディレクトリーがPATHに含まれていないため、実行するにはカレントディレクトリーからパスを指定する必要がある。

$ ./hello
hello

上出来だ。初めてのC++プログラムが実行できた。さっそくC++を学んでいきたいところだが、その前にC++プログラミングに必要なツールの使い方を学ぶ必要がある。

GCC: C++コンパイラー

GCCはC++のソースファイルからプログラムを生成するC++コンパイラーだ。

GCCの基本的な使い方は以下のとおり。

g++ その他のオプション -o 出力するファイル名 ソースファイル名

ソースファイル名は複数指定することができる。

$ g++ -o abc a.cpp b.cpp c.cpp

これについては分割コンパイルの章で詳しく解説する。

コンパイラーはメッセージを出力することがある。コンパイルメッセージには、エラーメッセージと警告メッセージとがある。

エラーメッセージというのは、ソースコードに文法上、意味上の誤りがあるため、コンパイルできない場合に生成される。エラーメッセージはエラーの箇所も教えてくれる。ただし、文法エラーは往々にして適切な誤りの箇所を指摘できないこともある。これは、C++の文法としては正しくないテキストファイルから、妥当なC++であればどういう間違いなのかを推測する必要があるためだ。

警告メッセージというのは、ソースコードにコンパイルを妨げる文法上、意味上の誤りは存在しないが、誤りの可能性が疑われる場合に出力される。

コンパイラーオプション

GCCのコンパイラーオプションをいくつか学んでいこう。

-std=はC++の規格を選択するオプションだ。C++17に準拠したいのであれば-std=c++17を指定する。読者が本書を読むころには、C++20や、あるいはもっと未来の規格が発行されているかもしれない。常に最新のC++規格を選択するオプションを指定するべきだ。

-Wallはコンパイラーの便利な警告メッセージのほとんどすべてを有効にするオプションだ。コンパイラーによる警告メッセージはプログラムの不具合を未然に発見できるので、このオプションは指定すべきだ。

--pedantic-errorsはC++の規格を厳格に守るオプションだ。規格に違反しているコードがコンパイルエラー扱いになる。

これをまとめると、GCCは以下のように使う。

g++ -std=c++17 -Wall --pedantic-errors -o 出力ファイル名 入力ファイル名

ところで、GCCのオプションはとても多い。すべてを知りたい読者は、以下のようにしてGCCのマニュアルを読むとよい。

$ man gcc

手元にマニュアルがない場合、GCCのWebサイトにあるオンラインマニュアルも閲覧できる。

ヘッダーファイルの省略

先ほどのソースコードをもう一度見てみよう。冒頭に以下のような行がある。

#include <iostream>

これは#includeディレクティブ(#include directive)といい、プリプロセッサー(preprocessor)の一部だ。プリプロセッサーについて詳しくは煩雑になるので巻末資料を参照してもらうとして、このコードはiostreamライブラリを使うために必要で、その意味としてはヘッダーファイルiostreamの取り込みだ。

C++の標準ライブラリを使うには、ライブラリごとに対応した#includeディレクティブを書かなければならない。それはあまりにも煩雑なので、本書では標準ライブラリのヘッダーファイルをすべて#includeしたヘッダーファイル(header file)を作成し、それを#includeすることで、#includeを書かなくて済むようにする。

そのためにはまず標準ライブラリのヘッダーファイルのほとんどすべてを#includeしたヘッダーファイル、all.hを作成する。

#include <cstddef>
#include <limits>
#include <climits>
#include <cfloat>
#include <cstdint>
#include <cstdlib>
#include <new>
#include <typeinfo>
#include <exception>
#include <initializer_list>
#include <cstdalign>
#include <stdexcept>
#include <cassert>
#include <cerrno>
#include <system_error>
#include <string>

#if __has_include(<string_view>)
#   include <string_view>
#endif

#include <array>
#include <deque>
#include <forward_list>
#include <list>
#include <vector>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <stack>
#include <iterator>
#include <algorithm>
#include <cfenv>
#include <random>
#include <numeric>
#include <cmath>
#include <iosfwd>
#include <iostream>
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>
#include <iomanip>
#include <sstream>
#include <fstream>

#if __has_include(<filesystem>)
#   include <filesystem>
#endif

#include <cstdio>
#include <cinttypes>


#include <regex>
#include <atomic>
#include <thread>
#include <mutex>
#include <shared_mutex>
#include <condition_variable>
#include <future>

using namespace std::literals ;

このようなヘッダーファイルall.hを作成したあとに、ソースファイルで以下のように書けば、ほかのヘッダーファイルを#includeする必要がなくなる。

#include "all.h"

// その他のコード

//から行末まではコメントで、好きなテキストを書くことができる。

しかし、この最初の1行の#includeも面倒だ。そこでGCCのオプション-includeを使い、all.hを常に#includeした扱いにする。

$ g++ -include all.h -o program main.cpp

このようにすると、main.cppが以下のコードでもコンパイルできるようになる。

// main.cpp
// 面倒な#includeなどなし

int main()
{
    std::cout << "hello" ;
}

これでヘッダーファイルが省略できるようになった。

コンパイル済みヘッダー(precompiled header)

C++はソースファイルをコンパイルする必要がある言語だ。コンパイルには時間がかかる。コンパイルにどれだけ時間がかかっているかを計測するには、以下のようにするとよい。

$ time g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp

どうだろうか。読者の環境にもよるが、知覚できるぐらいの時間がかかっているのではないだろうか。プログラミングの習得にはコードを書いてから実行までの時間が短い方がよい。そこで本格的にC++を学ぶ前に、コンパイル時間を短縮する方法を学ぶ。

プログラムで変更しないファイルを事前にコンパイルしておくと、変更した部分だけコンパイルすればよいので、コンパイル時間の短縮になる。GCCでは、ヘッダーファイルを事前にコンパイルする特別な機能がある。標準ライブラリのヘッダーファイルは変更しないので、事前にコンパイルしておけばコンパイル時間の短縮になる。

事前にコンパイルしたヘッダーファイルのことをコンパイル済みヘッダー(precompiled header)という。

すでに作成したall.hはコンパイル済みヘッダーとするのに適切なヘッダーファイルだ。

コンパイル済みヘッダーファイルを作成するには、ヘッダーファイル単体をGCCに与え、出力するファイルをヘッダーファイル名.gchとする。ヘッダーファイル名がall.hの場合、all.h.gchとなる。

GCCのオプションにはほかのソースファイルをコンパイルするときと同じオプションを与えるほか、ヘッダーファイルがC++で書かれていることを示すオプション-x c++-headerを与える。

$ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h

こうすると、コンパイル済みヘッダーファイルall.h.gchが生成できる。

GCCはヘッダーファイルを使うときに、同名の.gchファイルが存在する場合は、そちらをコンパイル済みヘッダーファイルとして使うことで、ヘッダーファイルの処理を省略する。

$ g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp

コンパイル済みヘッダーは1回のコンパイルにつき1つしか使うことができない。そのため、コンパイル済みヘッダーとするヘッダーファイルを定め、そのヘッダーファイル内にほかのヘッダーをすべて記述する。本書ではコンパイル済みヘッダーファイルとする元のヘッダーファイルの名前をall.hとする。

さっそくコンパイル時間の短縮効果を確かめてみよう。

$ ls
all.h main.cpp
$ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h
$ ls
all.h all.h.gch main.cpp
$ time g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp

Make: ビルドシステム

コンパイルと実行のまとめ

ここまで、我々はソースファイルをコンパイルして実行可能ファイルを生成し、プログラムを実行する方法について学んできた。これまでに学んできたことを一連のコマンドで振り返ってみよう。

$ ls
all.h main.cpp
$ cat all.h
#include <iostream>
$ cat main.cpp
int main() { std::cout << "hello"s ; }

まず、カレントディレクトリーにはall.hmain.cppがある。この2つのファイルは実行可能ファイルを生成するために必要なファイルだ。今回、その中身は最小限にしてある。本当のall.hは、実際には前回書いたように長い内容になる。

$ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h
$ ls
all.h all.h.gch main.cpp

次に、ソースファイルのコンパイルを高速化するために、ヘッダーファイルall.hから、コンパイル済みヘッダーファイルall.h.gchを生成する。

$ g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp
$ ls
all.h all.h.gch main.cpp program

プリコンパイル済みヘッダーファイルall.h.gchとC++ソースファイルmain.cppから、実行可能ファイルprogramを生成する。

$ ./program
hello

実行可能ファイルprogramを実行する。

これで読者はC++のプログラミングを学び始めるにあたって必要なことはすべて学んだ。さっそくC++を学んでいきたいところだが、その前にもう1つ、ビルドシステムを学ぶ必要がある。

依存関係を解決するビルドシステム

以上のC++のソースファイルからプログラムを実行するまでの流れは、C++のプログラムとしてはとても単純なものだが、それでも依存関係が複雑だ。

プログラムの実行にあたって最終的に必要なのはファイルprogramだが、このファイルはGCCで生成しなければならない。ところでGCCでファイルprogramを生成するには、事前にall.h, all.h.gch, main.cppが必要だ。all.h.gchall.hからGCCで生成しなければならない。

一度コンパイルしたプログラムのソースファイルを書き換えて再びコンパイルする場合はどうすればいいだろう。main.cppだけを書き換えた場合、all.hは何も変更されていないので、コンパイル済みヘッダーファイルall.h.gchの再生成は必要ない。all.hだけを書き換えた場合は、all.h.gchを生成するだけでなく、programも再生成しなければならない。

プログラムのコンパイルには、このような複雑な依存関係の解決が必要になる。依存関係の解決を人間の手で行うのはたいへんだ。例えば読者が他人によって書かれた何千ものソースファイルと、プログラムをコンパイルする手順書だけを渡されたとしよう。手順書に従ってコンパイルをしたとして、ソースファイルの一部だけを変更した場合、いったいどの手順は省略できるのか、手順書から導き出すのは難しい。するとコンパイルを最初からやり直すべきだろうか。しかし、1つのソースファイルのコンパイルに1秒かかるとして、何千ものソースファイルがある場合、何千秒もかかってしまう。たった1つのソースファイルを変更しただけですべてをコンパイルし直すのは時間と計算資源の無駄だ。

この依存関係の問題は、ビルドシステムによって解決できる。本書ではGNU Makeというビルドシステムを学ぶ。読者がこれから学ぶビルドシステムによって、以下のような簡単なコマンドだけで、他人の書いた何千ものソースファイルからなるプログラムがコンパイル可能になる。

何千ものソースファイルから実行可能ファイルを生成したい。

$ make

これだけだ。makeというコマンド1つでプログラムのコンパイルは自動的に行われる。

何千ものソースファイルのうち、1つのソースファイルだけを変更し、必要な部分だけを効率よく再コンパイルしたい。

$ make

これだけだ。makeというコマンド1つでプログラムの再コンパイルは自動的に行われる。

ところで、生成される実行可能ファイルの名前はプログラムごとにさまざまだ。プログラムの開発中は、共通の方法でプログラムを実行したい。

$ make run

これでどんなプログラム名でも共通の方法で実行できる。

ソースファイルから生成されたプログラムなどのファイルをすべて削除したい。

$ make clean

これで生成されたファイルをすべて削除できる。

テキストエディターにはVimを使っているがわざわざVimからターミナルに戻るのが面倒だ。

:make

VimはノーマルモードからMakeを呼び出すことができる。もちろん、:make run:make cleanもできる。

依存関係を記述するルール

依存関係はどのように表現したらいいのだろうか。GNU MakeではMakefileという名前のファイルの中に、ターゲット(targets)、事前要件(prerequisites)、レシピ(recipes)という3つの概念で依存関係をルール(rules)として記述する。ルールは以下の文法だ。

ターゲット : 事前要件
[TAB文字]レシピ

レシピは必ずTAB文字を直前に書かなければならない。スペース文字ではだめだ。これはmakeの初心者を混乱させる落とし穴の1つとなっている。忘れずにTAB文字を打とう。

問題を簡単に理解するために、以下のような状況を考えよう。

$ ls
source
$ cat source > program

この例では、ファイルprogramを生成するためにはファイルsourceが必要だ。ファイルsourceはすでに存在している。

ターゲットは生成されるファイル名だ。この場合programとなる。

program : 事前要件
    レシピ

事前要件ターゲットを生成するために必要なファイル名だ。この場合sourceとなる。

program : source
    レシピ

レシピターゲットを生成するために必要な動作だ。この場合、cat source > programとなる

program : source
    cat source > program

さっそくこのルールを、ファイルMakefileに書き込み、makeを呼び出してみよう。

$ ls
Makefile source 
$ cat Makefile
program : source
    cat source > program
$ make
cat source > program
$ ls
Makefile program source

これがMakeの仕組みだ。ターゲットの生成に必要な事前要件と、ターゲットを生成するレシピを組み合わせたルールで依存関係を記述する。makeを実行すると、実行したレシピが表示される。

もう少しMakeのルールを追加してみよう。例えばファイルsourceはあらかじめ存在するのではなく、ファイルsource01, source02, source03の中身をこの順番で連結して生成するとしよう。以下のように書ける。

program : source
    cat source > program

source : source01 source02 source03
    cat source01 source02 source03 > source

GNU MakeはカレントディレクトリーにあるファイルMakefileの一番上に書かれたルールを実行しようとする。programを生成するにはsourceが必要だが、sourceの生成には別のルールの実行が必要だ。Makefileはこの依存関係を自動で解決してくれる。

$ touch source01 source02 source03
$ ls
Makefile source01 source02 source03
$ make
cat source01 source02 source03 > source
cat source > program
$ ls
Makefile program source source01 source02 source03

すでにmakeを実行したあとで、もう一度makeを実行するとどうなるだろうか。

$ make
make: 'program' is up to date.

このメッセージの意味は「programは最新だ」という意味だ。makeはファイルのタイムスタンプを調べ、もしファイルprogramよりsourceのタイムスタンプの方が若い場合、つまりprogramが変更されたよりもあとにsourceが変更された場合、ルールを実行する。

試しにファイルsource02のタイムスタンプを更新してみよう。

$ touch source02
$ make
cat source01 source02 source03 > source
cat source > program

ファイルsource事前要件source02を含む。source02のタイムスタンプがsourceより若いので、sourceが再び生成される。すると、sourceのタイムスタンプがprogramのタイムスタンプよりも若くなったので、programも生成される。

もう1つ例を見てみよう。

$ touch a b c
$ ls
a b c Makefile

あるディレクトリーにファイルa, b, cがある。

Makefileは以下の内容になっている。

D : A B C
    cat A B C > D

A : a
    cat a > A

B : b
    cat b > B

C : c
    cat c > C

このMakefileを呼び出したときに作られるのはファイルDだ。ファイルDを作るにはファイルA, B, Cが必要だ。このファイルはそれぞれファイルa, b, cから生成されるルールが記述してある。

これをmakeすると以下のようにファイルA, B, C, Dが作られる。

$ ls
a b c Makefile
$ make
cat a > A
cat b > B
cat c > C
cat A B C > D

ここで、ファイルbのタイムスタンプだけを更新してmakeしてみよう。

$ touch b
$ make
cat b > B
cat A B C > D

ファイルbのタイムスタンプがファイルBより若くなったので、ファイルBがターゲットとなったルールが再び実行される。ファイルA, Cのルールは実行されない。そしてファイルBのタイムスタンプがファイルDより若くなったので、ファイルDがターゲットとなったルールが再び実行される。

makeにより、処理する必要のあるルールだけが部分的に処理されていることがわかる。

makeは適切なルールさえ書けば、依存関係の解決を自動的に行ってくれる。

コメント

Makefileにはコメントを書くことができる。#で始まる行はコメント扱いされる。

# programを生成するルール
program : source
    cat source > program

# sourceを生成するルール
source : source01 source02 source03
    cat source01 source02 source03 > source

変数

Makefileには変数を書くことができる。

変数の文法は以下のとおり。

variable = foobar

target : $(variable)

これは、

target : foobar

と書いたものと同じように扱われる。

変数は=の左側に変数名、右側に変数の内容を書く。

変数を使うときは、$(変数名)のように、$()で変数名を包む。

自動変数

GNU Makeは便利なことに、いくつかの変数を自動で作ってくれる。

$@ ターゲット

$@はルールのターゲットのファイル名になる。

target :
    echo $@

このMakefileを実行すると以下のように出力される。

$ make
echo target

$< 最初の事前要件

$<はルールの最初の事前要件のファイル名になる。

target : A B C
    echo $<

このMakefileを実行すると以下のように出力される。

$ make
echo A

$^ すべての事前要件

$^はすべての事前要件のファイル名が空白区切りされたものになる

target : A B C
    echo $^

このMakefileを実行すると以下のように出力される。

$ make
echo A B C

自動変数の組み合わせ

例えばターゲットを生成するために事前要件ターゲットのファイル名をレシピに書く場合、

target : prerequisite
    cat prerequisite > target

と書く代わりに、

target : prerequisite
    cat $< > $@

と書ける。

PHONYターゲット

PHONYターゲットとは、ファイル名を意味せず、単にレシピを実行するターゲット名としてのみ機能するターゲットのことだ。

hi :
    echo hi

hello :
    echo hello

これを実行すると以下のようになる。

$ make
echo hi
hi
$ make hi
echo hi
hi
$ make hello
echo hello
hello

makeを引数を付けずに実行すると、一番上に書かれたルールが実行される。引数としてターゲットを指定すると、そのターゲットのルールと、依存するルールが実行される。

ただし、ターゲットと同じファイル名が存在すると、ルールは実行されない。

$ touch hello
$ make hello
make: 'hello' is up to date.

GNU Makeはこの問題に対処するため、.PHONYターゲットという特殊な機能がある。これはPHONYターゲットを.PHONYターゲットの事前要件とすることで、ターゲットと同じファイル名の存在の有無にかかわらずルールを実行させられる。

hello :
    echo hello

.PHONY : hello

PHONYターゲットはコンパイルしたプログラムの実行や削除に使うことができる。

hello : hello.cpp
    g++ -o $@ $<

run : hello
    ./hello

clean :
    rm -rf ./hello

.PHONY : run clean

入門用の環境構築

以上を踏まえて、C++入門用の環境構築をしてこの章のまとめとする。

今回構築する環境のファイル名とその意味は以下のとおり。

main.cpp
C++のコードを書く all.h
標準ライブラリのヘッダーファイルを書く all.h.gch
コンパイル済みヘッダー program
実行可能ファイル Makefile
GNU Makeのルールを書く

使い方は以下のとおり。

make
コンパイルする make run
コンパイルして実行 make clean
コンパイル結果を削除

GCCに与えるコンパイラーオプションを変数にまとめる。

gcc_options = -std=c++17 -Wall --pedantic-error

言語はC++17、すべての警告を有効にし、規格準拠ではないコードはエラーとする。

プログラムをコンパイルする部分は以下のとおり。

program : main.cpp all.h all.h.gch
    g++ $(gcc_options) -include all.h $< -o $@

all.h.gch : all.h
    g++ $(gcc_options) -x c++-header -o $@ $<

実行可能ファイルprogramと、コンパイル済みヘッダーall.h.gchをコンパイルするルールだ。

PHONYターゲットは以下のとおり。

run : program
    ./program

clean :
    rm -f ./program
    rm -f ./all.h.gch

.PHONY : run clean

makeでコンパイル。make runで実行。make cleanでコンパイル結果の削除。

Makefile全体は以下のようになる。

gcc_options = -std=c++17 -Wall --pedantic-errors

program : main.cpp all.h all.h.gch
    g++ $(gcc_options) -include all.h $< -o $@

all.h.gch : all.h
    g++ $(gcc_options) -x c++-header -o $@ $<

run : program
    ./program

clean :
    rm -f ./program
    rm -f ./all.h.gch

.PHONY : run clean

C++ヒッチハイクガイド

プログラミング言語の個々の機能の解説を理解するためには、まず言語の全体像を掴まなければならない。この章ではC++のさまざまなコードをひと通り観光していく。ここではコードの詳細な解説はしない。

最小のコード

以下はC++の最小のコードだ。

int main(){}

暗号のようなコードで訳がわからないが、これが最小のコードだ。mainというのはmain関数のことだ。C++ではプログラムの実行はmain関数から始まる。

ソースコードにコメントを記述して、もう少しわかりやすく書いてみよう。

int     // 関数の戻り値の型
main    // 関数名
()      // 関数の引数
{       // 関数の始まり
        // 実行される処理
}       // 関数の終わり

//から行末まではコメントだ。コメントには好きなことを書くことができる。

このコードと1つ前のコードは、コメントの有無を別にすれば何の違いもない。このコードで使っている、intとかmainとか記号文字の1つ1つをトークン(token)と呼ぶ。C++ではトークンの間に空白文字や改行文字をいくら使ってもよい。

なので、

int main(){ }

と書くこともできるし、

int    main    (    )    {   }

と書くこともできるし、紙に印刷する都合上とても読みづらくなるかもしれないが

int
main
(
)
{
}

と書くこともできる。

ただし、トークンの途中で空白文字や改行文字を使うことはできない。以下のコードは間違っている。

i
nt ma in(){}

標準出力

// helloと改行を出力するプログラム
int main()
{
    std::cout << "hello"s ;
}

標準出力はプログラムの基本だ。C++で標準出力する方法はいくつもあるが、<iostream>ライブラリを利用するものが最も簡単だ。

std::coutは標準出力を使うためのライブラリだ。

<<operator <<という演算子だ。C++では演算子にも名前が付いていて、例えば+operator +となる。<<も演算子の一種だ。

"hello"sというのは文字列で、二重引用符で囲まれた中の文字列が標準出力に出力される。

セミコロン;は文の区切り文字だ。C++では文の区切りは明示的にセミコロンを書く必要がある。ほかの言語では改行文字を文脈から判断して文の区切りとみなすこともあるが、C++では明示的に文の区切り文字としてセミコロンを書かなければならない。

セミコロンを書き忘れるとエラーとなる。

int main()
{
    // エラー! セミコロンがない
    std::cout << "error"s
}

複数の文を書いてみよう。

int main()
{
    std::cout << "one "s ;
    std::cout << "two "s ;
    std::cout << "three "s ;
}

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

one two three 

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

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

int main()
{
    std::cout << "aaa"s << "bbb"s << "ccc"s ;
}

出力はaaabbbcccとなる。

文字列

二重引用符で囲まれた文字列を、文字どおり文字列という。文字列には末尾にsが付くものと付かないものがある。これには違いがあるのだが、わからないうちはsを付けておいた方が便利だ。

int main()
{
    // これは文字列
    std::cout << "hello"s ;
    // これも文字列、ただし不便
    std::cout << "hello" ;
}

文字列リテラルの中にバックスラッシュを書くと、エスケープシーケンスとして扱われる。最もよく使われるのは改行文字を表す\nだ。

int main()
{
    std::cout << "aaa\nbbb\nccc"s ;
}

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

aaa
bbb
ccc

バックスラッシュを文字列で使いたい場合は\\と書かなければならない。

int main()
{
    std::cout << "\\n is a new-line.\n"s ;
}

文字列は演算子operator +で「足す」ことができる。「文字列を足す」というのは、「文字列を結合する」という意味だ。

int main()
{
    std::cout << "hello"s + "world"s ;
}

整数と浮動小数点数

iostreamは文字列のほかにも、整数や浮動小数点数を出力できる。さっそく試してみよう。

int main()
{
    std::cout
        << "Integer: "s << 42 << "\n"s
        << "Floating Point: "s << 3.14 ;
}

-1230123といった数値を整数という。3.14のような数値を浮動小数点数という。

数値を扱えるのだから、計算をしてみたいところだ。C++は整数同士の演算子として、四則演算(+-*/)や剰余(%)をサポートしている。

int main()
{
    std::cout
        << 3 + 5 << " "s << 3 - 5 << " "s
        << 3 * 5 << " "s << 3 / 5 << " "s
        << 3 % 5 ;
}

演算子は組み合わせて使うこともできる。その場合、演算子*/%は演算子+-よりも優先される。

int main()
{
    // 7
    std::cout << 1 + 2 * 3 ;
}

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

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

int main()
{
    // 9
    std::cout << (1 + 2) * 3 ;
}

これは1+2が先に計算され3となり、3*3が計算され9となる。

浮動小数点数同士でも四則演算ができる。剰余はできない。

int main()
{
    std::cout
        << 3.5 + 7.11 << " "s << 3.5 - 7.11 << " "s
        << 3.5 * 7.11 << " "s << 3.5 / 7.11 ;
}

では整数と浮動小数点数を演算した場合どうなるのだろう。さっそく試してみよう。

int main()
{
    std::cout << 1 + 0.1 ;
}

結果は1.1だ。整数と浮動小数点数を演算した結果は浮動小数点数になる。

そういえばC++には文字列もあるのだった。文字列と文字列は足すことができる。数値と数値も足すことができる。では数値と文字列を足すとどうなるのだろう。

int main()
{
    std::cout << 1 + "234"s ;
}

この結果はエラーになる。

いや待て、C++には末尾にsを付けない文字列もあるのだった。これも試してみよう。

int main()
{
    std::cout << 1 + "234" ;
}

結果はなんと34になるではないか。C++では謎の数学により1 + "234" = "34"であることが判明した。この謎はいずれ解き明かすとして、いまは文字列には必ず末尾にsを付けることにしよう。その方が安全だ。

変数(variable)

さあどんどんプログラミング言語によくある機能を見ていこう。次は変数だ。

int main()
{
    // 整数の変数
    auto answer = 42 ;
    std::cout << answer << "\n"s ;
    // 浮動小数点数の変数
    auto pi = 3.14 ;
    std::cout << pi << "\n"s ;

    // 文字列の変数
    auto question = "Life, The Universe, and Everything."s ;
    std::cout << question ;
}

変数はキーワードautoに続いて変数名を書き、=に続いて値を書くことで宣言できる。変数の宣言は文なので、文末にはセミコロンが必要だ。

auto 変数名 = 値 ;

変数名はキーワード、アンダースコア(_)で始まる名前、アンダースコア2つ(__)を含む名前以外は自由に名付けることができる。

変数の最初の値は、= 値の代わりに(値){値}と書いてもよい。

int main()
{
    auto a = 1 ;
    auto b(2) ;
    auto c{3} ;
}

この=, (), {}による変数の初期値の指定を、初期化という。

変数は使う前に宣言しなければならない。

int main()
{
    // エラー、名前xは宣言されていない
    std::cout << x ;
    auto x = 123 ;
}

変数の値は初期化したあとにも演算子=で変更できる。これを代入という。

int main()
{
    // 変数の宣言
    auto x
    // 初期化
    = 123 ;

    // 123
    std::cout << x ;

    // 代入
    x = 456 ;

    // 456
    std::cout << x ;

    // もう一度代入
    x = 789 ;
    // 789
    std::cout << x ;
}

代入演算子operator =は左辺に変数名を、右辺に代入する値を書く。面白いこととして、右辺には代入する変数名そのものを書ける。

int main()
{
    auto x = 10 ;
    x = x + 5 ;

    // 15
    std::cout << x ;
}

operator =は「代入」という意味で、「等号」という意味ではないからだ。x=x+5は、「xx+5は等しい」という独創的な数学上の定義ではなく、「変数xに代入前の変数xの値に5を加えた数を代入する」という意味だ。

変数のいまの値に対して演算した結果を変数に代入するという処理はとてもよく使うので、C++にはx = x + aと同じ意味で使える演算子、operator +=もある。

int main()
{
    auto x = 1 ;
    // x = x + 5と同じ
    x += 5 ;
}

operator +=と同様に、operator -=, operator *=, operator /=, operator %=もある。

C++の変数は、専門用語を使うと「静的型付け」になる。静的型付けと対比されるのが「動的型付け」だ。もっと難しく書くと、動的型付け言語の変数は、C++で言えば型情報付きのvoid *型の変数のような扱いを受ける。

C++の変数にはがある。というのは値の種類を表す情報のことだ。

例えば、以下は変数が動的型付けの言語JavaScriptのコードだ。

var x = 1 ;
x = "hello" ;
x = 2 ;

JavaScriptではこのコードは正しい。変数xは数値型であり、文字列型に代わり、また数値型に戻る。

C++ではこのようなコードは書けない。

int main()
{
    auto x = 1 ;
    // エラー
    x = "hello"s ;
    x = 2 ;
}

C++では、変数xは整数型であり、文字列型に変わることはない。整数型の変数に文字列型を代入しようとするとエラーとなる。

C++では型に名前が付いている。整数型はint、浮動小数点数型はdouble、文字列型はstd::stringだ。

int main()
{
    // iはint型
    auto i = 123 ;
    // dはdouble型
    auto d = 1.23 ;
    // sはstd::string型
    auto s = "123"s ;
}

実は変数の宣言でautoと書く代わりに、具体的な型を書いてもよい。

int main()
{
    int i           = 123 ;
    double d        = 1.23 ;
    std::string s   = "123"s ;
}

整数型(int)と浮動小数点数型(double)はそれぞれお互いの型の変数に代入できる。ただし、変数の型は変わらない。単に一方の型の値がもう一方の型の値に変換されるだけだ。

int main()
{
    // 浮動小数点数型を整数型に変換
    int a = 3.14 ;
    // 3
    std::cout << a << "\n"s ;

    // 整数型を浮動小数点数型に変換
    double d = 123 ;
    // 123
    std::cout << d ;
}

浮動小数点数型を整数型に変換すると、小数部が切り捨てられる。この場合、3.14の小数部0.14が切り捨てられ3となる。0.9999も小数部が切り捨てられ0になる。

int main()
{
    int i = 0.9999 ;
    // 0
    std::cout << i ;
}

整数型を浮動小数点数型に変換すると、値を正確に表現できる場合はその値になる。正確に表現できない場合は近い値になる。

int main()
{
    double d = 1234567890 ;
    // 正確に表現できるかどうかわからない
    std::cout << d ;
}

整数型と浮動小数点数型の挙動についてはあとの章で詳しく解説する。また、これ以外にも型はいくらでもあるし、読者が新しい型を作り出すこともできる。これもあとの章で詳しく解説する。

関数(function)

「変数ぐらい知っている。さっさと教えてもらいたい。どうせC++の関数は書きづらいのだろう」と考える読者の皆さん、お待たせしました。こちらがC++の関数でございます。

int main()
{
    // 関数
    auto print = [](auto x)
    {
        std::cout << x << "\n"s ;
    } ;

    // 関数呼び出し
    print(123) ;
    print(3.14) ;
    print("hello") ;
}

C++では関数も変数として扱える。auto print =までは変数だ。変数の初期化として関数を書いている。より正確にはラムダ式と呼ばれる関数を値として書くための文法だ。

ラムダ式は以下のような文法を持つ。

[] // ラムダ式導入部
() // 引数
{} // 本体

ラムダ式は[]で始まり、()の中に引数を書き、{}の中の文が実行される。

例えば以下は引数を2回標準出力する関数だ。

int main()
{
    auto twice = [](auto x)
    {
        std::cout << x << " "s << x << "\n"s ;
    } ;

    twice(5) ;
}

引数はauto 引数名で受け取れる。引数を複数取る場合は、カンマ,で区切る。

int main()
{
    auto print_two = []( auto x, auto y )
    {
        std::cout << x << " "s << y << "\n"s ;
    } ;

    print_two( 1, 2 ) ;
    print_two( "Pi is", 3.14 ) ;
}

引数を取らないラムダ式を書く場合は、単に()と書く。

int main()
{
    auto no_args = []()
    {
        std::cout << "Nothing.\n" ;
    } ;

    no_args() ;
}

関数は演算子operator ()を関数の直後に書いて呼び出す。これが演算子であるというのは少し不思議な感じがするが、C++では紛れもなく演算子だ。operator +とかoperator -などと同じ演算子だ。

int main()
{
    // 何もしない関数
    auto func = [](){} ;

    // operator ()の適用
    func() ;
    // これもoperator ()
    func    (   ) ;
}

演算子operator ()は、ラムダ式そのものに対して適用することもできる。

int main()
{
    // 変数fをラムダ式で初期化
    auto f = [](){} ;
    // 変数fを関数呼び出し
    f() ;

    // ラムダ式を関数呼び出し
    [](){}() ;
}

このコードを見ると、operator ()が単なる演算子であることがよくわかるだろう。[](){}がラムダ式でその直後の()が関数呼び出し演算子だ。

関数は値を返すことができる。関数から値を返すには、return文を使う。

int main()
{
    auto plus = []( auto x, auto y )
        { return x + y ; } ;

    std::cout
        << plus( 1, 2 ) << "\n"s
        << plus( 1.5, 0.5 ) << "\n"s
        << plus( "123"s, "456"s) ;
}

関数はreturn文を実行すると処理を関数の呼び出し元に返す。

int main()
{
    auto f = []()
    {
        std::cout << "f is called.\n" ;
        return 0 ; // ここで処理が戻る
        std::cout << "f returned zero.\n" ;
    } ;

    auto result = f() ;
}

これを実行すると以下のようになる。

$ make
f is called.

return文以降の文が実行されていないことがわかる。

本当の関数

実はラムダ式は本当のC++の関数ではない。本当の関数はとても書きづらいので心して読むべきだ。

読者は本書の冒頭で使ったmain関数という言葉を覚えているだろうか。覚えていないとしても、サンプルコードに必ずと言っていいほど出てくるmainという名前は気になっていたことだろう。

int main(){}

これを見ると、聡明な読者はラムダ式と似通ったところがあることに気付くだろう。

[](){}

末尾の(){}が同じだ。これは同じ意味だ。()は関数の引数で、{}は関数の本体だ。

では残りの部分はどうだろうか。intは関数の戻り値の型、mainは関数の名前だ。

C++の本当の関数は以下のような文法で定義される。

int     // 戻り値の型
main    // 関数名
()      // 関数の引数
{}      // 関数の本体

試しに、int型の引数を2つ取り足して返す関数plusを書いてみよう。

int plus( int x, int y )
{
    return x + y ;
}

int main()
{
    auto x = plus( 1, 2 ) ;
}

では次に、double型の引数を2つ取り足して返す関数plusを書いてみよう。

double plus( double x, double y )
{
    return x + y ;
}

int main()
{
    auto x = plus( 1.0, 2.0 ) ;
}

最後のstd::string型の引数を2つ取り足して返す関数plusは読者への課題とする。

これがC++の本当の関数だ。C++の関数では、型をすべて明示的に書かなければならない。型を間違えるとエラーだ。

しかも、C++の関数は、戻り値の型を正しく返さなければならない。

int f()
{
    // エラー、return文がない
}

もし、何も値を返さない関数を書く場合は、どの値でもないという特別な型、void型を関数の戻り値の型として書かなければならないという特別なルールまである。

void f()
{
    // OK
}

ただし、戻り値の型については、具体的な型の代わりにautoを書くこともできる。その場合、return文で同じ型さえ返していれば、気にする必要はない。

// void
auto a() { }
// int
auto b() { return 0 ; }
// double
auto c() { return 0.0 ; }
// std::string
auto d() { return ""s ; }

// エラー
// return文の型が一致しない。
auto e()
{
    return 0 ;
    return 0.0 ;
}

デバッグ:コンパイルエラーメッセージの読み方

やれやれ疲れた。この辺でひと休みして、デバッグについて考えよう。まずはコンパイルエラーについてだ。

プログラムにはさまざまなバグがあるが、コンパイルエラーは最も簡単なバグだ。というのも、プログラムのバグの存在が実行前に発覚したわけだから、手間が省ける。もしコンパイルエラーにならない場合、実行した結果から、バグがあるかどうかを判断しなければならない。

読者の中には、せっかく書いたソースコードをコンパイルしたらコンパイルエラーが出たので、運が悪かったとか、失敗したとか、怒られてつらい気持ちになったなどと感じることがあるかもしれない。しかしそれは大違いだ。コンパイラーによって読者はプログラムを実行することなくバグが発見できたのだから、読者は運が良かった、大成功した、褒められて最高の気持ちになったと感じるべきなのだ。

さあ皆さんご一緒に、

熟練のプログラマーは自分の書いたコードがコンパイルエラーを出さずに一発でコンパイルが通った場合、逆に不安になるくらいだ。

もしバグがあるのにコンパイルエラーが出なければ、バグの存在に気が付かないまま、読者の書いたソフトウェアは広く世の中に使われ、10年後、20年後に最もバグが発見されてほしくない方法で発見されてしまうかもしれない。すなわち、セキュリティ上問題となる脆弱性という形での発覚だ。しかし安心してほしい。いま読者が出したコンパイルエラーによって、そのような悲しい未来の可能性は永久に排除されたのだ。コンパイルエラーはどんどん出すとよい。

コンパイルエラーの原因は2つ。

  1. 文法エラー
  2. 意味エラー
  3. コンパイラーのバグ

3つだった。コンパイルエラーの原因は3つ。

  1. 文法エラー
  2. 意味エラー
  3. コンパイラーのバグ
  4. コンピューターの故障

4つだった。ただ、3.と4.はめったにないから無視してよい。

文法エラー

文法エラーとは、C++というプログラミング言語の文法に従っていないエラーのことだ。これはC++として解釈できないので、当然エラーになる。

よくある文法エラーとしては、文末のセミコロンを打ち忘れたものがある。例えば以下のコードには間違いがある。

int main()
{
    auto x = 1 + 1 
    auto y = x + 1 ;
}

これをコンパイルすると以下のようにコンパイルエラーメッセージが出力される。

$ make
g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program
main.cpp: In function ‘int main()’:
main.cpp:4:5: error: expected ‘,’ or ‘;’ before ‘auto’
     auto y = x + 1 ;
     ^~~~
main.cpp:3:10: warning: unused variable ‘x’ [-Wunused-variable]
     auto x = 1 + 1
          ^
Makefile:4: recipe for target 'program' failed
make: *** [program] Error 1

コンパイラーのメッセージを読み慣れていない読者はここで考えることを放棄してコンピューターの電源を落とし家を出て街を徘徊し夕日を見つめて人生、宇宙、すべてについての究極の質問への答えを模索してしまうことだろう。

しかし恐れるなかれ。コンパイラーのエラーメッセージを読み解くのは難しくない。

まず最初の2行を見てみよう。

$ make
g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program

1行目はシェルにmakeを実行させるためのコマンド、2行目はmakeが実行したレシピの中身だ。これはコンパイラーによるメッセージではない。

3行目からはコンパイラーによる出力だ。

main.cpp: In function ‘int main()’:

コンパイラーはソースファイルmain.cppの中の、int main()という関数について、特に言うべきことがあると主張している。

言うべきこととは以下だ。

main.cpp:4:5: error: expected ‘,’ or ‘;’ before ‘auto’
     auto y = x + 1 ;
     ^~~~

GCCというコンパイラーのエラーメッセージは、以下のフォーマットを採用している。

ソースファイル名:行番号:列番号: メッセージの種類: メッセージの内容

ここでのメッセージの種類はerror、つまりこのメッセージはエラーを伝えるものだ。

ソースファイル名はmain.cpp、つまりエラーはmain.cppの中にあるということだ。

行番号というのは、最初の行を1行目とし、改行ごとにインクリメントされていく。今回のソースファイルの場合、以下のようになる。

1 int main()
2 {
3     auto x = 1 + 1 
4     auto y = x + 1 ;
5 }

もし読者が素晴らしいテキストエディターであるVimを使っている場合、:set nuすると行番号を表示できる。

その上でエラーメッセージの行番号を確認すると4とある。つまりコンパイラーは4行目に問題があると考えているわけだ。

4行目を確認してみよう。

    auto y = x + 1 ;

何の問題もないように見える。さらにエラーメッセージを読んでみよう。

列番号が5となっている。列番号というのは、行頭からの文字数だ。最初の文字を1文字目とし、文字ごとにインクリメントされていく。

123456789...
    auto y = x + 1 ;

4行目は空白文字を4つ使ってインデントしているので、autoaの列番号は5だ。ここに問題があるのだろうか。何も問題がないように見える。

この謎を解くためには、メッセージの内容を読まなければならない。

expected ‘,’ or ‘;’ before ‘auto’
     auto y = x + 1 ;
     ^~~

これは日本語に翻訳すると以下のようになる。

‘auto’の前に','か';'があるべき
     auto y = x + 1 ;
     ^~~

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

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

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

    ,auto y = x + 1 ;

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

main.cpp: In function ‘int main()’:
main.cpp:4:6: error: expected unqualified-id before ‘auto’
     ,auto y = x + 1 ;
      ^~~~

では';'ならばどうか。

    ;auto y = x + 1 ;

これはコンパイルが通るようだ。

しかしなぜこれでコンパイルが通るのだろう。そのためには、コンパイラーが問題だとした行の1つ上の行を見る必要がある。

    auto x = 1 + 1
    auto y = x + 1 ;

コンパイラーにとって、改行は空白文字と同じくソースファイル中の意味のあるトークン(キーワードや名前や記号)を区切る文字でしかない。コンパイラーにとって、このコードは実質以下のように見えている。

auto x=1+1 auto y=x+1;

"1 auto"というのは文法エラーだ。なのでコンパイラーは文法エラーが発覚する最初の文字である'auto''a'を指摘したのだ。

人間にとって自然になるように修正すると、コンパイラーが指摘した行の1つ上の行の行末に';'を追加すべきだ。

    auto x = 1 + 1 ;
    auto y = x + 1 ;

さて、問題自体は解決したわけだが、残りのメッセージも見ていこう。

main.cpp:3:10: warning: unused variable ‘x’ [-Wunused-variable]
     auto x = 1 + 1

これはコンパイラーによる警告メッセージだ。警告メッセージについて詳しくは、デバッグ:警告メッセージの章で解説する。

Makefile:4: recipe for target 'program' failed
make: *** [program] Error 1

これはGNU Makeによるメッセージだ。GCCがソースファイルを正しくコンパイルできず、実行が失敗したとエラーを返したので、レシピの実行が失敗したことを伝えるメッセージだ。

プログラムはどうやってエラーを通知するのか。main関数の戻り値によってだ。main関数は関数であるので、戻り値がある。main関数の戻り値はint型だ。

// 戻り値の型
int
// main関数の残りの部分
main() { }

main関数が何も値を返さない場合、return 0したものとみなされる。main関数が0もしくはEXIT_SUCCESSを返した場合、プログラムの実行の成功を通知したことになる。

// 必ず実行が成功したと通知するプログラム
int main()
{
    return 0 ;
}

プログラムの実行が失敗した場合、main関数はEXIT_FAILUREを返すことでエラーを通知できる。

// 必ず実行が失敗したと通知するプログラム
int main()
{
    return EXIT_FAILURE ;
}

EXIT_SUCCESSEXIT_FAILUREはマクロだ。

#define EXIT_SUCCESS
#define EXIT_FAILURE

その中身はC++標準規格では規定されていない。どうしても値を知りたい場合は以下のプログラムを実行してみるとよい。

int main()
{
    std::cout
        << "EXIT_SUCCESS: "s << EXIT_SUCCESS << "\n"s
        << "EXIT_FAILURE: "s   << EXIT_FAILURE ;  
}

文法エラーというのは厄介なバグだ。というのも、コンパイラーというのは正しい文法のソースファイルを処理するように作られている。文法を間違えた場合、ソースファイル全体が正しくないということになる。コンパイラーは文法違反に遭遇した場合、なるべく人間がよく間違えそうなパターンをヒューリスティックに指摘することもしている。そのため、エラーメッセージに指摘された行番号と列番号は、必ずしも人間にとっての問題の箇所と一致しない。

もう1つ例を見てみよう。

int main()
{
    // 引数を3つ取って足して返す関数
    auto f = [](auto a, auto b, auto c)
    { return a + b + c ; } ;

    std::cout << f(1+(2*3),4-5,6/(7-8))) ;
}

GCCによるコンパイルエラーメッセージだけ抜粋すると以下のとおり。

main.cpp: In function ‘int main()’:
main.cpp:7:40: error: expected ‘;’ before ‘)’ token
     std::cout << f(1+(2*3),4-5,6/(7-8))) ;
                                        ^

さてさっそく読んでみよう。すでに学んだように、GCCのメッセージのフォーマットは以下のとおりだ。

ソースファイル名:行番号:列番号: メッセージの種類: メッセージの内容

これに当てはめると、問題はソースファイルmain.cppの7行目の40列目にある。

エラーメッセージは、「';'がトークン')'の前にあるべき」だ。

トークン(token)というのは'std'とか'::'とか'cout'といったソースファイルの空白文字で区切られた最小の文字列の単位のことだ。

抜粋されたソースコードに示された問題の箇所、つまり7行目40列目にあるトークンは')'だ。この前に';'が必要とはどういうことだろう。

問題を探るため、7行目のトークンを詳しく分解してみよう。以下は7行目と同じソースコードだが、トークンをわかりやすく分解してある。

std::cout << // 標準出力
f // 関数名
    ( // 開き括弧
        1+(2*3),    // 第1引数
        4-5,        // 第2引数
        6/(7-8)     // 第3引数
    ) // 開き括弧に対応する閉じ括弧
    ) // ???
    ; // 終端文字

これを見ると、閉じ括弧が1つ多いことがわかる。

意味エラー

意味エラーとは、ソースファイルは文法的に正しいが、意味的に間違っているコンパイルエラーのことだ。

さっそく例を見ていこう。

int main()
{
    auto x = 1.0 % 1.0 ;
}

このコードをコンパイルすると出力されるエラーメッセージは以下のとおり。

main.cpp: In function ‘int main()’:
main.cpp:3:18: error: invalid operands of types ‘double’ and ‘double’ to binary ‘operator%’
     auto x = 1.0 % 1.0 ;
              ~~~~^~~~~

問題の箇所は3行目の18列目、'%'だ。

エラーメッセージは、「二項 'operator%'に対して不適切なオペランドである型'double''double'」とある。

前の章を読み直すとわかるとおり、operator %は剰余を計算する演算子だが、この演算子にはdouble型を渡すことはできない。

このコードはどうだろう。

// 引数を1つ取る関数
void f( int x ) { }

int main()
{
    // 引数を2つ渡す
    f( 1, 2 ) ;
}

このようなエラーメッセージになる。

main.cpp: In function ‘int main()’:
main.cpp:7:13: error: too many arguments to function ‘void f(int)’
     f( 1, 2 ) ;
             ^
main.cpp:2:6: note: declared here
 void f( int x ) { }
      ^

問題の箇所は7行目。「関数'void f(int)'に対して実引数が多すぎる」とある。関数fは引数を1つしか取らないのに、2つの引数を渡しているのがエラーの原因だ。

2つ目のメッセージはエラーではなくて、エラーを補足説明するための注記(note)メッセージだ。ここで言及している関数fとは、2行目に宣言されていることを説明してくれている。

意味エラーはときとしておぞましいほどのエラーメッセージを生成することがある。例えば以下の一見無害そうなコードだ。

int main()
{
    "hello"s << 1 ;
}

このコードは文法的に正しいが、意味的に間違っているコードだ。このコードをコンパイルすると膨大なエラーメッセージが出力される。しかも問題の行番号特定以外、大して役に立たない。

コンパイラーのバグ

C++コンパイラーもソフトウェアであり、バグがある。コンパイラーにバグがある場合、正しいC++のソースファイルがコンパイルできないことがある。

読者がそのようなコンパイラーの秘孔を突くコードを書くことはまれだ。しかし、もしそのようなコードを偶然にも書いてしまった場合、GCCは、

gcc: internal compiler error: エラー内容
Please submit a full bug report,
with preprocessed source if appropriate.
See <ドキュメントへのファイルパス> for instructions.

のようなメッセージを出力する。

これはGCCのバグなので、見つけた読者は適切な方法でバグ報告をしよう。

条件分岐の果てのレストラン

さてC++の勉強に戻ろう。この章では条件分岐について学ぶ。

複合文

条件分岐とループについて学ぶ前に、まず複合文(compound statement)やブロック(block)と呼ばれている、複数の文をひとまとめにする文について学ばなければならない。

C++では(statement)が実行される。については詳しく説明すると長くなるが、';'で区切られたものがだ。

int main()
{
    // 文
    auto x = 1 + 1 ;
    // 文
    std::cout << x ;

    // 空文
    // 実は空っぽの文も書ける。
    ;
}

複数の{}で囲むことで、1つの文として扱うことができる。これを複合文という。

int main()
{
    // 複合文開始
    {
        std::cout << "hello\n"s ;
        std::cout << "hello\n"s ;
    } // 複合文終了

    // 別の複合文
    { std::cout << "world\n"s ; }

    // 空の複合文
    { }
}

複合文には';'はいらない。

int main()
{
    // ;はいらない
    { }

    // これは空の複合文に続いて
    // 空文があるだけのコード
    { } ;
}

複合文の中に複合文を書くこともできる。

int main()
{
    {{{}}} ;
}

関数の本体としての一番外側'{}'はこの複合文とは別のものだが、読者はまだ気にする必要はない。

複合文は複数のをひとまとめにして、1つのとして扱えるようにするぐらいの意味しか持っていない。ただし、変数の見え方に影響する。変数は宣言された最も内側の複合文の中でしか使えない。

int main()
{
    auto a = 0 ;

    {
        auto b = 0 ;
        {
            auto c = 0 ;
            // cはここまで使える
        }
        // bはここまで使える
    }
    // aはここまで使える
}

これを専門用語では変数寿命とかブロックスコープ(block-scope)という。

内側のブロックスコープの変数が、外側のブロックスコープの変数と同じ名前を持っていた場合はエラーではない。外側の変数が内側の変数で隠される。

int main()
{
    auto x = 0 ;
    {
        auto x = 1 ;
        {
            auto x = 2 ;
            // 2
            std::cout << x ;
        }
        // 1
        std::cout << x ;
        x = 42 ;
        // 42
        std::cout << x ;
    }
    // 0
    std::cout << x ;
}

慣れないうちは驚くかもしれないが、多くのプログラミング言語はこのような挙動になっているものだ。

条件分岐

すでに読者はさまざまな数値計算を学んだ。読者は12345 + 6789の答えや、8073 * 132 / 5の答えを計算できる上、この2つの答えをさらに掛け合わせた結果だって計算できる。

int main()
{
    auto a = 12345 + 6789 ;
    auto b = 8073 * 132 / 5 ;
    auto sum = a + b ;

    std::cout
        << "a=12345 + 6789=" << a << "\n"s
        << "b=8073 * 132 / 5=" << b << "\n"s
        << "a+b=" << sum << "\n"s ;
}

なるほど、答えがわかった。ところで変数aと変数bはどちらが大きいのだろうか。大きい変数だけ出力したい。この場合は条件分岐を使う。

C++では条件分岐にif文を使う。

int main()
{
    auto a = 12345 + 6789 ;
    auto b = 8073 * 132 / 5 ;


    if ( a < b )
    {
        // bが大きい
        std::cout << b ;
    }
    else
    {
        // aが大きい
        std::cout << a ;
    }
}

if文は以下のように書く。

if ( 条件 )
1
else
2

条件が真(true)のときは文1が実行され、偽(false)のときは文2が実行される。

elseの部分は書かなくてもよい。

if ( 条件 )
1
2

その場合、条件が真のときだけ文1が実行される。条件の真偽にかかわらず文2は実行される。

int main()
{
    if ( 2 < 1 )
        std::cout << "sentence 1.\n" ; // 文1
    std::cout << "sentence 2.\n" ; // 文2
}

この例では、21より小さい場合は文1が実行される。文2は必ず実行される。

条件次第で複数の文を実行したい場合、複合文を使う。

int main()
{
    if ( 1 < 2 )
    {
        std::cout << "yes!\n" ;
        std::cout << "yes!\n" ;
    }
}

条件とか真偽についてはとてもとても深い話があるのだが、その解説はあとの章に回すとして、まずは以下の比較演算子を覚えよう。

演算子 意味
a == b abと等しい
a != b abと等しくない
a < b abより小さい
a <= b abより小さい、もしくは等しい
a > b abより大きい
a >= b abより大きい、もしくは等しい

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

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

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

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

int main()
{
    auto a = "dog"s ;
    auto b = "dog"s ;
    auto c = "cat"s ;

    if ( a == b )
    {
        std::cout << "a == b\n"s ;
    }
    else
    {
        std::cout << "a != b\n" ;
    }

    if ( a == c )
    {
        std::cout << "a == c\n" ;
    }
    else
    {
        std::cout << "a != c\n" ;
    }
}

では文字列に大小はあるのだろうか。文字列に大小はある。

int main()
{
    auto cat = "cat"s ;
    auto dog = "dog"s ;

    if ( cat < dog )
    {   // 猫は小さい
        std::cout << "cat is smaller.\n"s ;
    }
    else
    {   // 犬は小さい
        std::cout << "dog is smaller.\n"s ;
    }

    auto longcat = "longcat"s ;

    if ( longcat > cat )
    {   // longcatは長い
        std::cout << "Longcat is Looong.\n"s ;
    }
    else
    {
        std::cout << "Longcat isn't that long. Sigh.\n"s ;
    }
}

実行して確かめてみよう。ほとんどの読者の実行環境では以下のようになるはずだ。ほとんどの、というのは、そうではない環境も存在するからだ。読者がそのような稀有な環境を使っている可能性はまずないだろうが。

cat is smaller.
Longcat is Looong.

なるほど。"cat"s"dog"sよりも小さく(?)、"longcat"s"cat"sよりも長い(大きい?)ようだ。なんだかよくわからない結果になった。

これはどういうことなのか。もっと簡単な文字列で試してみよう。

int main()
{
    auto x = ""s ;

    // aとbはどちらが小さいのだろうか?
    if ( "a"s < "b"s )
    {   x = "a"s ; }
    else
    {   x = "b"s ; }
 
    // 小さい方の文字が出力される
    std::cout << x ;
}

これを実行するとaと出力される。すると"a"s"b"sより小さいようだ。

もっと試してみよう。

int main()
{
    auto x = ""s ;
    if ( "aa"s < "ab"s )
    { x = "aa"s ; }
    else
    { x = "ab"s ; }

    // 小さい文字列が出力される
    std::cout << x ;
}

これを実行すると、aaと出力される。すると"aa"s"ab"sより小さいことになる。

文字列の大小比較は文字単位で行われる。まず最初の文字が大小比較される。もし等しい場合は、次の文字が大小比較される。等しくない最初の文字の結果が、文字列の大小比較の結果となる。

条件式

条件とは何だろう

if文の中で書く条件(condition)は、条件式(conditional expression)とも呼ばれている(expression)の一種だ。というのは例えば"1+1"のようなものだ。の中に書くことができ、これを式文(expression statement)という。

int main()
{
    1 + 1 ; // 式文
}

"a==b""a\<b"のような条件なので、として書くことができる。

int main()
{
    1 == 1 ;
    1 < 2 ;
}

C++では多くの式には型がある。たとえば"123"int型で、"123+4"int型だ。

int main()
{
    auto a = 123 ; // int
    auto b = a + 4 ; // int
    auto c = 1.0 ; // double
    auto d = "hello"s ; // std::string
}

とすると、"1==2""3!=3"のような条件式にも型があるのではないか。型があるのであれば変数に入れられるはずだ。試してみよう。

int main()
{
    if (  1 == 1 )
    { std::cout << "1 == 1 is true.\n"s ; }
    else
    { std::cout << "1 == 1 is false.\n"s ; }

    auto x = 1 == 1 ;
    if ( x )
    { std::cout << "1 == 1 is true.\n"s ; }
    else
    { std::cout << "1 == 1 is false.\n"s ; }
}

"if(x)""if(1==1)"と書いた場合と同じように動く。

変数に入れられるのであれば出力もできるのではないだろうか。試してみよう。

int main()
{
    auto a = 1 == 1 ; // 正しい
    auto b = 1 != 1 ; // 間違い
    std::cout << a << "\n"s << b ;
}
1
0

なるほど、条件が正しい場合"1"になり、条件が間違っている場合"0"になるようだ。

ではif文の中に10を入れたらどうなるのだろうか。

// 条件が正しい値だけ出力される。
int main()
{
    if ( 1 ) std::cout << "1\n"s ;
    if ( 0 ) std::cout << "0\n"s ;
    if ( 123 ) std::cout << "123\n"s ;
    if ( -1 ) std::cout << "-1\n"s ;
}

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

1
123
-1

この結果を見ると、条件として1, 123, -1は正しく、0は間違っているということになる。ますます訳がわからなくなってきた。

bool型

そろそろ種明かしをしよう。条件式の結果は、bool型という特別な型を持っている。

int main()
{
    auto a = 1 == 1 ; // bool型
    bool A = 1 == 1 ; // 型を書いてもよい
}

int型の変数には整数の値が入る。double型の変数には浮動小数点数の値が入る。std::string型の変数には文字列の値が入る。

すると、bool型の変数にはbool型の値が入る。

bool型には2つの値がある。条件が正しいことを意味するtrueと、条件が間違っていることを意味するfalseだ。

int main()
{
    bool correct = true ;
    bool wrong = false ;
}

bool型にこれ以外の値は存在しない。

bool型の値を正しく出力するには、std::boolalphaを出力する。

int main()
{
    std::cout << std::boolalpha ;
    std::cout << true << "\n"s << false ;
}
true
false

std::boolalpha自体は何も出力をしない。一度std::boolalphaを出力すると、それ以降のbool値がtrue/falseで出力されるようになる。

元に戻すにはstd::noboolalphaを使う。

int main()
{
    std::cout << std::boolalpha ;
    std::cout << true << false ;
    std::cout << std::noboolalpha ;
    std::cout << true << false ;
}

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

truefalse10

すでに学んだ比較演算子は、正しい場合にbool型の値trueを、間違っている場合にbool型の値falseを返す。

int main()
{
    // true
    bool a = 1 == 1 ;
    // false
    bool b = 1 != 1 ;

    // true
    bool c = 1 < 2 ;
    // false
    bool d = 1 > 2 ;
}

先に説明したif文条件が「正しい」というのはtrueのことで、「間違っている」というのはfalseのことだ。

int main()
{
    // 出力される
    if ( true )
        std::cout << "true\n"s ;

    // 出力されない。
    if ( false )
        std::cout << "false\n"s ; 
}

bool型の演算

bool型にはいくつかの演算が用意されている。

論理否定: operator !

"!a"atrueの場合falseに、falseの場合trueになる。

int main()
{
    std::cout << std::boolalpha ;

    // false
    std::cout << !true << "\n"s ;

    // true
    std::cout << !false << "\n"s ;
}

論理否定演算子を使うと、falseのときのみ実行されてほしい条件分岐が書きやすくなる。

// ロケットが発射可能かどうかを返す関数
bool is_rocket_ready_to_launch()
{
    // まだだよ
    return false ;
}

int main()
{

    // ロケットが発射可能ではないときに実行される
    if ( !is_rocket_ready_to_launch() )
    {   // もうしばらくそのままでお待ちください
        std::cout << "Standby...\n" ;
    }
}

この例では、ロケットが発射可能でない場合のみ、待つようにアナウンスする。

同じように、trueのときに実行されてほしくない条件分岐も書ける。

// ロケットが発射可能かどうかを返す関数
bool is_rocket_ready_to_launch()
{
    // もういいよ
    return true ;
}

int main()
{
    // ロケットが発射可能なときに実行される
    if ( !is_rocket_ready_to_launch() )
    {   // カウントダウン
        std::cout << "3...2...1...Hallelujah!\n"s ;
    }

}

この2つの例では、ロケットの状態が実行すべき条件ではないので、正しく何も出力されない。

同値比較: operator ==, !=

bool型の値の同値比較はわかりやすい。truetrueと等しく、falsefalseと等しく、truefalseは等しくない。

int main()
{
    std::cout << std::boolalpha ;
    auto print = [](auto b)
    { std::cout << b << "\n"s ; } ;

    print( true  == true  ) ; // true
    print( true  == false ) ; // false
    print( false == true  ) ; // false
    print( false == false ) ; // true

    print( true  != true  ) ; // false
    print( true  != false ) ; // true
    print( false != true  ) ; // true
    print( false != false ) ; // false
}

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

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

int main()
{
    bool b = (1 < 2) == true ;
}

"(1\<2)"trueなので、"(1\<2)==true""true==true"と同じ意味になる。この結果はもちろん"true"だ。

論理積: operator &&

"a && b"abがともにtrueのときにtrueとなる。それ以外の場合はfalseとなる。これを論理積という。

表にまとめると以下のようになる。

結果
false && false false
false && true false
true && false false
true && true true

さっそく確かめてみよう。

int main()
{
    std::cout << std::boolalpha ;
    auto print = []( auto b )
    { std::cout << b << "\n"s ; } ;

    print( false && false ) ; // false
    print( false && true  ) ; // false
    print( true  && false ) ; // false
    print( true  && true  ) ; // true
}

論理積は、「AかつB」を表現するのに使える。

例えば、人間の体温が平熱かどうかを判断するプログラムを書くとする。36.1℃以上、37.2℃以下を平熱とすると、if文を使って以下のように書くことができる。

int main()
{
    // 体温
    double temperature = 36.6 ;

    // 36.1度以上
    if ( temperature >= 36.1 )
        if ( temperature <= 37.2 )
        { std::cout << "Good.\n"s ; }
        else
        { std::cout << "Bad.\n"s ; }
    else
    { std::cout << "Bad.\n"s ; }
}

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

int main()
{
    double temperature = 36.6 ;

    if ( ( temperature >= 36.1 ) && ( temperature <= 37.2 ) )
    { std::cout << "Good.\n"s ; }
    else
    { std::cout << "Bad.\n"s ; }
}

論理和: operator ||

"a || b"abがともにfalseのときにfalseとなる。それ以外の場合はtrueとなる。これを論理和という。

表にまとめると以下のようになる。

結果
false || false false
false || true true
true || false true
true || true true

さっそく確かめてみよう。

int main()
{
    std::cout << std::boolalpha ;
    auto print = []( auto b )
    { std::cout << b << "\n"s ; } ;

    print( false || false ) ; // false
    print( false || true  ) ; // true
    print( true  || false ) ; // true
    print( true  || true  ) ; // true
}

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

例えば、ある遊園地の乗り物には安全上の理由で身長が1.1m未満、あるいは1.9mを超える人は乗れないものとする。この場合、乗り物に乗れる身長かどうかを確かめるコードは、if文を使うと以下のようになる。

int main()
{
    double height = 1.3 ;

    if ( height < 1.1 )
    { std::cout << "No."s ; }
    else if ( height > 1.9 )
    { std::cout << "No."s ; }
    else
    { std::cout << "Yes."s ; }
}

論理和を使うと以下のように簡潔に書ける。

int main()
{
    double height = 1.3 ;

    if ( ( height < 1.1 ) || ( height > 1.9 ) )
    { std::cout << "No."s ; }
    else
    { std::cout << "Yes."s ; }
}

短絡評価

論理積と論理和は短絡評価と呼ばれる特殊な評価が行われる。これは、左から右に最小限の評価をするという意味だ。

論理積では、“a && b”とある場合、abがともにtrueである場合のみ、結果はtrueになる。もし、afalseであった場合、bの結果如何にかかわらず結果はfalseとなるので、bは評価されない。

int main()
{
    auto a = []()
    {
        std::cout << "a\n"s ;
        return false ;
    } ;
    auto b = []()
    {
        std::cout << "b\n"s ;
        return true ;
    } ;

    bool c = a() && b() ;
    std::cout << std::boolalpha << c ; 
}

これを実行すると以下のようになる。

a
false

関数呼び出し"a()"の結果はfalseなので、"b()"は評価されない。評価されないということは関数呼び出しが行われず、当然標準出力も行われない。

同様に、論理和では、"a || b"とある場合、abのどちらか片方でもtrueであれば、結果はtrueとなる。もし、atrueであった場合、bの結果如何にかかわらず結果はtrueとなるので、bは評価されない。

int main()
{
    auto a = []()
    {
        std::cout << "a\n"s ;
        return true ;
    } ;
    auto b = []()
    {
        std::cout << "b\n"s ;
        return false ;
    } ;

    bool c = a() || b() ;
    std::cout << std::boolalpha << c ; 
}

結果、

a
true

"b()"が評価されていないことがわかる。

boolの変換

bool型の値と演算はこれで全部だ。値はtrue/falseの2つのみ。演算は==, !=, !&&||の5つだけだ。

読者の中には納得のいかないものもいるだろう。ちょっと待ってもらいたい。boolの大小比較できないのだろうか。boolの四則演算はできないのか。"if(123)"などと書けてしまうのは何なのか。

好奇心旺盛な読者は本書の解説を待たずしてすでに自分でいろいろとコードを書いて試してしまっていることだろう。

boolの大小比較はどうなるのだろうか。

int main()
{
    std::cout << std::boolalpha ;

    bool b = true < false ;
    std::cout << b ;
}

このコードを実行すると、出力は"false"だ。"true \< false"の結果が"false"だということは、truefalseより大きいということになる。

四則演算はどうか?

int main()
{
    auto print = [](auto x)
    { std::cout << x << "\n"s ; } ;

    print( true  + true  ) ;
    print( true  + false ) ;
    print( false + true  ) ;
    print( false + false ) ;
}

結果、

2
1
1
0

不思議な結果だ。"true+true""2""true+false""1""false+false""0"。これはtrue1false0ならば納得のいく結果だ。大小比較の結果としても矛盾していない。

すでに見たように、std::boolalphaを出力していない状態でboolを出力するとtrue1false0となる。

int main()
{
    std::cout << true << false ;
}

結果、

10

これはbool型整数型が変換されているのだ。

異なる型の値が変換されるというのは、すでに例がある。整数型浮動小数点数型だ。

int main()
{
    // 3
    int i = 3.14 ;
    std::cout << i << "\n"s ;

    // 123.0
    double d = 123 ;
    std::cout << d << "\n"s ;
}

浮動小数点数型整数型に変換できる。その際に小数部は切り捨てられる。整数型浮動小数点数型に変換できる。小数部はない。

これと同じように、bool型整数型と変換ができる。

bool型のtrue整数型に変換すると1になる。false0になる。

int main()
{
    // 1
    int True = true ;
    // 0
    int False = false ;
}

同様に、整数型のゼロをbool型に変換するとfalseになる。非ゼロはtrueになる。

int main()
{
    // false
    bool Zero = 0 ;

    // すべてtrue
    bool One = 1 ;
    bool minus_one = -1 ;
    bool OneTwoThree = 123 ;  
}

したがって、"if (0)""if (false)"と等しく、"if (1)""if(-1)"など非ゼロな値は"if (true)"と等しい。

int main()
{
    // 出力されない
    if ( 0 )
        std::cout << "No output.\n"s ;

    // 出力される
    if ( 1 )
        std::cout << "Output.\n"s ;
}

大小比較は単にboolを整数に変換した結果を比較しているだけだ。"true \< false""1 \< 0"と書くのと同じだ。

int main()
{
    std::cout << std::boolalpha ;

    // 1 < 0
    std::cout << (true < false) ;
}

同様に四則演算もbool型を整数型に変換した上で計算をしているだけだ。"true + true""1 + 1"と書くのと同じだ。

int main()
{
    // 1 + 1
    std::cout << (true + true) ;
}

C++では、bool型整数型の変換は暗黙に行われてしまうので注意が必要だ。

デバッグ: コンパイル警告メッセージ

やれやれ、条件分岐は難しかった。この辺でもう一度ひと休みして、息抜きとしてデバッグの話をしよう。今回はコンパイラーの警告メッセージ(warning messages)についてだ。

コンパイラーはソースコードに文法エラーや意味エラーがあると、エラーメッセージを出すことはすでに学んだ。

コンパイラーがエラーメッセージを出さなかったとき、コンパイラーはソースコードには文法エラーや意味エラーを発見できず、コンパイラーは意味のあるプログラムを生成することができたということを意味する。しかし、コンパイルが通って実行可能なプログラムが生成できたからといって、プログラムにバグがないことは保証できない。

たとえば、変数xyを足して出力するプログラムを考える。

int main()
{
    auto x = 1 ;
    auto y = 2 ;

    std::cout << x + x ;
}

このプログラムにはバグがある。プログラムの仕様は変数xyを足すはずだったが変数xxを足してしまっている。

コンパイラーはこのソースコードをコンパイルエラーにはしない。なぜならば上のコードは文法的に正しく、意味的にも正しいコードだからだ。

警告メッセージはこのような疑わしいコードについて、エラーとまではいかないまでも、文字どおり警告を出す機能だ。例えば上のコードをGCCでコンパイルすると以下のような警告メッセージを出す。

$ make
g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program
main.cpp: In function ‘int main()’:
main.cpp:5:10: warning: unused variable ‘y’ [-Wunused-variable]
     auto y = 2 ;
          ^

すでに説明したように、GCCのメッセージは

ソースファイル名:行番号:列番号:メッセージの種類:メッセージの内容

というフォーマットを取る。

このメッセージのフォーマットに照らし合わせると、このメッセージはソースファイルmain.cppの5行目の10列目について何かを警告している。警告はメッセージの種類としてwarningが使われる。

警告メッセージの内容は、「未使用の変数'y' [-Wunused-variable]」だ。コード中で'y'という名前の変数を宣言しているにもかかわらず、使っている場所がない。使わない変数を宣言するのはバグの可能性が高いので警告しているのだ。

[-Wunused-variable]というのはGCCに与えるこの警告を有効にするためのオプション名だ。GCCに-Wunused-variableというオプションを与えると、未使用の変数を警告するようになる。

$ g++ -Wunused-variable その他のオプション

今回は-Wallというすべての警告を有効にするオプションを使っているので、このオプションを使う必要はない。

もう1つ例を出そう。以下のソースコードは変数xの値が123と等しいかどうかを調べるものだ。

int main()
{
    // xの値は0
    auto x = 0 ;

    // xが123と等しいかどうか比較する
    if ( x = 123 )
        std::cout << "x is 123.\n"s ;
    else
        std::cout << "x is NOT 123.\n"s ;
}

これを実行すると、"x is 123.\n"と出力される。しかし、変数xの値は0のはずだ。なぜか0123は等しいと判断されてしまった。いったいどういうことだろう。

この謎は警告メッセージを読むと解ける。

g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program
main.cpp: In function ‘int main()’:
main.cpp:5:12: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
     if ( x = 123 )
          ~~^~~~~

main.cppの5行目の12列目、「真偽値として使われている代入は括弧で囲むべき」とある。これはいったいどういうことか。よく見てみると、演算子が同値比較に使う==ではなく、=だ。=は代入演算子だ。

int main()
{
    auto x = 0 ;

    // 代入
    // xの値は1
    x = 1 ;

    // 同値比較
    x == 1 ;
}

実はif文条件にはあらゆるを書くことができる。代入というのは、実は代入式という式なので、if文の中にも書くことができる。その場合、式の結果の値は代入される変数の値になる。

そして思い出してほしいのは、整数型はbool型に変換されるということだ。0false、非ゼロはtrueだ。

int main()
{
    auto x = 0 ;
    // 1はtrue
    bool b1 = x = 1 ;
    if ( x = 1 ) ;

    // 0はfalse
    bool b0 = x = 0 ;
    if ( x = 0 ) ;
}

つまり、"if(x=1)"というのは、"if(1)"と書くのと同じで、これは最終的に、"if(true)"と同じ意味になる。

警告メッセージの「括弧で囲むべき」というのは、括弧で囲んだ場合、この警告メッセージは出なくなるからだ。

int main()
{
    auto x = 0 ;

    if ( (x = 0) )
        std::cout << "x is 123.\n"s ;
    else
        std::cout << "x is NOT 123.\n"s ;
}

このコードをコンパイルしても警告メッセージは出ない。

わざわざ括弧で囲むということは、ちゃんと代入を意図して使っていることがわかっていると意思表示したことになり、結果として警告メッセージはなくなる。

この警告メッセージ単体を有効にするオプションは-Wparenthesesだ。

警告メッセージは万能ではない。ときにはまったく問題ないコードに対して警告メッセージが出たりする。これは仕方がないことだ。というのもコンパイラーはソースコード中に表現されていない、人間の脳内にある意図を読むことはできないからだ。ただし、警告メッセージにはひと通り目を通して、それが問題ない誤検知であるかどうかを確認することは重要だ。

最近体重が気になるあなたのための標準入力

これまでのおさらい

ここまで学んできた範囲でも、かなりのプログラムが書けるようになってきた。試しにちょっとプログラムを書いてみよう。

最近肥満が気になる読者は、肥満度を把握するためにBMI(Body Mass Index)を計算して出力するプログラムを書くことにした。

BMIの計算は以下のとおり。

\[ BMI = \frac{体重_{kg}}{身長^2_{m}} \]

本書をここまで読み進めた読者ならば、このようなプログラムは簡単に書けるだろう。計算は小数点以下の値を扱う必要があるために、変数は浮動小数点数型(double)にする。掛け算はoperator *で、割り算はoperator /だ。出力にはstd::coutを使う。

int main()
{
    // 身長1.63m
    double height = 1.63 ;
    // 体重73kg
    double mass = 73.0 ;

    // BMIの計算
    double bmi = mass / (height*height) ;

    // BMIの出力
    std::cout << "BMI="s << bmi << "\n"s ;
}

結果は"27.4756"となった。これだけでは太っているのか痩せているのかよくわからない。調べてみると、BMIの数値と肥満との関係は以下の表のとおりになるそうだ。

BMI 状態
18.5未満 痩せすぎ(Underweight)
18.5以上、25未満 普通(Normal)
25以上、30未満 太り気味(Overweight)
30以上 肥満(Obese)

ではさっそく、この表のようにBMIから肥満状態も出力してくれるように、プログラムを書き換えよう。

int main()
{
    // 身長1.63m
    double height = 1.63 ;
    // 体重73kg
    double mass = 73.0 ;

    // BMIの計算
    double bmi = mass / (height*height) ;

    // BMIの出力
    std::cout << "BMI="s << bmi << "\n"s ;

    // 状態の判定をする関数
    auto status = []( double bmi )
    {
        if ( bmi < 18.5 )
            return "Underweight.\n"s ;
        else if ( bmi < 25.0 )
            return "Normal.\n"s ;
        else if ( bmi < 30.0 )
            return "Overweight.\n"s ;
        else
            return "Obese."s ;
    } ;

    // 状態の出力
    std::cout << status(bmi) ;
}

ここまで問題なく読むことができただろうか。ここまでのコードはすべて、本書を始めから読めば理解できる機能しか使っていない。わからない場合、この先に進む前に本書をもう一度始めから読み直すべきだろう。

標準入力

上のプログラムには実用にする上で1つ問題がある。身長と体重の値を変えたい場合、ソースコードを書き換えてコンパイルしなければならないのだ。

例えば読者の身長が1.8mで体重が80kgの場合、以下のように書き換えなければならない。

int main()
{
    // 身長1.63m
    double height = 1.80 ;
    // 体重73kg
    double mass = 80.0 ;

    // BMIの計算
    double bmi = mass / (height*height) ;

    // BMIの出力
    std::cout << "BMI="s << bmi << "\n"s ;
}

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

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

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

int main()
{
    // 入力を受け取るための変数
    std::string x{} ;
    // 変数に入力を受け取る
    std::cin >> x ;
    // 入力された値を出力
    std::cout << x ;
}

実行結果、

$ make run
hello
hello

標準入力はデフォルトでは、プログラムを実行したユーザーがターミナルから入力する。上の実行結果の2行目は、ユーザーの入力だ。

std::cinは入力された文字列を変数に入れる。入力は空白文字や改行で区切られる。そのため、空白で区切られた文字列を渡すと、以下のようになる。

$ make run
hello world
hello

入力は複数取ることができる。

int main()
{
    std::string x{} ;
    std::string y{} ;
    std::cin >>  x >> y ;
    std::cout << x << y ;
}

実行結果、

$ make run
hello world
helloworld

空白文字は文字列の区切り文字として認識されるので変数x, yには入らない。

std::cinでは文字列のほかにも整数や浮動小数点数、boolを入力として得ることができる。

int main()
{
    // 整数
    int i{} ;
    std::cin >> i ;
    // 浮動小数点数
    double d{} ;
    std::cin >> d ;
}

実行結果、

$ make run
123 1.23

数値はデフォルトで10進数として扱われる。

boolの入力には注意が必要だ。普通に書くと、ゼロがfalse, 非ゼロがtrueとして扱われる。

int main()
{
    bool b{} ;
    std::cin >> b ;

    std::cout << std::boolalpha << b << "\n"s ;
}

実行結果、

$ make run
1
true
$ make run
0
false
$ make run
123
true
$ make run
-1
true

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

int main()
{
    // bool型
    bool b{} ;
    std::cin >> std::boolalpha >> b ;

    std::cout << std::boolalpha << b ;
}

実行結果

$ make run
true
true
$ make run
false
false

std::boolalphaを入出力するというのは、実際には何も入出力しないので奇妙に見えるが、そういう設計になっているので仕方がない。

では標準入力を学んだので、さっそくBMIを計算するプログラムを標準入力に対応させよう。

int main()
{
    // 身長の入力
    double height{} ;
    std::cout << "height(m)>" ;
    std::cin >> height ;

    // 体重の入力
    double mass{} ;
    std::cout << "mass(kg)>" ;
    std::cin >> mass ;

    double bmi = mass / (height*height) ;

    std::cout << "BMI=" << bmi << "\n"s ;
}

上出来だ。

リダイレクト

標準入出力が扱えるようになれば、もう自分の好きなプログラムを書くことができる。プログラムというのはけっきょく、入力を得て、処理して、出力するだけのものだからだ。入力はテキストだったりグラフィックだったり何らかの特殊なデバイスだったりするが、基本は変わらない。

たとえば読者はまだC++でファイルを読み書きする方法を知らないが、標準入出力さえ使えれば、ファイルの読み書きはリダイレクトを使うだけでできるのだ。

int main()
{
    std::cout << "hello" ;
}

これは"hello"と標準出力するだけの簡単なプログラムだ。このプログラムをコンパイルしたプログラム名をprogramとしよう。標準出力の出力先はデフォルトで、ユーザーのターミナルになる。

$ ./program
hello

リダイレクトを使えば、この出力先をファイルにできる。リダイレクトを使うには"プログラム \> ファイル名"とする。

$ ./program > hello.txt
$ cat hello.txt
hello

ファイルへの簡単な書き込みは、リダイレクトを使うことであとから簡単に実現可能だ。

リダイレクトはファイルの読み込みにも使える。例えば先ほどのBMIを計算するプログラムを用意しよう。

// bmi
int main()
{
    double height{ } ;
    double mass { } ;

    std::cin >> height >> mass ;

    std::cout << mass / (height*height) ;
}

このプログラム名をbmiとして、通常どおり実行すると以下のようになる。

$ ./bmi
1.63
73
27.4756

このうち、1.6373はユーザーによる入力だ。これを毎回手で入力するのではなく、ファイルから入力することができる。つまり以下のようなファイルを用意して、

1.63
73

このファイルを例えば、"bodymass.txt"とする。手で入力する代わりに、このファイルを入力として使いたい。これにはリダイレクトとして"プログラム名 \< ファイル名"とする。

$ ./bmi < bodymass.txt
27.4756

リダイレクトの入出力を組み合わせることも可能だ。

$ cat bodymass.txt
1.63
73
$ ./bmi < bodymass.txt > index.txt
$ cat index.txt
27.4756

もちろん、このようなファイルの読み書きは簡易的なものだが、かなりの処理がこの程度のファイル操作でも行えるのだ。

パイプ

プログラムが出力した結果をさらに入力にすることだってできる。

例えば、先ほどのプログラムbmiに入力するファイルbodymass.txtの身長の単位がメートルではなくセンチメートルだったとしよう。

163
73

この場合、プログラムbmiを書き換えて対処することもできるが、プログラムに入力させる前にファイルを読み込み、書き換えて出力し、その出力を入力とすることもできる。

まず、身長の単位をセンチメートルからメートルに直すプログラムを書く。

// convert
int main()
{
    double height{} ;
    double mass{} ;

    std::cin >> height >> mass ;

    // 身長をセンチメートルからメートルに直す
    // 体重はそのままでよい
    std::cout << height/100.0 << "\n"s << mass ;
}

このプログラムをconvertと名付け、さっそく使ってみよう。

$ ./convert
163
73
1.63
73

身長の単位がセンチメートルからメートルに正しく直されている。

これをリダイレクトで使うとこうなる。

$ ./convert < bodymass.txt > fixed_bodymass.txt
$ ./bmi < fixed_bodymass.txt
27.4756

しかしこれではファイルが増えて面倒だ。この場合、パイプを使うとスッキリと書ける。

パイプはプログラムの標準出力をプログラムの標準入力とするの使い方は、"プログラム名 | プログラム名"だ。

$ ./convert < bodymass.txt | ./bmi
27.4756

ところで、すでに何度か説明なしで使っているが、POSIX規格を満たすOSにはcatというプログラムが標準で入っている。cat ファイル名は指定したファイル名の内容を標準出力する。標準出力はパイプで標準入力にできる。

$ cat bodymass.txt | ./convert | ./bmi
27.4756

プログラムの組み合わせ

現代のプログラミングというのは、すでに存在するプログラムを組み合わせて作るものだ。もし、自分の必要とする処理がすでに実装されているのであれば、自分で書く必要はない。

例えば、読者はまだカレントディレクトリー下のファイルの一覧を列挙する方法を知らない。しかしPOSIX規格を満たすOSにはlsというカレントディレクトリー下のファイルの一覧を列挙するプログラムが存在する。これを先ほどまでBMIの計算などの作業をしていたディレクトリー下で実行してみよう。

$ ls
all.h  all.h.gch  bmi  bodymass.txt  convert  data  main.cpp  Makefile  program

ファイルの一覧が列挙される。そしてこれはプログラムlsによる標準出力だ。標準出力ということは、リダイレクトしてファイルに書き込んだり、パイプで別のプログラムに渡したりできるということだ。

$ ls > files.txt
$ ls | ./program

標準入出力が扱えれば、ネットワークごしにWebサイトをダウンロードすることもできる。これにはほとんどのGNU/LinuxベースのOSに入っているcurlというプログラムを使う。

$ curl https://example.com

プログラムcurlは指定されたURLからデータをダウンロードして、標準出力する。標準出力するということは、パイプによって標準入力にできるということだ。

$ curl https://example.com | ./program

読者はC++でネットワークアクセスする方法を知らないが、すでにネットワークアクセスは可能になった。

ほかにも便利なプログラムはたくさんある。プログラミングの学び始めはできることが少なくて退屈になりがちだが、読者はもうファイルの読み書きやネットワークアクセスまでできるようになったのだから、退屈はしないはずだ。

ループ

さて、ここまでで変数や関数、標準入出力といったプログラミングの基礎的な概念を教えてきた。あと1つでプログラミングに必要な基礎的な概念はすべて説明し終わる。ループだ。

これまでのおさらい

C++では、プログラムは書いた順番に実行される。これを逐次実行という。

int main()
{
    std::cout << 1 ;
    std::cout << 2 ;
    std::cout << 3 ;
}

実行結果、

123

この実行結果が"123"以外の結果になることはない。C++ではプログラムは書かれた順番に実行されるからだ。

条件分岐は、プログラムの実行を条件付きで行うことができる。

int main()
{
    std::cout << 1 ;

    if ( false )
        std::cout << 2 ;

    std::cout << 3 ;

    if ( true )
        std::cout << 4 ;
    else
        std::cout << 5 ;
}

実行結果、

134

条件分岐によって、プログラムの一部を実行しないということが可能になる。

goto文

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

無限ループ

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

// 1回"hello\n"を出力する関数
void hello()
{
    std::cout << "hello\n"s ;
}

int main()
{
    hello() ;
    hello() ;
    hello() ;
}

10回出力する場合はどうするのだろう。10回書けばよい。コードは省略する。

では100回出力する場合はどうするのだろう。100回書くのだろうか。100回も同じコードを書くのはとても面倒だ。読者がVimのような優秀なテキストエディターを使っていない限り100回も同じコードを間違えずに書くことは不可能だろう。Vimならば1回書いたあとにノーマルモードで"100."するだけで100回書ける。

実際のところ、100回だろうが、1000回だろうが、あらかじめ回数がコンパイル時に決まっているのであれば、その回数だけ同じ処理を書くことで実現可能だ。

しかし、プログラムを外部から強制的に停止させるまで、無限に出力し続けるプログラムはどう書けばいいのだろうか。そういった停止しないプログラムを外部から強制的に停止させるにはCtrl-Cを使う。

以下はそのようなプログラムの実行例だ。

$ make run
hello
hello
hello
hello
...
[Ctrl-Cを押す]

goto文は指定したラベルに実行を移す機能だ。

ラベル名 : 文

goto ラベル名 ;
int main()
{
    std::cout << 1 ;

    // ラベルskipまで飛ぶ
    goto skip ;

    std::cout << 2 ;

// ラベルskip
skip :
    std::cout << 3 ;
}

これを実行すると以下のようになる。

13

2を出力すべき文の実行が飛ばされていることがわかる。

これだけだと"if (false)"と同じように見えるが、goto文はソースコードの上に飛ぶこともできるのだ。

void hello()
{
    std::cout << "hello\n"s ;
}
int main()
{
loop :
    hello() ;
    goto loop ; 
}

これは"hello\n"を無限に出力するプログラムだ。

このプログラムを実行すると、

  1. 関数helloが呼ばれる
  2. goto文でラベルloopまで飛ぶ
  3. 1.に戻る

という処理を行う。

終了条件付きループ

ひたすら同じ文字列を出力し続けるだけのプログラムというのも味気ない。もっと面白くてためになるプログラムを作ろう。例えば、ユーザーから入力された数値を合計し続けるプログラムはどうだろう。

いまから作るプログラムを実行すると以下のようになる。

$ make run
> 10
10
> 5
15
> 999
1014
> -234
780

このプログラムは、

  1. "\>"と表示してユーザーから整数値を入力
  2. これまでの入力との合計値を出力
  3. 1.に戻る

という動作を繰り返す。先ほど学んだ無限ループと同じだ。

さっそく作っていこう。

int input()
{
    std::cout << ">"s ;
    int x {} ;
    std::cin >> x ;
    return x ;
}

int main()
{
    int sum = 0 ;
loop :
    sum = sum + input() ;
    std::cout << sum << "\n"s ;
    goto loop ;
}

関数input"\>"を表示してユーザーからの入力を得て戻り値として返すだけの関数だ。

"sum = sum + input()"は、変数sumに新しい値を代入するもので、その代入する値というのは、代入する前の変数sumの値と関数inputの戻り値を足した値だ。

このような変数xに何らかの値nを足した結果を元の変数xに代入するという処理はとても多く使われるので、C++では"x = x + n"を意味する省略記法"x += n"がある。

int main()
{
    int x = 1 ;
    int n = 5 ;

    x = x + n ; // 6
    x += n ; // 11
}

さて、本題に戻ろう。上のプログラムは動く。しかし、プログラムを停止するにはCtrl-Cを押すしかない。できればプログラム自ら終了してもらいたいものだ。

そこで、ユーザーが0を入力したときはプログラムを終了するようにしよう。

int input()
{
    std::cout << ">"s ;
    int x {} ;
    std::cin >> x ;
    return x ;
}

int main()
{
    int sum = 0 ;
loop :
    // 一度入力を変数に代入
    int x = input() ;
    // 変数xが0でない場合
    if ( x != 0 )
    {// 実行
        sum = sum + x ;
        std::cout << sum << "\n"s ;
        goto loop ;
    }
    // x == 0の場合、ここに実行が移る
    // main関数の最後なのでプログラムが終了
}

うまくいった。このループは、ユーザーが0を入力した場合に繰り返しを終了する、条件付きのループだ。

インデックスループ

最後に紹介するループは、インデックスループだ。\(n\)"hello\n"sを出力するプログラムを書こう。問題は、この\(n\)はコンパイル時には与えられず、実行時にユーザーからの入力で与えられる。

// n回出力する関数の宣言
void hello_n( int n ) ;

int main()
{
    // ユーザーからの入力
    int n {} ;
    std::cin >> n ;
    // n回出力
    hello_n( n ) ;
}

このコードをコンパイルしようとするとエラーになる。これは実はコンパイルエラーではなくてリンクエラーという種類のエラーだ。その理由は、関数hello_nに対する関数の定義が存在しないからだ。

関数というのは宣言と定義に分かれている。

// 関数の宣言
void f( ) ;

// 宣言
void f( )
// 定義
{ }

関数の宣言というのは何度書いても大丈夫だ。

// 宣言
int f( int x ) ;

// 再宣言
int f( int x ) ;

// 再宣言
int f( int x ) ;

関数の宣言というのは戻り値の型や関数名や引数リストだけで、";"で終わる。

関数の定義とは、関数の宣言のあとの"{}"だ。この場合、宣言のあとに";"は書かない。

int f( int x ) { return x ; }

関数の定義は一度しか書けない。

// 定義
void f() {}
// エラー、再定義
void f() {}

なぜ関数は宣言と定義とに分かれているかというと、C++では名前は宣言しないと使えないためだ。

int main()
{
    // エラー
    // 名前fは宣言されていない
    f() ;
}

// 定義
void f() { }

なので、必ず名前は使う前に宣言しなければならない。

// 名前fの宣言
void f() ;

int main()
{
    // OK、名前fは関数
    f() ;
}

// 名前fの定義
void f() { }

さて、話を元に戻そう。これから学ぶのは\(n\)"hello\n"sと出力するプログラムの書き方だ。ただし\(n\)はユーザーが入力するので実行時にしかわからない。すでに我々はユーザーから\(n\)の入力を受け取る部分のプログラムは書いた。

// n回出力する関数の宣言
void hello_n( int n ) ;

int main()
{
    // ユーザーからの入力
    int n {} ;
    std::cin >> n ;
    // n回出力
    hello_n( n ) ;
}

あとは関数hello_n(n)\(n\)"hello\n"sと出力するようなループを実行すればいいのだ。

すでに我々は無限回"hello\n"sと出力する方法を知っている。まずは無限回ループを書こう。

void hello_n( int n )
{
loop :
    std::cout << "hello\n"s ;
    goto loop ;
}

終了条件付きループで学んだように、このループを\(n\)回繰り返した場合に終了させるには、if文を使って、終了条件に達したかどうかで実行を分岐させればよい。

void hello_n( int n )
{
loop :
    // まだn回繰り返していない場合
    if ( ??? )
    { // 以下を実行
        std::cout << "hello\n"s ;
        goto loop ;
    }
}

このコードを完成させるにはどうすればいいのか。まず、現在何回繰り返しを行ったのか記録する必要がある。このために変数を作る。

int i = 0 ;

変数iの初期値は0だ。まだ繰り返し実行を1回も行っていないということは、つまり0回繰り返し実行をしたということだ。

1回繰り返し実行をするたびに、変数iの値を1増やす。

i = i + 1 ;

これはすでに学んだように、もっと簡単に書ける。

i += 1 ;

実は、さらに簡単に書くこともできる。変数の代入前の値に1を足した値を代入する、つまり変数の値を1増やすというのはとてもよく書くコードなので、とても簡単な演算子が用意されている。operator ++だ。

int main()
{
    int i = 0 ;
    ++i ; // 1
    ++i ; // 2
    ++i ; // 3
}

これで変数iの値は1増える。これをインクリメント(increment)という。

インクリメントと対になるのがデクリメント(decrement)だ。これは変数の値を1減らす。演算子はoperator --だ。

int main()
{
    int i = 0 ;
    --i ; // -1
    --i ; // -2
    --i ; // -3
}

さて、必要な知識は学び終えたので本題に戻ろう。\(n\)回の繰り返しをしたあとにループを終了するには、まずいま何回繰り返し実行しているのかを記録する必要がある。その方法を学ぶために、0, 1, 2, 3, 4…と無限に出力されるプログラムを書いてみよう。

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

$ make run
1, 2, 3, 4, 5, 6, [Ctrl-C]

Ctrl-Cを押すまでプログラムは無限に実行される。

ではどうやって書くのか。以下のようにする。

  1. 変数iを作り、値を0にする
  2. 変数i", "sを出力する
  3. 変数iをインクリメントする
  4. goto 2.

この処理を素直に書くと以下のコードになる。

int main()
{
    // 1. 変数iを作り、値を0にする
    int i = 0 ;
loop :
    // 2. 変数iと", "sを出力する
    std::cout << i << ", "s ;
    // 3. 変数iをインクリメントする
    ++i ;
    // 4. goto 2
    goto loop ;
}

どうやら、いま何回繰り返し実行しているか記録することはできるようになったようだ。

ここまでくればしめたもの。あとはgoto文を実行するかどうかをif文で条件分岐すればよい。しかし、if文の中にどんな条件を書けばいいのだろうか。

void hello_n( int n )
{
    int i = 0 ;
loop :
    // まだn回繰り返し実行をしていなければ実行
    if ( ??? )
    {
        std::cout << "hello\n"s ;
        ++i ;
        goto loop ;
    }
}

具体的に考えてみよう。n == 3のとき、つまり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"だ。

void hello_n( int n )
{
    int i = 0 ;
loop :
    if ( i != n )
    {
        std::cout << "hello\n"s ;
        ++i ;
        goto loop ;
    }
}

さっそく実行してみよう。

$ make run
3
hello
hello
hello
$ make run
2
hello
hello

なるほど、動くようだ。しかしこのプログラムにはバグがある。-1を入力すると、なぜか大量のhelloが出力されてしまうのだ。

$ make run
-1
hello
hello
hello
hello
[Ctrl-C]

この原因はまだ現時点の読者には難しい。この謎はいずれ明らかにするとして、いまはnが負数の場合にプログラムを0回の繰り返し分の実行で終了するように書き換えよう。

void hello_n( int n )
{
    // nが負数ならば
    if ( n < 0 )
        // 関数の実行を終了
        return ;

    int i = 0 ;
loop :
    if ( i != n )
    {
        std::cout << "hello\n"s ;
        ++i ;
        goto loop ;
    }
}

while文

goto文は極めて原始的で使いづらい機能だ。現実のC++プログラムではgoto文はめったに使われない。もっと簡単な機能を使う。ではなぜgoto文が存在するかというと、goto文は最も原始的で基礎的で、ほかの繰り返し機能はif文goto文に変換することで実現できるからだ。

goto文より簡単な繰り返し文に、while文がある。ここではgoto文while文を比較することで、while文を学んでいこう。

無限ループ

無限ループをgoto文で書く方法を思い出してみよう。

int main()
{
    auto hello = []()
    { std::cout << "hello\n"s ; } ;

loop :
    // 繰り返し実行される文
    hello() ;
    goto loop ;
}

このコードで本当に重要なのは関数helloを呼び出している部分だ。ここが繰り返し実行される文で、ラベル文goto文は、繰り返し実行を実現するために必要な記述でしかない。

そこでwhile(true)だ。while(true)goto文ラベル文よりも簡単に無限ループを実現できる。

while (true) 文

while文は文を無限に繰り返して実行してくれる。試してみよう。

int main()
{
    auto hello = []()
    { std::cout << "hello\n"s ; } ;

    while (true)
        hello() ;
}

このコードの重要な部分は以下の2行。

while (true)
    hello() ;

これをgoto文ラベル文を使った無限ループと比べてみよう。

loop:
    hello() ;
    goto loop ;

どちらも同じ意味のコードだが、while文の方が明らかに書きやすくなっているのがわかる。

goto文で学んだ、ユーザーからの整数値の入力の合計の計算を繰り返すプログラムをwhile(true)で書いてみよう。

int input()
{
    std::cout << ">"s ;
    int x {} ;
    std::cin >> x ;
    return x ;
}

int main()
{
    int sum = 0 ;

    while( true )
    {
        sum += input() ;
        std::cout << sum << "\n"s ;
    }
}

重要なのは以下の5行だ。

while( true )
{
    sum += input() ;
    std::cout << sum << "\n"s ;
}

これをgoto文で書いた場合と比べてみよう。

loop :
    sum += input() ;
    std::cout << sum << "\n"s ;
    goto loop ;

本当に重要で本質的な、繰り返し実行をする部分の2行のコードはまったく変わっていない。それでいてwhile(true)の方が圧倒的に簡単に書ける。

終了条件付きループ

なるほど、無限ループを書くのに、goto文を使うよりwhile(true)を使った方がいいことがわかった。ではほかのループの場合でも、while文の方が使いやすいだろうか。

本書を先頭から読んでいる優秀な読者はwhile(true)truebool型の値であることに気が付いているだろう。実はwhile(E)の括弧の中Eは、if(E)と書くのとまったく同じ条件なのだ。条件trueであれば繰り返し実行される。falseなら繰り返し実行されない。

while ( 条件 ) 文
int main()
{
    // 実行されない
    while ( false )
        std::cout << "No"s ;

    // 実行されない
    while ( 1 > 2 )
        std::cout << "No"s ;

    // 実行される
    // 無限ループ
    while ( 1 < 2 )
        std::cout << "Yes"s ;
}

while文を使って、0が入力されたら終了する合計値計算プログラムを書いてみよう。

int input()
{
    std::cout << ">"s ;
    int x {} ;
    std::cin >> x ;
    return x ;
}

int main()
{
    int sum = 0 ;
    int x {} ;

    while( ( x = input() ) != 0 )
    {
        sum += x ;
        std::cout << sum << "\n"s ;
    }
}

重要なのはこの5行。

while( ( x = input() ) != 0 )
{
    sum += x ;
    std::cout << sum << "\n"s ;
}

ここではちょっと難しいコードが出てくる。whileの中の条件が、"( x = input() ) != 0"になっている。これはどういうことか。

実は条件bool型に変換さえできればどんな式でも書ける。

int main()
{
    int x { } ;

    if ( (x = 1) == 1 )
        std::cout << "(x = 1) is 1.\n"s ;
}

このコードでは、“(x=1)”と“1”が等しい“==”かどうかを判断している。“(x=1)”という式は変数x1を代入する式だ。代入式の値は、代入された変数の値になる。この場合変数xの値だ。変数xには1が代入されているので、その値は1、つまり“(x=1) == 1”は“1 == 1”と書くのと同じ意味になる。この結果はtrueだ。

さて、このことを踏まえて、“( x = input() ) != 0”を考えてみよう。

( x = input() )”は変数xに関数inputを呼び出した結果を代入している。関数inputはユーザーから入力を得て、その入力をそのまま返す。つまり変数xにはユーザーの入力した値が代入される。その結果が0と等しくない“!=”かどうかを判断している。つまり、ユーザーが0を入力した場合はfalse、非ゼロを入力した場合はtrueとなる。

while(条件)条件trueとなる場合に繰り返し実行をする。結果として、ユーザーが0を入力するまで繰り返し実行をするコードになる。

goto文を使った終了条件付きループと比較してみよう。

loop:
    if ( (x = input() ) != 0 )
    {
        sum += x ;
        std::cout << sum << "\n"s ;
        goto loop ;
    }

while文の方が圧倒的に書きやすいことがわかる。

インデックスループ

\(n\)"hello\n"sと出力するプログラムをwhile文で書いてみよう。ただし\(n\)はユーザーが入力するものとする。

まずはgoto文でも使ったループ以外の処理をするコードから。

void hello_n( int n ) ;

int main()
{
    int n {} ;
    std::cin >> n ;
    hello_n( n ) ;
}

あとは関数hello_n(n)がインデックスループを実装するだけだ。ただしnが負数ならば何も実行しないようにしよう。

goto文でインデックスループを書くときに学んだように、

  1. n < 0ならば関数を終了
  2. 変数iを作り値を0にする
  3. i != nならば繰り返し実行
  4. 出力
  5. ++i
  6. goto 3.

while文で書くだけだ。

void hello_n( int n )
{
    // 1. n < 0ならば関数を終了
    if ( n < 0 )
        return ;

    // 2. 変数iを作り値を0にする
    int i = 0 ;

    // 3. i != nならば繰り返し実行
    while( i != n )
    {   // 4. 出力
        std::cout << "hello\n"s ;
        // 5. ++i
        ++i ;
    } // 6. goto 3
}

重要な部分だけ抜き出すと以下のとおり。

while( i != n )
{
    std::cout << "hello\n"s ;
    ++i ;
}

goto文を使ったインデックスループと比較してみよう。

loop :
    if ( i != n )
    {
        std::cout << "hello\n"s ;
        ++i ;
        goto loop ;
    }

読者の中にはあまり変わらないのではないかと思う人もいるかもしれない。しかし、次の問題を解くプログラムを書くと、while文がいかに楽に書けるかを実感するだろう。

問題:以下のような九九の表を出力するプログラムを書きなさい。

1   2   3   4   5   6   7   8   9   
2   4   6   8   10  12  14  16  18  
3   6   9   12  15  18  21  24  27  
4   8   12  16  20  24  28  32  36  
5   10  15  20  25  30  35  40  45  
6   12  18  24  30  36  42  48  54  
7   14  21  28  35  42  49  56  63  
8   16  24  32  40  48  56  64  72  
9   18  27  36  45  54  63  72  81

もちろん、このような文字列を愚直に出力しろという問題ではない。

int main()
{
    // 違う!
    std::cout << "1 2 3 4 5..."s ;
}

逐次実行、条件分岐、ループまでを習得した誇りある本物のプログラマーである我々は、もちろん九九の表はループを書いて出力する。

まず出力すべき表を見ると、数値が左揃えになっていることに気が付くだろう。

4   8   12
5   10  15

8は1文字、10は2文字にもかかわらず、1215は同じ列目から始まっている。これは出力するスペース文字を調整することでも実現できるが、ここでは単にタブ文字を使っている。

タブ文字はMakefileを書くのにも使った文字で、C++の文字列中に直接書くこともできるが、エスケープ文字\tを使ってもよい。

int main()
{
    std::cout << "4\t8\t12\n5\t10\t15"s ;
}

エスケープ文字\nが改行文字に置き換わるように、エスケープ文字\tはタブ文字に置き換わる。

九九の表はどうやって出力すればよいだろうか。計算自体はC++では"a*b"でできる。上の表がどのように計算されているかを考えてみよう。

1*1 1*2 1*3 1*4 1*5 1*6 1*7 1*8 1*9 
2*1 2*2 2*3 2*4 2*5 2*6 2*7 2*8 2*9 
3*1 3*2 3*3 3*4 3*5 3*6 3*7 3*8 3*9 
4*1 4*2 4*3 4*4 4*5 4*6 4*7 4*8 4*9 
5*1 5*2 5*3 5*4 5*5 5*6 5*7 5*8 5*9 
6*1 6*2 6*3 6*4 6*5 6*6 6*7 6*8 6*9 
7*1 7*2 7*3 7*4 7*5 7*6 7*7 7*8 7*9 
8*1 8*2 8*3 8*4 8*5 8*6 8*7 8*8 8*9 
9*1 9*2 9*3 9*4 9*5 9*6 9*7 9*8 9*9

これを見ると、"a*b"のうちのa1から9までインクリメントし、それに対してb1から9までインクリメントさせればよい。つまり、9回のインデックスループの中で9回のインデックスループを実行することになる。ループの中のループだ。

while ( 条件 )
    while ( 条件 )

さっそくそのようなコードを書いてみよう。

int main()
{
    // 1から9まで
    int a = 1 ;
    while ( a <= 9 )
    {
        // 1から9まで
        int b = 1 ;
        while ( b <= 9 )
        {
            // 計算結果を出力
            std::cout << a * b << "\t"s ;
            ++b ;
        }
        // 段の終わりに改行
        std::cout << "\n"s ;
        ++a ;
    }
}

うまくいった。

ところで、このコードをgoto文で書くとどうなるだろうか。

int main()
{
    int a = 1 ;
loop_outer :
    if ( a <= 9 )
    {
        int b = 1 ;
loop_inner :
        if ( b <= 9 )
        {
            std::cout << a * b << "\t"s ;
            ++b ;
            goto loop_inner ;
        }
        std::cout << "\n"s ;
        ++a ;
        goto loop_outer ;
    }
}

とてつもなく読みにくい。

for文

ところでいままでwhile文で書いてきたインデックスループには特徴がある。

試しに1から100までの整数を出力するコードを見てみよう。

int main()
{
    int i = 1 ;
    while ( i <= 100 )
    {
        std::cout << i << " "s ;
        ++i ;
    }
}

このコードを読むと、以下のようなパターンがあることがわかる。

int main()
{
    // ループ実行前の変数の宣言と初期化
    int i = 1 ;
    // ループ中の終了条件の確認
    while ( i <= 100 )
    {
        // 実際に繰り返したい文
        std::cout << i << " "s ;
        // 各ループの最後に必ず行う処理
        ++i ;
    }
}

ここで真に必要なのは、「実際に繰り返したい文」だ。その他の処理は、ループを実現するために必要なコードだ。ループの実現に必要な処理が飛び飛びの場所にあるのは、はなはだわかりにくい。

for文はそのような問題を解決するための機能だ。

for ( 変数の宣言 ; 終了条件の確認 ; 各ループの最後に必ず行う処理 ) 文

for文を使うと、上のコードは以下のように書ける。

int main()
{
    for ( int i = 1 ; i <= 100 ; ++i )
    {
        std::cout << i << " "s ;
    } 
}

ループの実現に必要な部分だけ抜き出すと以下のようになる。

// for文の開始
for (
// 変数の宣言と初期化
int i = 1 ;
// 終了条件の確認
i <= 100 ;
// 各ループの最後に必ず行う処理
++i )

for文はインデックスループによくあるパターンをわかりやすく書くための機能だ。例えばwhile文のときに書いた九九の表を出力するプログラムは、for文ならばこんなに簡潔に書ける。

int main()
{
    for ( int a = 1 ; a <= 9 ; ++a )
    {
        for ( int b = 1 ; b <= 9 ; ++b )
        { std::cout << a*b << "\t"s ; }

        std::cout << "\n"s ;
    }
}

while文を使ったコードと比べてみよう。

int main()
{
    int a = 1 ;
    while ( a <= 9 )
    {
        int b = 1 ;
        while ( b <= 9 )
        {
            std::cout << a * b << "\t"s ;
            ++b ;
        }
        std::cout << "\n"s ;
        ++a ;
    }
}

格段に読みやすくなっていることがわかる。

C++ではカンマ','を使うことで、複数のを1つのに書くことができる。

int main()
{
    int a = 0, b = 0 ;
    ++a, ++b ;
}

for文でもカンマが使える。九九の表を出力するプログラムは、以下のように書くこともできる。

int main()
{
    for ( int a = 1 ; a <= 9 ; ++a, std::cout << "\n"s )
        for ( int b = 1 ; b <= 9 ; ++b )
            std::cout << a*b << "\t"s ;
}

変数もカンマで複数宣言できると知った読者は、以下のように書きたくなるだろう。

int main()
{
    for (   int a = 1, b = 1 ;
            a <= 9 ;
            ++a, ++b,
            std::cout << "\n"s
        )
            std::cout << a*b << "\t"s ;
}

これは動かない。なぜならば、for文を2つネストさせたループは、\(a \times b\)回のループで、変数a1から9まで変化するそれぞれに対して、変数b1から9まで変化する。しかし、上のfor文1つのコードは、変数a, bともに同時に1から9まで変化する。したがって、これは単にa回のループでしかない。a回のループの中でb回のループをすることで\(a \times b\)回のループを実現できる。

for文では使わない部分を省略することができる。

int main()
{
    bool b = true ;
    // for文による変数宣言は使わない
    for ( ; b ; b = false )
        std::cout << "hello"s ;
}

for文で終了条件を省略した場合、trueと同じになる。

int main()
{
    for (;;)
        std::cout << "hello\n"s ;
}

このプログラムは"hello\n"sと無限に出力し続けるプログラムだ。"for(;;)""for(;true;)"と同じ意味であり、"while(true)"とも同じ意味だ。

do文

do文while文に似ている。

dowhile ( 条件 ) ;

比較のためにwhile文の文法も書いてみると以下のようになる。

while ( 条件 ) 文

while文はまず条件を確認しtrueの場合を実行する。これを繰り返す。

int main()
{
    while ( false )
    {
        std::cout << "hello\n"s ;
    }
}

do文はまずを実行する。しかる後に条件を確認しtrueの場合繰り返しを行う。

int main()
{
    do {
        std::cout << "hello\n"s ;
    } while ( false ) ;
}

違いがわかっただろうか。do文は繰り返し実行するを、条件がなんであれ、最初に一度実行する。

do文を使うと条件にかかわらず文を1回は実行するコードが、文の重複なく書けるようになる。

break文

ループの実行の途中で、ループの中から外に脱出したくなった場合、どうすればいいのだろうか。例えばループを実行中に何らかのエラーを検出したので処理を中止したい場合などだ。

while ( true )
{
    // 処理

    if ( is_error() )
        // エラーのため脱出したくなった

    // 処理
}

break文はループの途中から脱出するための文だ。

break ;

break文for文while文do文の中でしか使えない。

break文for文while文do文の外側に脱出する。

int main()
{
    while ( true )
    {
        // 処理

        break ;

        // 処理
    }
}

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

int main()
{
    while ( true )
    {
        // 処理

        goto break_while ;

        // 処理
    }
break_while : ;
}

break文は最も内側の繰り返し文から脱出する

int main()
{
    while ( true ) // 外側
    {
        while ( true ) // 内側
        {
            break ;
        }
        // ここに脱出
    }
}

continue文

ループの途中で、いまのループを打ち切って次のループに進みたい場合はどうすればいいのだろう。例えば、ループの途中でエラーを検出したので、そのループについては処理を打ち切りたい場合だ。

while ( true )
{
    // 処理

    if ( is_error() )
        // このループは打ち切りたい

    // 処理
}

continue文はループを打ち切って次のループに行くための文だ。

continue ;

continue文for文while文do文の中でしか使えない。

int main()
{
    while ( true )
    {
        // 処理

        continue ;

        // 処理
    }
}

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

int main()
{
    while ( true )
    {
        // 処理

        goto continue_while ;

        // 処理

continue_while : ;
    }
}

continue文はループの最後に処理を移す。その結果、次のループを実行するかどうかの条件を評価することになる。

continue文は最も内側のループに対応する。

int main()
{
    while ( true ) // 外側
    {
        while ( true ) // 内側
        {
            continue ;
            // continueはここに実行を移す
        }
    }
}

再帰関数

最後に関数でループを実装する方法を示してこの章を終わりにしよう。

関数は関数を呼び出すことができる。

void f() { }

void g()
{
    f() ; // 関数fの呼び出し
}

int main()
{
    g() ; // 関数gの呼び出し
}

ではもし、関数が自分自身を呼び出したらどうなるだろうか。

void hello()
{
    std::cout << "hello\n" ;
    hello() ;
}

int main()
{
    hello() ;
}
  1. 関数mainは関数helloを呼び出す
  2. 関数hello"hello\n"と出力して関数helloを呼び出す

関数helloは必ず関数helloを呼び出すので、この実行は無限ループする。

関数が自分自身を呼び出すことを、再帰(recursion)という。

なるほど、再帰によって無限ループを実現できることはわかった。では終了条件付きループは書けるだろうか。

関数はreturn文によって呼び出し元に戻る。単に'return ;'と書けば再帰はしない。そして、if文によって実行は分岐できる。これを使えば再帰で終了条件付きループが実現できる。

試しに、ユーザーが0を入力するまでループし続けるプログラムを書いてみよう。

// ユーザーからの入力を返す
int input ()
{
    int x { } ;
    std::cin >> x ;
    return x ;
}

// 0の入力を終了条件としたループ
void loop_until_zero()
{
    if ( input() == 0 )
        return ;
    else
        loop_until_zero() ;
}

int main()
{
    loop_until_zero() ;
}

書けた。

ではインデックスループはどうだろうか。1から10までの整数を出力してみよう。

インデックスループを実現するには、書き換えられる変数が必要だ。関数は引数で値を渡すことができる。

void g( int x ) { }
void f( int x ) { g( x+1 ) ; }

int main() { f( 0 ) ; }

これを見ると、関数mainは関数fに引数0を渡し、関数fは関数gに引数1を渡している。これをもっと再帰的に考えよう。

void until_ten( int x )
{
    if ( x > 10 )
        return ;
    else
    {
        std::cout << x << "\n" ;
        return until_ten( x + 1 ) ;
    }
}

int main()
{
    until_ten(1) ;
}

関数mainは関数until_tenに引数1を渡す。

関数until_tenは引数が10より大きければ何もせず処理を戻し、そうでなければ引数を出力して再帰する。そのとき引数は\(+1\)される。

これによりインデックスループが実現できる。

関数は戻り値を返すことができる。再帰で戻り値を使うことにより面白い問題も解くことができる。

例えば、10だけを使った10進数の整数を2進数に変換するプログラムを書いてみよう。

$ make run
> 0
0
> 1
1
> 10
2
> 11
3
> 1010
10
> 1111
15

まず10進数と2進数を確認しよう。数学的に言うと「10を底にする」とか「2を底にする」という言い方をする。

具体的な例を出すと10進数では1,2,3,4,5,6,7,8,9,0の文字を使う。1234は以下のようになる。

\[ 1234 = 1 \times 10^3 + 2 \times 10^2 + 3 \times 10^1 + 4 \times 10^0 = 1 \times 1000 + 2 \times 100 + 3 \times 10 + 4 \times 1 \]

10進数で1010は以下のようになる。

\[ 1010 = 1 \times 10^3 + 0 \times 10^2 + 1 \times 10^1 + 0 \times 10^0 = 1 \times 1000 + 0 \times 100 + 1 \times 10 + 0 \times 1 \]

2進数では1,0の文字を使う。1010は以下のようになる。

\[ 1010 = 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 = 1 \times 8 + 0 \times 4 + 1 \times 2 + 0 \times 1 \]

2進数の1010は10進数では10になる。

では問題を解いていこう。

問題を難しく考えるとかえって解けなくなる。ここではすでに10進数から2進数への変換は解決したものとして考えよう。関数convertによってその問題は解決した。

// 2進数への変換
int convert( int n ) ;

まだ我々は関数convertの中身を書いていないが、すでに書き終わったと仮定しよう。するとプログラムの残りの部分は以下のように書ける。

int convert( int n ) ;

// 入力
int input()
{
    std::cout << "> " ;
    int x{} ;
    std::cin >> x ;
    return x ;
}

// 出力
void output( int binary )
{
    std::cout << binary << "\n"s ;
}

int main()
{
    // 入力、変換、出力のループ
    while( true )
    {
        auto decimal = input() ;
        auto binary = convert( decimal ) ;
        output( binary ) ;
    } 
}

あとは関数convertを実装すればよいだけだ。

関数convertに引数を渡したときの結果を考えてみよう。convert(1010)10を返し、convert(1111)15を返す。

ではconvert(-1010)の結果はどうなるだろうか。これは-10になる。

負数と正数の違いを考えるのは面倒だ。ここでは正数を引数として与えると10進数から2進数へ変換した答えを返してくる魔法のような関数solveをすでに書き終えたと仮定しよう。我々はまだ関数solveを書いていないが、その問題は未来の自分に押し付けよう。

// 1,0のみを使った10進数から
// 2進数へ変換する関数
int solve( int n ) ;

すると、関数convertがやるのは負数と正数の処理だけでよい。

  1. 引数が正数の場合はそのまま関数solveに渡してreturn
  2. 引数が負数の場合は絶対値を関数solveに渡して負数にしてreturn
int convert( int n )
{
    // 引数が正数の場合
    if ( n > 0 )
        // そのまま関数solveに渡してreturn
        return solve( n ) ;
    else // 引数が負数の場合
        // 絶対値を関数solveに渡して負数にしてreturn
        return - solve( -n ) ;
}

nが負数の場合の絶対値は-nで得られる。その場合、関数solveの答えは正数なので負数にする。

あとは関数solveを実装するだけだ。

今回、引数の整数を10進数で表現した場合に2,3,4,5,6,7,8,9が使われている場合は考えないものとする。

// OK
solve(10111101) ;
// あり得ない
solve(2) ;

再帰で問題を解くには再帰的な考え方が必要だ。再帰的な考え方では、問題の一部のみを解き、残りは自分自身に丸投げする。

まずとても簡単な1桁の変換を考えよう。

solve(0) ; // 0
solve(1) ; // 1

引数が01の場合、単にその値を返すだけだ。関数solveには正数しか渡されないので、負数は考えなくてよい。すると、以下のように書ける。

int solve( int n )
{
    if ( n <= 1 )
        return n ;
    else
        // その他の場合
}

その他の場合とは、桁数が多い場合だ。

solve(10) ;  // 2
solve(11) ;  // 3
solve(110) ; // 4
solve(111) ; // 5

関数solveが解決するのは最下位桁だ。110の場合は0で、111の場合は1となる。最も右側の桁のみを扱う。数値から10進数で表記したときの最下位桁を取り出すには、10で割った余りが使える。覚えているだろうか。剰余演算子のoperator %を。

int solve( int n )
{
    if ( n <= 1 )
        return n ;
    else // 未完成
        return n%10 ;
}

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

solve(10) ;  // 0
solve(11) ;  // 1
solve(110) ; // 0
solve(111) ; // 1

これで関数solveは最下位桁に完全に対応した。しかしそれ以外の桁はどうすればいいのだろう。

ここで再帰的な考え方が必要だ。関数solveはすでに最下位桁に完全に対応している。ならば次の桁を最下位桁とした数値で関数solveを再帰的に呼び出せばいいのではないか。

以下はsolve(n)が再帰的に呼び出す関数だ。

solve(10) ;  // solve(1)
solve(11) ;  // solve(1)
solve(100) ; // solve(10)→solve(1)
solve(110) ; // solve(11)→solve(1)
solve(111) ; // solve(11)→solve(1)

10進数表記された数値から最下位桁を取り除いた数値にするというのは、11を1に, 111を11にする処理だ。これは数値を10で割ればよい。

10  / 10 ; // 1
11  / 10 ; // 1
100 / 10 ; // 10
110 / 10 ; // 11
111 / 10 ; // 11

10進数表記は桁が1つ上がると10倍される。だから10で割れば最下位桁が消える。ところで、我々は計算しようとしているのは2進数だ。2進数では桁が1つ上がると2倍される。なので、再帰的に関数solveを呼び出して得られた結果は2倍しなければならない。そして足し合わせる。

int solve( int n )
{
    // 1桁の場合
    if ( n <= 1 )
        return n ; // 単に返す
    else // それ以外
        return
            // 最下位桁の計算
            n%10
            // 残りの桁を丸投げする
            // 次の桁なので2倍する
            + 2 * solve( n/10 ) ;
}

冗長なコメントを除いて短くすると以下のとおり。

int solve( int n )
{
    if ( n <= 1 )
        return n ;
    else
        return n%10 + 2 * solve( n/10 ) ;
}

再帰ではないループで関数solveを実装するとどうなるのだろうか。

引数の数値が何桁あっても対応できるよう、ループで1桁ずつ処理していくのは変わらない。

もう一度2進数の計算を見てみよう。

\[ 1010 = 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 = 1 \times 8 + 0 \times 4 + 1 \times 2 + 0 \times 1 \]

1桁目は0で、この値は\(0 \times 2^0\)、2桁目は1で、この値は\(1 \times 2^1\)になる。

一般に、\(i\)桁目の値は\(i桁目の数字 \times 2^{i-1}\)になる。

すると解き方としては、各桁の値を計算した和を返せばよい

int solve( int n )
{
    // 和
    int result = 0 ;
    // i桁目の数字に乗ずる値
    int i = 1 ;

    // 桁がなくなれば終了
    while ( n != 0 )
    {
        // 現在の桁を計算して足す
        result += n%10 * i ;
        // 次の桁に乗ずる値
        i *= 2 ;
        // 桁を1つ減らす
        n /= 10 ;
    }

    return result ;
}

再帰を使うコードは、再帰を理解できれば短く簡潔でわかりやすい。ただし、再帰を理解するためにはまず再帰を理解しなければならない。

再帰は万能ではない。そもそも関数とは、別の関数から呼ばれるものだ。関数mainだけは特別で、関数mainを呼び出すことはできない。

int main()
{
    main() ; // エラー
}

関数の実行が終了した場合、呼び出し元に処理が戻る。そのために関数は呼び出し元を覚えていなければならない。これには通常スタックと呼ばれるメモリーを消費する。

void f() { }            // gに戻る
void g() { f() ; }      // mainに戻る 
int main() { g() ; }

関数の中の変数も通常スタックに確保される。これもメモリーを消費する。

void f() { }

void g()
{
    int x {} ;
    std::cin >> x ;
    f() ;   // 関数を呼び出す
    // 関数を呼び出したあとに変数を使う
    std::cout << x ;
}

このコードでは、関数gが変数xを用意し、関数fを呼び出し、処理が戻ったら変数xを使っている。このコードが動くためには、変数xは関数fが実行されている間もスタックメモリーを消費し続けなければならない。

スタックメモリーは有限であるので、以下のような再帰による無限ループは、いつかスタックメモリーを消費し尽して実行が止まるはずだ。

void hello()
{
    std::cout << "hello\n" ;
    hello() ;
}

int main() { hello() ; }

しかし、大半の読者の環境ではプログラムの実行が止まらないはずだ。これはコンパイラーの末尾再帰の最適化によるものだ。

末尾再帰とは、関数のすべての条件分岐の末尾が再帰で終わっている再帰のことだ。

例えば以下は階乗を計算する再帰で書かれたループだ。

int factorial( int n )
{
    if ( n < 1 )
        return 0 ;
    else if ( n == 1 )
        return 1 ;
    else
        return n * factorial(n-1) ;
}

factorial(n)\(1 \times 2 \times 3 \times ... \times n\)を計算する。

この関数は、引数n1未満であれば引数が間違っているので0を返す。そうでない場合でn1であれば1を返す。それ以外の場合、n * factorial(n-1)を返す。

このコードは末尾再帰になっている。末尾再帰は非再帰のループに機械的に変換できる特徴を持っている。例えば以下のように、

int factorial( int n )
{
    int temp = n ;

loop :
    if ( n < 1 )
        return 0 ;
    else if ( n == 1 )
        return temp * 1 ;
    else
    {
        n = n-1 ;
        temp *= n ;
        goto loop ;
    }
}

関数のすべての条件分岐の末尾が再帰になっているため、機械的に関数呼び出しをgoto文で置き換えることができる。

ただし、プログラミング言語C++の標準規格は、C++の実装に末尾再帰の最適化を義務付けてはいない。そのため、末尾再帰が最適化されるかどうかはC++コンパイラー次第だ。

再帰は強力なループの実現方法で、再帰的な問題を解くのに最適だが、落とし穴もある。

メモリーを無限に確保する

これまでのまとめ

ここまで読み進めてきた読者は、逐次実行、条件分岐、ループに加えて、変数と関数を理解した。これだけの要素を習得したならば、本質的にはプログラミングはほぼできるようになったと言ってよい。ただし、まだできないことがある。動的なメモリー確保だ。

標準入力から0が入力されるまで任意個の整数値を受け取り、小さい値から順に出力するプログラムを実装しよう。以下はそのようなプログラムの実行例だ。

$ make run
100
-100
1
6
3
999
-5000
0
-5000
-100
1
3
6
100
999

0が入力されるまで、1番目に、2番目に小さい値はわからない。そのため、この問題の解決には、入力をすべて保持しておく必要がある。

ここで必要なのは、値をいくらでも保持しておく方法と、値に順番があり、\(i\)番目の値を間接的に指定して読み書きできる方法だ。その方法としてC++には標準ライブラリstd::vectorがある。

vector

std::vector<T>T型の値をいくらでも保持できる。Tには保持する値の型を指定する。例えばintとかdoubleとかstd::stringだ。

int main()
{
    // 整数型intの値を保持するvector
    std::vector<int> vi ;

    // 浮動小数点数型doubleの値を保持するvector
    std::vector<double> vd ;

    // 文字列型std::stringの値を保持するvector
    std::vector<std::string> vs ;
}

std::vector<T>というのはそれ自体が型になっている。そしてTには型を指定する。ということは、vector型の値を保持するvectorも書けるということだ。

int main()
{
    // 整数型intを保持するvectorを保持するvector
    std::vector< std::vector< int > > vvi ;
}

もちろん、上のvectorを保持するvectorも書ける。その場合、std::vector<std::vector<std::vector<int>>>になる。このvectorを保持するvectorも当然書けるが省略する。

std::vector型の変数にはメンバー関数push_backを使うことで値を保持できる。

int main()
{
    std::vector<int> v  ;

    v.push_back(1) ;
    v.push_back(2) ;
    v.push_back(3) ;
}

メンバー関数(member function)というのは特別な関数で、詳細はまだ説明しない。ここで覚えておくべきこととしては、メンバー関数は一部の変数に使うことができること、メンバー関数fを変数xに使うには'x.f(...)'のように書くこと、を覚えておこう。

std::vectorはメモリーの続く限りいくらでも値を保持できる。試しに1000個の整数を保持させてみよう。

int main()
{
    std::vector<int> v ;

    for ( int i = 0 ; i != 1000 ; ++i )
    {
        v.push_back( i ) ;
    }
}

このプログラムは0から999までの1000個の整数をstd::vectorに保持させている。

std::vectorでは保持する値のことを要素という。要素は順番を持っている。メンバー関数push_backは最後の要素の次に要素を追加する。最初に要素はない。もしくは0個ある空の状態だと言ってもよい。

int main()
{
    std::vector<int> v ;

    // vは空

    // 要素数1、中身は{1}
    v.push_back(1) ;
    // 要素数2、中身は{1,2}
    v.push_back(2) ;
    // 要素数3、中身は{1,2,3}
    v.push_back(3) ;
}

std::vectorはメンバー関数size()で現在の要素数を取得できる。

int main()
{
    std::vector<int> v ;

    // 0
    std::cout << v.size() ;
    v.push_back(1) ;
    // 1
    std::cout << v.size() ;
    v.push_back(2) ;
    // 2
    std::cout << v.size() ;
}

せっかく値を入れたのだから取り出したいものだ。std::vectorではメンバー関数at(i)を使うことで、i番目の要素を取り出すことができる。このiのことを添字、インデックスと呼ぶ。ここで注意してほしいのは、最初の要素は0番目で、次の要素は1番目だということだ。最後の要素はsize()-1番目になる。

int main()
{

    std::vector<int> v ;

    for ( int i = 0 ; i != 10 ; ++i )
    {
        v.push_back(i) ;
    }

    // vの中身は{0,1,2,3,4,5,6,7,8,9}

    // 0, 0番目の最初の要素
    std::cout << v.at(0) ;
    // 4, 4番目の要素
    std::cout << v.at(4) ;
    // 9, 9番目の最後の要素
    std::cout << v.at(9) ;
}

この例ではループを使っている。読者はすでにループについては理解しているはずだ。上のコードが理解できないのであれば、もう一度ループの章に戻って学び直すべきだ。

もしat(i)に要素数を超えるiを渡してしまった場合どうなるのだろうか。

int main()
{
    std::vector<int> v { } ;
    v.push_back(0) ;
    // vには0番目の要素しかない
    // 1番目は誤り
    std::cout << v.at(1) ;
}

実行して確かめてみよう。

$ ./program
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 1) >= this->size() (which is 1)
Aborted (core dumped)

なにやら恐ろしげなメッセージが表示されるではないか。しかし心配することはない。このメッセージはむしろうれしいメッセージだ。変数vに1番目の要素がないことを発見してくれたという実行時のエラーメッセージだ。すでに学んだように、エラーメッセージは恐れるものではない。エラーメッセージはうれしいものだ。エラーメッセージが出たらありがとう。エラーメッセージがあるおかげでバグの存在がわかる。

このメッセージの本当の意味はいずれ例外やデバッガーを解説する章で説明するとして、vectorの要素数を超える指定をしてはいけないことを肝に銘じておこう。もちろん、-1もダメだ。

メンバー関数at(i)に与える引数iの型は整数型ではあるのだがint型ではない。std::size_t型という特殊な型になる。メンバー関数sizeも同様にstd::size_t型を返す。

int main()
{
    std::vector<int> v ;

    // std::size_t型
    std::size_t size = v.size() ;

    v.push_back(0) ;

    // std::size_t型
    std::size_t index = 0 ;
    v.at( index ) ;
}

なぜint型ではダメなのか。その謎は整数の章で明らかになる。ここではstd::size_t型は負数が使えない整数型だということだけ覚えておこう。std::size_t型に-1はない。vectorの要素指定では負数は使えないので、負数が使えない変数を使うのは理にかなっている。

さて、これまでに学んだ知識だけを使って、std::vectorのすべての要素を順番どおりに出力するコードが書けるはずだ。

int main()
{

    std::vector<int> v ;

    for ( int iota = 0 ; iota != 10 ; ++iota )
    {
        v.push_back(iota) ;
    }

    for ( std::size_t index = 0 ; index != v.size() ; ++index )
    {
        std::cout << v.at(index) << " "s ;
    }
}

このコードが書けるということは、もう標準入力から0が入力されるまで任意個の値を受け取り、入力された順番で出力するプログラムも書けるということだ。

int input()
{
    int x{} ;
    std::cin >> x ;
    return x ;
}
int main()
{
    std::vector<int> v ;
    int x { } ;

    // 入力
    while ( ( x = input() ) != 0 )
    {
        v.push_back( x ) ;
    }

    // 出力
    for ( std::size_t index = 0 ; index != v.size() ; ++index )
    {
        std::cout << v.at(index) << " "s ;
    }
}

入力された順番に出力できるということは、その逆順にも出力できるということだ。

for ( std::size_t index = v.size()-1 ; index != 0 ; --index )
{
    std::cout << v.at(index) << " "s ;
}

std::cout << v.at(0) ;

最後に'v.at(0)'を出力しているのは、ループが'i == 0'のときに終了してしまうからだ。つまり最後に出力すべきvector最初の要素である'v.at(0)'が出力されない。

std::size_t型は-1が使えないため、このようなコードになってしまう。int型を使えば負数は使えるのだが、int型とstd::size_t型の比較はさまざまな理由で問題がある。その理由は整数の章で深く学ぶことになるだろう。

ところで、問題は入力された整数を小さい順に出力することだった。この問題を考えるために、まずvectorの中に入っている要素から最も小さい整数の場所を探すプログラムを考えよう。

問題を考えるにあたって、いちいち標準入力から入力を取るのも面倒なので、あらかじめvectorに要素を入れておく方法を学ぶ。実は、vectorの要素は以下のように書けば指定することができる。

int main()
{
    // 要素{1, 2, 3}
    std::vector<int> v = { 1,2,3 } ;

    // 1
    auto x = v.at(0) ;
    // 2
    auto y = v.at(1) ;
    // 3
    auto z = v.at(2) ;
}

この例では、1, 2, 3の整数が書かれた順番であらかじめvectorの要素として入った状態になる。

さて、以下のような要素のvectorから最も小さい整数を探すプログラムを考えよう。

std::vector<int> v = { 8, 3, 7, 4, 2, 9, 3 } ;

これを見ると、最も小さい整数は4番目(最初の要素は0番目なので4番目)にある2だ。ではどうやって探すのだろうか。

解決方法としては先頭から末尾まで要素を1つずつ比較して、最も小さい要素を見つけ出す。まず0番目の8が最も小さいと仮定する。現在わかっている中で最も小さい要素のインデックスを記録するために変数minを作っておこう。

min = 0
8 3 7 4 2 9 3
^

次に1番目の3min番目を比較する。1番目の方が小さいので変数min1を代入する。

min = 1
8 3 7 4 2 9 3
  ^

2番目の7min番目を比較するとまだ1番目の方が小さい。3番目の4と比較してもまだmin番目の方が小さい。

4番目の2min番目を比較すると、4番目の方が小さい。変数min4を代入しよう。

min = 4
8 3 7 4 2 9 3
        ^

5番目と6番目もmin番目より大きいので、これで変数minに代入された4番目の要素が最も小さいことがわかる。

vectorの変数をv、要素数をsizeとする。変数minには現在わかっている中で最も小さい要素へのインデックスが代入される。

  1. 変数minに0を代入する
  2. size回のループを実行する
  3. 変数index0からsize-1までの整数を代入する
  4. 'v.at(index) < v.at(min)'ならばmin = index

さっそく書いてみよう。

int main()
{
    // vectorの変数をv
    std::vector<int> v = { 8, 3, 7, 4, 2, 9, 3 } ;
    // 要素数をsizeとする
    std::size_t size = v.size() ;

    // 変数minに0を代入する
    std::size_t min = 0 ;

    // size回のループを実行する
    // 変数iに0からsize-1までの整数を代入する
    for ( std::size_t index = 1 ; index != size ; ++index )
    {
        // 'v.at(index) < v.at(min)'ならばmin = index
        if ( v.at(index) < v.at(min) )
            min = index ;
    }

    // 一番小さい値を出力
    std::cout << v.at(min) ;
}

うまくいった。

ところで、最終的に解きたい問題とは、vectorのすべての要素を小さい順に出力するということだ。すると、もっと小さい要素を出力した次に、2番目に小さい要素、3番目に小さい要素・・・と出力していく必要がある。

2番目に小さい要素を見つけるためには、1番目に小さい要素を探さなければよい。そこで、発見した最も小さい要素と先頭の要素を交換してしまい、先頭は無視して最も小さい要素を探すことを繰り返すと実現できる。

例えば以下のような要素があるとして、

8 3 7 4 2 9 3
        ^

最も小さい要素である4番目の2と0番目の8を交換する。

2 3 7 4 8 9 3
^       ^
+-------+

そして、0番目は無視して最も小さい要素を探す。

3 7 4 8 9 3
^

この場合、最も小さいのは0番目と5番目の3だ。どちらも同じだが今回は0番目を選ぶ。もともと0番目にあるので0番目と0番目を交換した結果は変わらない。

そして、新しい0番目は無視して最も小さい要素を探す。

7 4 8 9 3
        ^

今度は4番目の3だ。これも先頭と交換する

3 4 8 9 7
^       ^
+-------+

これを繰り返していけば、小さい順に要素を探していくことができる。

この処理を行うコードを考えるために、先ほどと似たようなコードを見てみよう。

int main()
{
    std::vector<int> v = { 8, 3, 7, 4, 2, 9, 3 } ;
    std::size_t size = v.size() ;

    // この部分を繰り返す? 
    { // これ全体が1つのブロック文
        std::size_t min = 0 ;

        for ( std::size_t index = 1 ; index != size ; ++index )
        {
            if ( v.at(index) < v.at(min) )
                min = index ;
        }

        // 出力
        std::cout << v.at(min) << " "s ;

        // 先頭と交換
    }
}

このコードはそのまま使えない。今回考えた方法では、先頭が1つずつずれていく。そのために、最も小さい要素を探すループを、さらにループさせる。

// 現在の先頭
for ( std::size_t head = 0 ; head != size ; ++head )
{
    // 現在の先頭であるmin番目を仮の最小の要素とみなすのでhead
    std::size_t min = head ;    
    // 現在の先頭の次の要素から探すのでhead + 1
    for ( std::size_t index = head + 1 ; index != size ; ++index )
    {
        if ( v.at(index) < v.at(min) )
            min = index ;
    }

    std::cout << v.at(min) << " "s ;

    // 先頭と交換
}

次に先頭(0番目)と現在見つけた最小の要素(min番目)を交換する方法を考えよう。

vectorn番目の要素の値をxに変更するには、単にv.at(n) = xと書けばよい。

int main()
{
    std::vector<int> v = {1,2,3} ;

    v.at(0) = 4 ;
    // vは{4,2,3}
}

すると、vectori番目の要素にj番目の要素値を入れるには'v.at(i) = v.at(j)'と書く。

int main()
{
    std::vector<int> v = {1,2,3} ;
    v.at(0) = v.at(2) ;
    // vは{3,2,3}
}

変数とまったく同じだ。

しかし、変数aに変数bの値を代入すると、変数aの元の値は消えてしまう。

int main()
{
    int a = 1 ;
    int b = 2 ;

    // aの元の値は上書きされる
    a = b ;
    // a == 2
    b = a ;
    // b == 2
}

変数a, bの値を交換するためには、変数への代入の前に、別の変数に値を一時退避しておく必要がある。

int main()
{
    int a = 1 ;
    int b = 2 ;

    // 退避
    auto temp = a ;

    a = b ;
    b = temp ;

    // a == 2
    // b == 1
}

さて、これで問題を解く準備はすべて整った。

int main()
{
    std::vector<int> v = { 8, 3, 7, 4, 2, 9, 3 } ;
    std::size_t size = v.size() ;
   
    // 先頭をずらすループ 
    for ( std::size_t head = 0 ; head != size ; ++head )
    {
        std::size_t min = head ;
        // 現在の要素の範囲から最小値を見つけるループ
        for ( std::size_t index = head+1 ; index != size ; ++index )
        {
            if ( v.at(index) < v.at(min) )
                min = index ;
        }
        // 出力
        std::cout << v.at(min) << " "s ;

        // 最小値を先頭と交換
        auto temp = v.at(head) ;
        v.at(head) = v.at(min) ;
        v.at(min) = temp ;
    }

    // 実行したあと
}

ところで、このプログラムの「実行したあと」地点でのvectorの中身はどうなっているだろうか。

int main()
{
    std::vector<int> v = { 8,3,7,4,2,9,3 } ;

    // 上と同じコードなので省略

    // 実行したあと
    std::cout << "\n"s ;
    
    for ( std::size_t index = 0, size = v.size() ; index != size ; ++index )
    {
        std::cout << v.at(index) << " "s ;
    }
}

これを実行すると以下のようになる。

$ make run
2 3 3 4 7 8 9
2 3 3 4 7 8 9

なんとvectorの要素も小さい順に並んでいる。この状態のことを、ソートされているという。ループの中で最も小さい値を出力していく代わりに、まずソートして先頭から値を出力してもよいということだ。

ソートにはさまざまな方法があるが、今回使ったのは選択ソート(selection sort)というアルゴリズムだ。

vectorを使う方法には、イテレーターというもっと便利な方法があるが、それはイテレーターの章で説明する。

デバッグ:printfデバッグ

ループと多数の要素の集合を扱えるようになったので、読者はもう相当複雑な処理をするプログラムでも書けるようになった。処理が複雑になってくると増えるのがバグだ。

この章では、伝統あるprintfデバッグを紹介する。

printfデバッグとは、プログラムの実行中に知りたい情報を出力することだ。printfとはC言語の伝統ある出力用のライブラリに由来する名前だが、本書ではiostreamを使う。

実践例

例えば前章で実装したようにvectorの要素を選択ソートでソートしたいとする。

選択ソートとは、要素の集合の中から0番目に来るべき要素の場所を探し、0番目の要素と交換し、1番目に来るべき要素の場所を探し、1番目の要素と交換し・・・を要素の数だけ繰り返すことによって要素全体をソートする方法だ。

以下のように書いたとする。

int main()
{
    std::vector<int> v = { 3,8,2,5,6,9,4,1,7 } ;
    auto size = v.size() ;

    for ( std::size_t head = 0 ; head != size ; ++head )
    {
        auto min = head ;
        for ( std::size_t index = head+1 ; index != size ; ++index )
        {
            if ( v.at(index) < v.at(min) )
                min = index ;
        }
        
        auto temp = v.at(head) ;
        v.at(head) = v.at(min) ;
        v.at(min) = v.at(head) ;
    }

    for ( std::size_t i = 0 ; i != size ; ++i )
    {
        std::cout << v.at(i) << " "s ;
    }
}

さっそく実行してみよう。

$ make run
1 1 1 1 1 1 1 1 7

コンパイルはできるが、なぜかうまく動かない。コードのどこかが間違っているのはわかる。しかしどこが間違っているのかはわからない。さっそくprintfデバッグにより問題のある箇所を特定してみよう。

printfデバッグを行うには、まずコード中の間違っていそうな箇所にアタリをつける必要がある。

問題がどこにあるかわからないが、ループのどこかで間違っていそうだ。一番外側のループにアタリをつけよう。ループが実行されるごとに変数vの中身を表示してみる。

for ( std::size_t head = 0 ; head != size ; ++head )
{
    // printfデバッグ
        std::cout << "debug: head = "s << head << ", v = { "s;
        for ( std::size_t i = 0 ; i != v.size() ; ++i )
        {
            std::cout << v.at(i) << " "s ;
        }
        std::cout << "}\n"s ;
    // printfデバッグ

そして実行した結果が以下だ。

$ make run
debug: v = { 3, 8, 2, 5, 6, 9, 4, 1, 7, }
debug: v = { 1, 8, 2, 5, 6, 9, 4, 1, 7, }
debug: v = { 1, 1, 2, 5, 6, 9, 4, 1, 7, }
debug: v = { 1, 1, 1, 5, 6, 9, 4, 1, 7, }
debug: v = { 1, 1, 1, 1, 6, 9, 4, 1, 7, }
debug: v = { 1, 1, 1, 1, 1, 9, 4, 1, 7, }
debug: v = { 1, 1, 1, 1, 1, 1, 4, 1, 7, }
debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, }
debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, }
1 1 1 1 1 1 1 1 7 

なぜか1が増えている。明らかにおかしい。しかしまだ問題の特定にまでは至らない。

内側のループにもprintfデバッグを追加してみよう。

auto min = head ;
for ( std::size_t index = head+1 ; index != size ; ++index )
{
    // printfデバッグ
        std::cout << v.at(index) << ", "s ;
    // printfデバッグ

    if ( v.at(index) < v.at(min) )
        min = index ;
}
// printfデバッグ
    std::cout << "\n"s ;
// printfデバッグ

そして実行する。

debug: v = { 3, 8, 2, 5, 6, 9, 4, 1, 7, }
8, 2, 5, 6, 9, 4, 1, 7, 
debug: v = { 1, 8, 2, 5, 6, 9, 4, 1, 7, }
2, 5, 6, 9, 4, 1, 7, 
debug: v = { 1, 1, 2, 5, 6, 9, 4, 1, 7, }
5, 6, 9, 4, 1, 7, 
debug: v = { 1, 1, 1, 5, 6, 9, 4, 1, 7, }
6, 9, 4, 1, 7, 
debug: v = { 1, 1, 1, 1, 6, 9, 4, 1, 7, }
9, 4, 1, 7, 
debug: v = { 1, 1, 1, 1, 1, 9, 4, 1, 7, }
4, 1, 7, 
debug: v = { 1, 1, 1, 1, 1, 1, 4, 1, 7, }
1, 7, 
debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, }
7, 
debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, }

1 1 1 1 1 1 1 1 7 

あまりいい情報は得られないようだ。問題はここではないらしい。

ひょっとしたら大小比較が間違っているのかもしれない。確かめてみよう。

for ( std::size_t index = head+1 ; index != size ; ++index )
{

    if ( v.at(index) < v.at(min) )
    {
        std::cout << v.at(index) << " < "s << v.at(min) << "\n"s ;
        min = index ;
    }
    else
    {
        std::cout << v.at(index) << " >= "s << v.at(min) << "\n"s ;
    }
}

実行結果は長いので一部だけ記載しておく。

debug: v = { 3, 8, 2, 5, 6, 9, 4, 1, 7, }
8 >= 3
2 < 3
5 >= 2
6 >= 2
9 >= 2
4 >= 2
1 < 2
7 >= 1

debug: v = { 1, 8, 2, 5, 6, 9, 4, 1, 7, }
2 < 8
5 >= 2
6 >= 2
9 >= 2
4 >= 2
1 < 2
7 >= 1

大小比較も問題ないようだ。では最終的に見つけた最も小さい値は、本当に最も小さい値だろうか。

// 最小値を探すループ
for ( std::size_t index = head+1 ; index != size ; ++index )
{
    // より小さい値があればそれを現在の最小値とする
    if ( v.at(index) < v.at(min) )
        min = index ;
}

// printfデバッグ
    std::cout << v.at(min) << "\n"s ;
// printfデバッグ 
debug: v = { 3, 8, 2, 5, 6, 9, 4, 1, 7, }
1
debug: v = { 1, 8, 2, 5, 6, 9, 4, 1, 7, }
1
debug: v = { 1, 1, 2, 5, 6, 9, 4, 1, 7, }
1
debug: v = { 1, 1, 1, 5, 6, 9, 4, 1, 7, }
1
debug: v = { 1, 1, 1, 1, 6, 9, 4, 1, 7, }
1
debug: v = { 1, 1, 1, 1, 1, 9, 4, 1, 7, }
1
debug: v = { 1, 1, 1, 1, 1, 1, 4, 1, 7, }
1
debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, }
1
debug: v = { 1, 1, 1, 1, 1, 1, 1, 1, 7, }
7
1 1 1 1 1 1 1 1 7 

見つけた値は最も小さいようだ。しかし毎回1になる。1が残っているのだから当然だが、なぜ残っているのだろう。

ひょっとしたら要素の交換が間違っているのかもしれない。printfデバッグしてみよう。

// printfデバッグ 
    std::cout << "debug before: "s <<  v.at(head) << ",  " << v.at(min) << "\n"s ;
// printfデバッグ

v.at(head) = v.at(min) ;
v.at(min) = v.at(head) ;

// printfデバッグ
    std::cout << "debug after : "s << v.at(head) << ", " << v.at(min) << "\n"s ;
// printfデバッグ

"debug before:"は交換前、"debug after:"は交換後の2つの要素の値だ。

以下は実行結果の一部だ。

debug: v = { 3, 8, 2, 5, 6, 9, 4, 1, 7, }
debug before: 3,  1
debug after : 1, 1
debug: v = { 1, 8, 2, 5, 6, 9, 4, 1, 7, }
debug before: 8,  1
debug after : 1, 1
debug: v = { 1, 1, 2, 5, 6, 9, 4, 1, 7, }
debug before: 2,  1
debug after : 1, 1
debug: v = { 1, 1, 1, 5, 6, 9, 4, 1, 7, }

これを見ると、要素の値の交換が正しく行われていないことがわかる。

問題の場所がわかったので、さっそくコードを見てみよう。

v.at(head) = v.at(min) ;
v.at(min) = v.at(head) ;

これは要するに以下のコードと同じだ。

int a = 0 ;
int b = 1 ;

a = b ; // a = 1
b = a ; // b = 1

変数a, bの値を交換したい場合、変数aに変数bを代入したあとに、変数bに変数aを代入する処理は誤りだ。なぜならば、変数bの代入のときには、変数aの値は変数bの値になってしまっているからだ。

前章で学んだように、こういう場合、別の変数に値を代入して退避させておく。

int a = 0 ;
int b = 1 ;

int temp = a ;
a = b ;
b = temp ;

こうしてprintfデバッグによって問題が解決した。

std::cerr

printfデバッグとして標準出力であるstd::coutに出力すると、プログラムの通常の標準出力と混ざって見づらくなる。例えば以下のプログラムを見てみよう。

// 1 * 2 * 3 * ... * nを計算するプログラム
int main()
{
    int n{} ;
    std::cin >> n ;
    if ( n < 1 )
        return -1 ;

    int sum = 1 ;  
    for ( int i = 2 ; i <= n ; ++i )
    {
        sum *= i ;

        // printfデバッグ
            std::cout << "debug: "s << i << ", " << sum << "\n"s ;
        // printfデバッグ
    }

    std::cout << sum ;
}

この場合、標準エラー出力を使うとプログラムの通常の出力とprintfデバッグ用の出力を分けることができる。

標準エラー出力を使うには、std::coutの代わりにstd::cerrを使う。

int main()
{
    // 標準出力
    std::cout << "standard output\n"s ;

    // 標準エラー出力
    std::cerr << "standard error output\n"s ;
}

このプログラムを実行すると一見すべて同じように出力されているように見える。

$ make run
standard output
standard error output

違いはリダイレクトやパイプを使うとわかる。

$ ./program > /dev/null
standard error output
$ ./program | grep error
standard error output 

標準出力には"standard output\n"しか出力されていない。通常のリダイレクトやパイプで扱われるのも標準出力だけだ。そのため、/dev/nullにリダイレクトすると標準エラー出力しか見えないし、grepにパイプしても標準出力しか受け取らない。

標準出力と標準エラー出力を別々にリダイレクトする方法もある。

$ ./program > cout.txt 2> cerr.txt

これを実行すると、ファイルcout.txtには"standard output\n"が、ファイルcerr.txtには"standard error output\n"が出力されている。

これを使って先ほどのプログラムを書き直すと以下のようになる。

// 1 * 2 * 3 * ... * nを計算するプログラム
int main()
{
    int n{} ;
    std::cin >> n ;
    if ( n < 1 )
        return -1 ;

    int sum = 1 ;  
    for ( int i = 2 ; i <= n ; ++i )
    {
        sum *= i ;

        // printfデバッグ
            // 標準エラー出力
            std::cerr << "debug: "s << i << ", " << sum << "\n"s ;
        // printfデバッグ
    }
    // 標準出力
    std::cout << sum ;
}

まとめ

printfデバッグはコード中のどこに問題があるかを絞り込むための方法だ。プログラムに問題が存在し、問題の発生の有無はプログラムの状態を調べることで判断できるが、コード中のどこに問題が存在するかわからないとき、printfデバッグで問題の箇所を絞り込むことができる。

printfデバッグのやり方は以下のとおり。

  1. コード中の間違っていそうな箇所にアタリをつける
  2. プログラムの状態を出力する
  3. 出力結果が期待どおりかどうかを調べる

printfデバッグは原始的だが効果的なデバッグ方法だ。あとの章ではデバッガーというより高級でプログラマーらしいデバッグ方法も紹介するが、そのような高級なデバッグ方法が使えない環境でも、printfデバッグならば使えることは多い。

整数

始めに書いておくがこの章はユーモア欠落症患者によって書かれており極めて退屈だ。しかし、整数の詳細はすべてのプログラマーが理解すべきものだ。心して読むとよい。

整数リテラル

整数リテラルとは整数の値を直接ソースファイルに記述する機能だ。本書ではここまで何の説明もなくリテラルを使っていた。例えば以下のように。

int main()
{
    int a = 123 ;
    int b = 0 ;
    int c = -123 ;
}

ここでは、'123', '0'がリテラルだ。'-123'というのは演算子operator -に整数リテラル123を適用したものだ。リテラルは123だけだ。ただしこれは細かい詳細なのでいまはそれほど気にしなくてもよい。

10進数リテラル

10進数リテラルは最も簡単で我々が日常的に使っている数の表記方法と同じものだ。接頭語は何も使わず数字には0, 1, 2, 3, 4, 5, 6, 7, 8, 9が使える。

// 10進数で123
int decimal = 123 ;

ただし、10進数リテラルの先頭を0にしてはならない。これは8進数リテラルになってしまう。

// 10進数で83
int octal = 0123 ;

2進数リテラル

2進数リテラルは接頭語'0b', '0B'から始まる。数字には0, 1を使うことができる。

// 10進数で5
int binary = 0b1010 ;

// 0bと0Bは同じ
int a = 0B1010 ;

8進数リテラル

8進数リテラルは接頭語'0'から始まる。数字には0, 1, 2, 3, 4, 5, 6, 7を使うことができる。

// 10進数で83
int octal = 0123 ;

// 10進数で342391
int a = 01234567 ;

16進数リテラル

16進数リテラルは接頭語'0x', '0X'から始まる。数字には0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f, A, B, C, D, E, Fが使える。ローマ字の大文字と小文字は意味が同じだ。a, b, c, d, e, fがそれぞれ10, 11, 12, 13, 14, 15を意味する。

// 10進数で291
int hexadecimal = 0x123 ;

// 0xと0Xは同じ
int a = 0X123 ;

// 10進数で10
int b = 0xa ;

// 10進数で15
int c = 0xf ;

数値区切り

長い整数リテラルは読みにくい。例えば10000000100000000はどちらが大きくて具体的にどのくらいの値なのかがわからない。C++には整数リテラルを読みやすいように区切ることのできる数値区切りという機能がある。整数リテラルはシングルクオート文字(')で区切ることができる。

int main()
{
    int a =   1000'0000 ;
    int b = 1'0000'0000 ;
}

区切り幅は何文字でもよい。

int main()
{
    int a = 1'22'333'4444'55555 ;
}

10進数整数リテラル以外でも使える。

int main()
{
    auto a = 0b10101010'11110000'00001111 ;
    auto b = 07'7'5 ;
    auto c = 0xde'ad'be'ef ;
}

整数の仕組み

情報の単位

0から100までの整数を表現するには101種類の状態を表現できる必要がある。コンピューターはどうやって整数を表現しているのかをここで学ぶ。

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

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

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

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

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

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

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

その場合、1バイトの整数の中の8ビットはちょうど2進数8桁で表現できる。

// 0
auto zero = 0b00000000 ;
// 255
auto max  = 0b11111111 ;

一番左側の桁が最上位桁で、一番右側の桁が最下位桁だ。これを最上位ビット、最下位ビットともいう。

正数だけを表現するならば話は簡単だ。1バイトの整数は0から255までの値を表現できる。これを符号なし整数(unsigned integer)という。

では負数を表現するにはどうしたらいいだろう。正数と負数を両方扱える整数表現のことを、符号付き整数(signed integer)という。1バイトは256種類の状態しか表現できないので、もし\(-1\)を表現したい場合、\(-1\)から254までの値を扱えることになる。

\(-1\)しか扱えないのでは実用的ではないので、負数と正数を同じ種類ぐらい表現したい。256の半分は128だが、1バイトで表現された整数は\(-128\)から128までを表現することはできない。0があるからだ。0を含めると、1バイトの整数は最大で\(-128\)から127までか、\(-127\)から128までを表現できる。どちらかに偏ってしまう。

では実際に1バイトで負数も表現できる整数表現を考えてみよう。

符号ビット

誰でも思いつきそうな表現方法に、符号ビットがある。これは最上位ビットを符号の有無を管理するフラグとして用いることにより、下位7ビットの値の符号を指定する方法だ。

符号ビット表現では\(-1\)と1は以下のように表現できる。

// 1
0b0'0000001
// -1
0b1'0000001

最上位ビットが0であれば正数、1であれば負数だ。

この一見わかりやすい表現方法には問題がある。まず表現できる値の範囲は\(-127\)から\(+127\)だ。先ほど、1バイトで正負になるべく均等に値を割り振る場合、\(-128\)から\(+127\)、もしくは\(-127\)から\(+128\)までを扱えると書いた。しかし符号ビット表現では\(-127\)から\(+127\)しか扱えない。残りの1はどこにいったのか。

答えはゼロにある。符号ビット表現ではゼロに2通りの表現がある。\(+0\)\(-0\)だ。

// +0
0b0'0000000
// -0
0b1'0000000

\(+0\)\(-0\)もゼロには違いない。しかし符号ビットが独立して存在しているために、ゼロが2種類ある。

符号ビットは電子回路で実装するには複雑という問題もある。

1の補数

1の補数は負数を絶対値を2進数で表したときの各ビットを反転させた値で表現する。たとえば\(-1\)は1(0b00000001)の1の補数の0b11111110で表現される。

// -1
0b11111110

// -2
0b11111101

\(-1\)\(-2\)を足すと結果は\(-3\)だ。この計算を1の補数で行うとどうなるか。

まず1の補数表現による\(-1\)\(-2\)を足す。

   11111110
+) 11111101
-----------
 1'11111011

この結果は9ビットになる。この整数は8ビットなので、9ビット目を表現することはできない。ただし1の補数表現の計算では、もし9ビット目が繰り上がった場合は、演算結果に1を足す取り決めがある。

   11111011
+)        1
-----------
   11111100

1の補数による\(-3\)は3の各ビットを反転したものだ。3は0b00000011で、そのビットを反転させたものは0b11111100だ。上の計算結果は\(-3\)の1の補数表現になった。

もう1つ例を見てみよう。5と\(-2\)を足すと3になる。

   00000101
+) 11111101
-----------
 1'00000010

繰り上がりが発生したので1を足すと

   00000010
+)        1
-----------
   00000011

3になった。

1の補数は引き算も足し算で表現できるので電子回路での実装が符号ビットよりもやや簡単になる。

ただし、1の補数にも問題がある。0の表現だ。0というのは0b00000000だが1の補数では\(-x\)\(x\)の各ビット反転ということを適用すると、\(-0\)0b11111111になる。すると、符号ビット表現と同じく、\(+0\)\(-0\)が存在することになる。したがって、1の補数8ビットで表現できる範囲は\(-127\)から\(+127\)になる。

2の補数

符号ビットと1の補数による負数表現にある問題は、2の補数表現で解決できる。

2の補数表現による負数は1の補数表現の負数に、繰り上がり時に足すべき1を加えた値になる。

\(-1\)は1の補数表現では、1(0b00000001)の各ビットを反転させた値になる(0b11111110)。2の補数表現では、1の補数表現に1を加えた値になるので、0b11111111になる。

同様に、\(-2\)0b11111110に、\(-3\)0b11111101になる。

2の補数表現の\(-1\)\(-2\)を足すと以下のようになる。

   11111111
+) 11111110
-----------
 1'11111101

9ビット目の繰り上がりを無視すると、計算結果は0b11111101になる。これは2の補数表現による\(-3\)と同じだ。

5と\(-2\)の計算も見てみよう。

   00000101
+) 11111110
-----------
 1'00000011

結果は3(0b00000011)だ。

2の補数表現は引き算も足し算で実装できる上に、ゼロの表現方法は1つで、\(+0\)\(-0\)が存在しない。8ビットの2の補数表現された整数の範囲は\(-128\)から\(+127\)になる。とても便利な負数の表現方法なのでほとんどのコンピューターで採用されている。

整数型

C++にはさまざまな整数型が存在する。C++はCから引き継いだ歴史的な経緯により、整数型の文法がわかりにくくなっている。

基本的には、符号付き整数型と符号なし整数型に分かれている。

符号付き整数型としては、signed char, short int, int, long int, long long intが存在する。符号付き整数型は負数を表現できる。

符号なし整数型としては、unsigned char, unsigned short int, unsigned int, unsigned long int, unsigned long long intが存在する。符号なし整数型は負数を表現できない。

int型

int型は最も基本となる整数型だ。C++で数値を扱う場合、多くはint型になる。

int x = 123 ;

整数リテラルの型は通常はint型になる。

// int
auto x = 123 ;

unsigned int型は符号のないint型だ。

unsigned int x = 123 ;

整数リテラルの末尾にu/Uと書いた場合、unsigned int型になる。

// int
auto x = 123 ;
// unsigned int
auto y = 123u ;

特殊なルールとして、単にsignedと書いた場合、それはintになる。unsignedと書いた場合は、unsigned intになる。

// int
signed a = 1 ;
// unsigned int
unsigned b = 1 ;

signed intと書いた場合、int型になる。signed intintの冗長な書き方だ。

long int型

long int型int型以上の範囲の整数を扱える型だ。具体的な整数型の値の範囲は実装依存だが、long int型int型の表現できる整数の範囲はすべて表現でき、かつint型以上の範囲の整数型を表現できるかもしれない型だ。

unsigned long int型は符号なしのlong intだ。

long int a = 123 ;
unsigned long int b = 123 ;

特殊なルールとして、単にlongと書いた場合、それはlong intになる。unsigned longと書いた場合、unsigned long intになる。

// long int
long a = 1 ;
// unsigned long int
unsigned long b = 1 ;

通常、intを省略して単にlongと書くことが多い。

整数リテラルの値がint型で表現できない場合、long型になる。例えば、int型で100億を表現できないが、long型では表現できる実装の場合、以下の変数along型になる。

// 100億
auto a = 100'0000'0000 ;

整数リテラルの値がlongでは表現できないがunsigned longでは表現できる場合、unsigned long型になる。

整数リテラルの末尾にl/Lと書いた場合、値にかかわらずlong型になる。

// int
auto a = 123 ;
// long
auto b = 123l ;
// long
auto c = 123L ;

符号なし整数型を意味するu/Uと組み合わせることもできる。

// unsigned long
auto a = 123ul ;
auto b = 123lu ;

順番と大文字小文字の組み合わせは自由だ。

long long int型

long long int型long int型以上の範囲の整数を扱える型だ。longと同じくlong longlong long intと同じで、unsigned long long intもある。

// long long int
long long a = 1 ;
// unsigned long long int
unsigned long long b = 1 ;

整数リテラルの値がlong型でも表現できないときは、long longが使われる。long longでも表現できない場合はunsigned long longが使われる。

整数リテラルの末尾にll/LLと書くとlong long int型になる。

// long long int
auto a = 123ll ;
// long long int
auto b = 123LL ;
// unsigned long long int
auto c = 123ull ;

short int型

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

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

char型

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

整数型のサイズ

整数型を含む変数のサイズは、sizeof演算子で確認することができる。sizeof(T)Tに型名や変数名を入れることで、サイズを取得することができる。

int main()
{
    std::cout << sizeof(int) << "\n"s ;

    int x{} ;
    std::cout << sizeof(x) ;
}

sizeof演算子std::size_t型を返す。vectorの章でも出てきたこの型は実装依存の符号なし型であると定義されている。単位はバイトだ。

以下が各種整数型のサイズを出力するプログラムだ。

int main()
{
    auto print = []( std::size_t s )
    { std::cout << s << "\n"s ; } ;

    print( sizeof(char) ) ;
    print( sizeof(short) ) ;
    print( sizeof(int) ) ;
    print( sizeof(long) ) ;
    print( sizeof(long long ) ) ;
}

このプログラムを筆者の環境で実行した結果が以下になる。

1
2
4
8
8

どうやら筆者の環境では、charが1バイト、shortが2バイト、intが4バイト、longlong longが8バイトのようだ。この結果は環境ごとに異なるので読者も自分でsizeof演算子をさまざまな型に適用して試してほしい。

整数型の表現できる値の範囲

整数型の表現できる値の最小値と最大値はstd::numeric_limits<T>で取得できる。最小値は::min()を、最大値は::max()で得られる。

int main()
{
    std::cout
        << std::numeric_limits<int>::min() << "\n"s
        << std::numeric_limits<int>::max() ;
}

実行結果

-2147483648
2147483647

どうやら筆者の環境ではint型は\(−21億4748万3648\)から21億4748万3647までの範囲の値を表現できるようだ。

unsigned intはどうだろうか。

int main()
{
    std::cout
        << std::numeric_limits<unsigned int>::min() << "\n"s
        << std::numeric_limits<unsigned int>::max() ;
}

実行結果

0
4294967295

どうやら筆者の環境では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\)は最小値になる。

int main()
{
    unsigned int min = std::numeric_limits<unsigned int>::min() ;
    unsigned int max = std::numeric_limits<unsigned int>::max() ;

    unsigned int min_minus_one = min - 1u ;
    unsigned int max_plus_one = max + 1u ;

    std::cout << min << "\n"s << max << "\n"s
        << min_minus_one << "\n"s << max_plus_one ;
}

8ビットの符号なし整数型があるとして、最小値は0b00000000(0)になるが、この値を\(-1\)すると0b11111111(255)となり、これは最大値になる。逆に、最大値である0b11111111(255)に\(+1\)すると0b00000000(0)となり、これは最小値になる。

これを数学的に厳密に書くと、「符号なし整数は算術モジュロ\(2^n\)の法に従う。ただし\(n\)は整数を表現する値のビット数である」となる。

符号付き整数型の場合、挙動は定められていない。ただし、一般に普及している2の補数表現の場合は、以下のような挙動になることが多い。

符号付き整数型の最小値を\(-1\)すると最大値になり、最大値を\(+1\)すると最小値になる。

int main()
{
     int min = std::numeric_limits<int>::min() ;
     int max = std::numeric_limits<int>::max() ;

     int min_minus_one = min - 1 ;
     int max_plus_one = max + 1 ;

    std::cout << min << "\n"s << max << "\n"s
        << min_minus_one << "\n"s << max_plus_one ;
}

これはなぜか。2の補数表現の8ビットの符号付き整数の最小値は0b10000000(\(-128\))だが、これを\(-1\)すると0b01111111(127)となり、これは最大値となる。逆に最大値0b01111111(127)を\(+1\)すると0b10000000(\(-128\))となり、これは最小値となる。

整数型の変換

整数型にはここで紹介しただけでも、さまざまな型がある。同じ型同士を使った方がよい。

以下は型が一致している例だ。

int main()
{
    int a = 123 ;
    long b = 123l ;
    long long c = 123ll ;

    unsigned int d = 123u ; 
}

以下は型が一致していない例だ。

int main()
{
    // intからshort
    short a = 123 ;
    // longからint
    int b = 123l ;

    // intからunsigned int
    unsigned int c = 123 ;
    // unsigned intからint
    int d = 123u ;
}

代入や演算で整数型が一致しない場合、整数型の変換が行われる。

整数型の変換で注意すべきこととしては、変換元の値を変換先の型で表現できない場合の挙動だ。

たとえばshort型とint型の表現できる最大値を調べるプログラムを書いてみよう。

int main()
{
    std::cout << "short: "s << std::numeric_limits<short>::max() << "\n"s
        << "int: "s << std::numeric_limits<int>::max() ;
}

これを実行すると筆者の環境では以下のようになる。

short: 32767
int: 2147483647

どうやら筆者の環境ではshort型は約3万、int型は約21億ぐらいの値を表現できるようだ。

では約3万までしか表現できないshort型に4万を代入しようとするとどうなるのか。これは1つ前の整数型の表現できる値の範囲で説明したものと同じことが起こる。

int main()
{
    short x = 40000 ;
    std::cout << x ;
}

このプログラムを実行した結果は実装ごとに異なる。例えば筆者の環境では以下のようになる。

-25536

整数型の変換は暗黙的に行われるが、明示的に行うこともできる。明示的な変換にはstatic_cast<T>(e)を使う。static_castは値eを型Tの値に変換する。

int main()
{
    int x = 123 ;
    short y = static_cast<short>(x) ;
}

浮動小数点数

浮動小数点数の型にはfloat, double, long doubleがある。floatが最も精度が低く、doublefloatと同等以上の精度を持ち、long doubledoubleと同等以上の精度を持つ。

float f = 1.0 ;
double d = 1.0 ;
long double ld = 1.0 ;

以下は浮動小数点数型の変数のサイズを調べるコードだ。

int main()
{
    auto print = [](std::size_t s )
    { std::cout << s << "\n"s ; } ;

    print( sizeof(float) ) ;
    print( sizeof(double) ) ;
    print( sizeof(long double) ) ;
}

筆者の環境では以下のように出力される。

4
8
16

浮動小数点数は一見整数と同じように扱える上、小数点以下の値も扱える。

double a = 1.23 ;
double b = 0.00001 ;

浮動小数点数が表現できる最大値は実装依存だが、通常はかなり大きな値を表現できる。

しかし、浮動小数点数は値を正確に表現しているわけではない。例えば以下のコードを実行してみよう。

int main()
{
    // 1万
    float a = 10000.0 ;
    // 1万分の1
    float b = 0.0001 ;

    // 1万足す1万分の1
    float c = a + b ;

    std::cout << a << "\n"s << b << "\n"s << c ;
}

変数aの値は1万、変数bの値は1万分の1だ。変数cの値はa+b10000.0001となるはずだが結果はどうだろう。

10000
0.0001
10000

変数cの値は10000.0001ではない。この謎は浮動小数点数を学べば明らかになる。

浮動小数点数リテラル

10進浮動小数点数リテラル

浮動小数点数リテラルの最も簡単な書き方は10進数で整数部を書き、小数点'.'を書き、続けて小数部を書く。末尾がf/Fならfloat型、末尾がなければdouble型、末尾がl/Lならlong double型だ。

// float
auto a = 123.456f ;
auto b = 123.456F ;

// double
auto c = 123.456 ;

// long double
auto d = 123.456l ;
auto e = 123.456L ;

10進数の仮数と指数による表記

123.456という値について考えてみよう。この値は以下のように表現することができる。

\[1.23456 \times 10^{2}\]

あるいは以下のように表現することもできる。

\[123456 \times 10^{-3}\]

あるいは以下のようにも表現できる。

\[123.456 \times 10^{0}\]

一般に、値は以下のように表現できるということだ。

\[a \times 10^{b}\]

浮動小数点数リテラルのもう1つの文法として、このabを指定するものがある。

// 値はすべて123.456
auto a = 1.23456e2 ;
auto b = 123456e-3 ;
auto c = 123.456e0 ;
auto d = 123.456E0 ;

この文法は、abe/Eを挟むことによって浮動小数点数の値を指定する。

このaを仮数部(fractional part)、bを指数部(exponent part)という。仮数のことはほかにも、coefficient, significand, mantissaなどと呼ばれたりもする。

そして、指数は底が10になる。

浮動小数点数は、値を正確に表現しているのではなく、仮数と指数の組み合わせで表現している。浮動小数点数が浮動と呼ばれる理由は、指数の存在によって小数点数が浮いているかのように動くからだ。

例えば、仮数と指数がともに符号付き1バイトの整数で表現された2バイトの浮動小数点数があるとする。指数、仮数ともに、\(-128\)から127までの範囲の整数を表現できる。この浮動小数点数は10000(1万)も100000000(1億)も1000000000000(1兆)も表現できる。それぞれ、1e4, 1e8, 1e12だ。

しかし、この浮動小数点数では1000100010000(1兆1億1万)を表現できない。なぜならば、この値を正確に表現するには、100010001e4を表現できる必要があるが、仮数は100010001を表現できないからだ。

浮動小数点数は値を必ずしも正確に表現できない。その代わり、とても大きな値や、とても小さな値を表現できる。

浮動小数点数の型を表す末尾のf/F/l/Lは同じように使える。

// float
auto a = 1.0e0f ;
// double
auto b = 1.0e0 ;
// long double
auto c = 1.0e0l ;

16進数の仮数と指数による表記

浮動小数点数の仮数部と指数部によるリテラルは、16進数で記述することもできる。

文法は、0xから始め、16進数の仮数部を書き、e/Eの代わりにp/Pを使い、指数部を10進数で指定する。このときの指数部の底は2になる。

値は

\[仮数 \times 2^{指数}\]

になる。

// 5496
double a = 0xabc.0p0 ;
// 7134
double b = 0xde.fp5 ;

浮動小数点数の表現と特性

浮動小数点数は指数と仮数で表現される。浮動小数点数の表現はさまざまだが、多くのアーキテクチャーでは国際標準規格のISO/IEC/IEEE 60559:2011が使われている。これは米国電気電子学会の規格IEEE 754-2008と同じ内容になっている。その大本はIntelが立案した規格、IEEE 754-1985だ。一般にはIEEE 754(アイトリプルイー 754)という名称で知られている。

IEEE 754では、浮動小数点数は符号ビット、仮数部、指数部からなる。本書ではIEEE 754を前提として、浮動小数点数で気を付けるべき特性を説明する。

\(+0.0\)\(-0.0\)

IEEE 754では符号ビットがあるので、ゼロには2種類ある。正のゼロと負のゼロだ。

int main()
{
    std::cout << 0.0 << "\n"s << -0.0 ;
}

\(+0.0\)\(-0.0\)の違いを浮動小数点数で表現することはできるが、値を比較すると同じものだとみなされる。

int main()
{
    // a, bは異なるビットパターンを持つ
    double a = +0.0 ;
    double b = -0.0 ;

    // true
    bool c = a == b ;
}

\(+∞\)\(-∞\)(無限大)

IEEE 754の浮動小数点数は正の無限と負の無限を表現できる。

浮動小数点数の値としての無限は、計算の結果として現れるほか、numeric_limits<T>::infinity()を使って取得できる。

int main()
{
    double a = std::numeric_limits<double>::infinity() ;
    double b = -std::numeric_limits<double>::infinity() ;

    std::cout << a << "\n"s << b ;
}

NaN(Not a Number)

NaN(Not a Number)は計算結果が未定義の場合を表現する浮動小数点数の特別な値だ。

計算結果が未定義な場合とは、例えばゼロで除算する場合だ。

値としてのNaNはnumeric_limits<T>::quiet_NaN()で取得できる。

int main()
{
    double NaN = std::numeric_limits<double>::quiet_NaN() ;
    std::cout << NaN ;
}

NaNとの比較結果はNaNと非NaNの非同値比較以外はすべてfalseとなる。

int main()
{
    double NaN = std::numeric_limits<double>::quiet_NaN() ;

    // true
    bool b = NaN != 0.0 ;

    // false
    bool a = NaN == 0.0 ;
    bool c = NaN == NaN ;
    bool d = NaN != NaN ;
    bool e = NaN < 0.0 ;
}

整数であれば、'a == b'falseであるならば、'a != b'なのだと仮定してもよいが、こと浮動小数点数の場合、NaNの存在があるために必ずしもそうとは限らない。上の例でわかるように、NaNとの比較はすべてfalseになる。

有効桁数

浮動小数点数は正確な値のすべての桁数を表現できない。表現できるのは仮数部が何桁を正確に表現できるかに依存している。この有効桁数は、numeric_limits<T>::digits10で取得できる。

int main()
{
    std::cout
        << "float: "s << std::numeric_limits<float>::digits10 << "\n"s
        << "double: "s << std::numeric_limits<double>::digits10 << "\n"s
        << "long double: "s << std::numeric_limits<long double>::digits10 << "\n"s ;
}

浮動小数点数型Tnumeric_limits<T>にはもう1つ、max_digits10がある。これは浮動小数点数を10進数表記にして、その10進数表記を浮動小数点数に戻したときに、浮動小数点数としての値を精度が落ちることなく再現できる桁数のことだ。

もう1つ興味深い値としては、numeric_limits<T>::epsilon()がある。これは浮動小数点数の1と比較可能な最小の値との差だ。

int main()
{
    std::cout
        << "float: "s << std::numeric_limits<float>::epsilon() << "\n"s
        << "double: "s << std::numeric_limits<double>::epsilon() << "\n"s
        << "long double: "s << std::numeric_limits<long double>::epsilon() << "\n"s ;
}

浮動小数点数同士の変換

浮動小数点数型は相互に変換できる。変換先の浮動小数点数型が変換元の値を完全に表現できるならばその値に、できないのであれば近い値に変換される。

int main()
{
    double a = 1.23456789 ;

    // 変換
    float b = a ;
    // 変換
    long double c = a ;
}

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

int main()
{
    // float
    auto a = 1.0f + 1.0f ;
    // double
    auto b = 1.0f + 1.0 ;
    // long double
    auto c = 1.0f + 1.0l ;
}

浮動小数点数と整数の変換

浮動小数点数型を整数型に変換すると、小数部が切り捨てられる。

int main()
{
    double a = 1.9999 ;
    // 1
    int x = a ;
}

変換元の浮動小数点数から小数部を切り捨てた値が変換先の整数型で表現できなかった場合は、何が起こるかわからない。

整数型を浮動小数点数型に変換すると、変換元の整数の値が変換先の浮動小数点数型で正確に表現できる場合はその値に、そうでない場合は表現できる最も近い値になる。

int main()
{
    int a = 1 ;
    // 1.0
    double b = a ;
}

浮動小数点数と整数を演算した場合、浮動小数点数型になる。

int main()
{
    // double
    auto a = 1.0 + 1 ;
    auto b = 1 + 1.0 ;

    // float
    auto c = 1.0f + 1 ;
}

名前

プログラミング言語C++にはさまざまな名前が出てくる。変数、関数、型など、さまざまなものに名前が付いている。この章では名前について学ぶ。

キーワード

一部の名前はキーワードとして予約され、特別な意味を持つ。キーワードは名前として使うことができない。

キーワードの一覧は以下のとおり。

alignas         alignof     asm         auto        bool            break
case            catch       char        char16_t    char32_t        class
concept         const       constexpr   const_cast  continue        decltype
default         delete      do          double      dynamic_cast    else
enum            explicit    export      extern      false           float
for             friend      goto        if          inline          int
long            mutable     namespace   new         noexcept        nullptr
operator        private     protected   public      register        reinterpret_cast
requires        return      short       signed      sizeof          static
static_assert   static_cast struct      switch      template        this
thread_local    throw       true        try         typedef         typeid
typename        union       unsigned    using       virtual         void
volatile        wchar_t     while

名前に使える文字

名前というのは根本的には識別子と呼ばれる文字列で成り立っている。

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
_

小文字と大文字は区別される。名前aと名前Aは別の名前だ。

ただし、名前はアラビア数字で始まってはならない。

int 123abc = 0 ; // エラー

名前にダブルアンダースコア(__)が含まれているものは予約されているので使ってはならない。ダブルアンダースコアとはアンダースコア文字が2つ連続したものをいう。

// 使ってはならない
// すべてダブルアンダースコアを含む
int __ = 0 ;
int a__ = 0 ;
int __a = 0 ;

アンダースコアに大文字から始まる名前も予約されているので使ってはならない。

// 使ってはならない
// アンダースコアに大文字から始まる
int _A = 0 ;

アンダースコアに小文字から始まる名前もグローバル名前空間で予約されているので使ってはならない。グローバル名前空間についてはこのあと説明する。

// 使ってはならない
// アンダースコアに小文字から始まる
int _a = 0 ;

予約されているというのは、C++コンパイラーがその名前をC++の実装のために使うかもしれないということだ。例えばC++コンパイラーは_Aという名前を特別な意味を持つものとして使うかもしれないし、その名前の変数や関数をプログラムに追加するかもしれない。

宣言と定義

C++では、名前は使う前に宣言しなければならない。

int main()
{
    int x = 0 ; // 宣言
    x = 1 ; // 使用
}

宣言する前に使うことはできない。

int main()
{
    // エラー、名前xは宣言されていない。
    x = 1 ; 

    int x = 0 ;
}

C++では多くの名前は宣言と定義に分かれている。宣言と定義の分離は関数が一番わかりやすい。

// 関数の宣言
int plus_one( int x ) ;

// 関数の定義
int plus_one( int x ) // 宣言部分
// 定義部分
// 関数の本体
{
    return x + 1 ;
}

関数の定義は宣言を兼ねる。

宣言は何度でも書くことができる。

int plus_one( int x ) ; // 初出
int plus_one( int x ) ; // OK
int plus_one( int x ) ; // OK

定義はプログラム中に一度しか書くことができない。

// 定義
int odr() { }

// エラー、定義の重複
int odr() { }

名前を使うのに事前に必要なのは宣言だ。定義は名前を使ったあとに書いてもよい。

// 宣言
int plus_one( int x ) ;

int main()
{
    plus_one( 1 ) ;
}

// 定義
int plus_one( int x )
{
    return x + 1 ;
}

ほとんどの変数は宣言と定義が同時に行われる。変数でも宣言と定義を分割して行う方法もあるのだが、解説は分割コンパイルの章で行う。

名前空間

本書をここまで読んだ読者は、一部の型名の記述が少し変なことに気が付いているだろう。

std::string a ;
std::vector<int> b ;

コロンやアングルブラケットは名前に使える文字ではない。信じられない読者は試してみるとよい。

// エラー
int :: = 0 ;
int <int> = 0 ;

莫大なエラーが表示されるだろうが、すでに学んだようにとてもいいことだ。コンパイラーが間違いを見つけてくれたのだから。わからないことがあったらどんどんコンパイルエラーを出すとよい。

実はstdというのは名前空間(namespace)の名前だ。ダブルコロン(::)は名前空間を指定する文法だ。

名前空間の文法は以下のとおり。

namespace ns {
// コード
}

名前空間の中の名前を参照するには::を使う。

ns::name ;

名前空間の中には変数も書ける。この変数は関数の内部に限定されたローカル変数とは違い、どの関数からでも参照できる。

namespace ns {
    int name{} ;
}

int f()
{
    return ns::name ;
}

int main()
{
    ns::name = 1 ;
}

名前空間の中で宣言された名前は、名前空間を指定しなければ使えなくなる。

namespace ns {
    int f() { return 0 ; }
}

int main()
{
    ns::f() ;

    f() ; // エラー
}

異なる名前空間名の下の名前は別の名前になる。

namespace a {
    int f() { return 0 ; }
}


namespace b {
    int f() { return 1 ; }
}

int main()
{
    a::f() ; // 0
    b::f() ; // 1
}

これだけを見ると、名前空間というのはわざわざ名前空間名を指定しなければ使えない面倒な機能に見えるだろう。名前空間の価値は複数人で同じプログラムのソースファイルを編集するときに出てくる。

例えば、アリスとボブがプログラムを共同で開発しているとする。あるプログラムのソースファイルfという名前の関数を書いたとする。ここで、同じプログラムを共同開発している他人もfという名前の関数を書いたらどうなるか。

// アリスの書いた関数f
int f() { return 0 ; }

// ボブの書いた関数f
int f() { return 1 ; }

すでに宣言と定義で学んだように、このコードはエラーになる。なぜならば、同じ名前に対して定義が2つあるからだ。

名前空間なしでこの問題を解決するためはに、アリスとボブが事前に申し合わせて、名前が衝突しないように調整する必要がある。

しかし名前空間があるC++では、そのような面倒な調整は必要がない。アリスとボブが別の名前空間を使えばいいのだ。

// アリスの名前空間
namespace alice {
    // アリスの書いた関数f
    int f() { return 0 ; }
}

// ボブの名前空間
namespace bob {
    // ボブの書いた関数f
    int f() { return 1 ; }
}

alice::fbob::fは別の名前なので定義の衝突は起こらない。

グローバル名前空間

名前空間に包まれていないソースファイルのトップレベルの場所は、実はグローバル名前空間(global name space)という名前のない名前空間で包まれているという扱いになっている。

// グローバル名前空間
int f() { return 0 ; }

namespace ns {
    int f() { return 1 ; }
}

int main()
{
    f() ; // 0
    ns::f() ; // 1
}

グローバル名前空間は名前の指定のない単なる::で指定することもできる。

int x { } ;

int main()
{
    x ; // ::xと同じ
    ::x ;
}

すでに名前空間の中では変数を宣言できることは学んだ。グローバル名前空間は名前空間なので同じように変数を宣言できる。

main関数はグローバル名前空間に存在しなければならない。

// グローバル名前空間
int main() { }

名前空間のネスト

名前空間の中に名前空間を書くことができる。

namespace A { namespace B { namespace C {
    int name {} ;
} } }

int main()
{
    A::B::C::name = 0 ;
}

名前空間のネストは省略して書くこともできる。

namespace A::B::C {
    int name { } ;
}

int main()
{
    A::B::C::name = 0 ;
}

名前空間名の別名を宣言する名前空間エイリアス

名前空間名には別名を付けることができる。これを名前空間エイリアスと呼ぶ。

たとえば名前空間名が重複することを恐れるあまり、とても長い名前空間名を付けたライブラリがあるとする。

namespace very_long_name {
    int f() { return 0 ; }
}

int main()
{
    very_long_name::f() ;
}

この関数fを使うために毎回very_long_name::fと書くのは面倒だ。こういうときには名前空間エイリアスを使うとよい。名前空間エイリアスは名前空間名の別名を宣言できる。

namespace 別名 = 名前空間名 ;

使い方。

namespace very_long_name {
    int f() { return 0 ; }
}

int main()
{
    // 名前空間エイリアス
    namespace vln = very_long_name ;
    // vlnはvery_long_nameのエイリアス
    vln::f() ;
}

名前空間エイリアスは元の名前空間名と同じように使える。意味も同じだ。

名前空間エイリアスはネストされた名前空間にも使える。

namespace A::B::C {
    int f() { return 0 ; }
}

int main()
{
    namespace D = A::B::C ;
    // DはA::B::Cのエイリアス
    D::f() ;
}

名前空間エイリアスを関数の中で宣言すると、その関数の中でだけ有効になる。

namespace A { int x { } ; }

int f()
{
    // Bの宣言
    namespace B = A ;
    // OK、Bは宣言されている
    return B::x ;
}

int g()
{
    // エラー、Bは宣言されていない
    return B::x ;
}

名前空間エイリアスを名前空間の中で宣言すると、宣言以降の名前空間内で有効になる。

namespace ns {
    namespace A { int x { } ; }
    namespace B = A ;

    // OK
    int f(){ return B::x ; }
    // OK
    int g(){ return B::x ; }

} // end namespace ns

// エラー、Bは宣言されていない
int h(){ return B::x ; }

グローバル名前空間は名前空間なので、名前空間エイリアスを宣言できる。

namespace long_name_is_loooong { }
namespace cat = long_name_is_loooong ;

名前空間名の指定を省略するusingディレクティブ

名前空間は名前の衝突を防ぐ機能だが、名前空間名をわざわざ指定するのは面倒だ。

int main()
{
    // std名前空間のstring
    std::string s ;
    // std名前空間のvector<int>
    std::vector<int> v ;

    // std名前空間のcout
    std::cout << 123 ;
}

もし自分のソースファイルがstring, vector<int>, cout、その他std名前空間で使われる名前をいっさい使っていない場合、名前の衝突は発生しないことになる。その場合でも名前空間名を指定しなければならないのは面倒だ。

C++では指定した名前空間を省略できる機能が存在する。usingディレクティブだ。

using namespace 名前空間名 ;

これを使えば、先ほどのコードは以下のように書ける。

int main()
{
    using namespace std ;
    // std名前空間のstring
    string s ;
    // std名前空間のvector<int>
    vector<int> v ;

    // std名前空間のcout
    cout << 123 ;
}

では名前が衝突した場合はどうなるのか。

namespace abc {
    int f() { return 0 ; }
}

int f() { return 1 ; }

int main()
{
    using namespace abc ;

    // エラー、名前が曖昧
    f() ;
}

名前fに対してどの名前を使用するのか曖昧になってエラーになる。このエラーを回避するためには、名前空間を直接指定する。

namespace abc {
    int f() { return 0 ; }
}

int f() { return 1 ; }

int main()
{
    using namespace abc ;

    // OK、名前空間abcのf
    abc::f() ;

    // OK、グローバル名前空間のf
    ::f() ;
}

usingディレクティブは関数の中だけではなく、名前空間の中にも書ける。

namespace A {
    int f(){ return 0 ; }
}

namespace B {
    using namespace A ;
    int g()
    {
        // OK、A::f
        f() ;
    }
}

名前空間の中にusingディレクティブを書くと、その名前空間の中では指定した名前空間を省略できる。

グローバル名前空間は名前空間なのでusingディレクティブが書ける。

using namespace std ;

ただし、グローバル名前空間の中にusingディレクティブを書くと、それ以降すべての箇所で指定した名前空間の省略ができてしまうので注意が必要だ。

名前空間を指定しなくてもよいinline名前空間

inline名前空間inline namespaceで定義する。

inline namespace name { }

inline名前空間内の名前は名前空間名を指定して使うこともできるし、名前空間を指定せずとも使うことができる。

inline namespace A {
    int a { } ;
}

namespace B {
    int b { } ;
}

int main()
{
    a = 0 ;     // A::a
    A::a = 0 ;  // A::a

    b = 0 ;     // エラー、名前bは宣言されていない
    B::b = 0 ;  // B::b
}

読者がinline名前空間を使うことはほとんどないだろうが、ライブラリのソースファイルを読むときには出てくるだろう。

型名

型名とは型を表す名前だ。

型名はintdoubleのように言語組み込みのキーワードを使うこともあれば、独自に作った型名を使うこともある。この独自に作った型名を専門用語ではユーザー定義された型(user-defined type)という。ユーザー定義された型を作る方法はさまざまだ。具体的に説明するのは本書のだいぶあとの方になるだろう。例としては、std::stringstd::vector<T>がある。標準ライブラリによってユーザー定義された型だ。

// 組み込みの型名
int i = 0 ;
double d = 0.0 ;

// ユーザー定義された型名
std::string s ;
std::vector<int> v ;

型名の別名を宣言するエイリアス宣言

長い名前空間名を書くのが煩わしいように、長い型名を書くのも煩わしい。名前空間名の別名を宣言できるように、型名も別名を宣言できる。

型名の別名を宣言するにはエイリアス宣言を使う。

using 別名 = 型名 ;

使い方。

int main()
{
    // エイリアス宣言
    using Number = int ;

    // Numberはintの別名
    Number x = 0 ;
}

型名の別名は型名と同じように使える。意味も同じだ。

歴史的な経緯により、エイリアス宣言による型名の別名のことを、typedef名(typedef name)という。これはtypedef名を宣言する文法が、かつてはtypedefキーワードを使ったものだったからだ。typedefキーワードを使ったtypedef名の宣言方法は、昔のコードによく出てくるので現代でも覚えておく必要はある。

typedef 型名 typedef名 ;

使い方。

int main()
{
    // typedef名による型名の宣言
    typedef int Number ;

    Number x = 0 ;
}

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

型名 変数名 ;

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

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

int main()
{
    int typedef Number ;
    Number x = 0 ;
}

しかし本書ではまだ教えていない複雑な型名について、このようなコードを書こうとするとコンパイルエラーになることに熟練のC++プログラマーは気が付くはずだ。その理由はとても難しい。

エイリアス宣言にはこのようなわなはない。

スコープ

スコープ(scope)というのはやや説明が難しい概念だ。名前空間や関数はスコープを持っている。とてもおおざっぱに説明するとカーリブラケット{}で囲まれた範囲がスコープだ。

namespace ns
{ // 名前空間スコープの始まり
} // 名前空間スコープの終わり

void f()
{ // 関数スコープの始まり

} // 関数スコープの終わり

これとは別にブロック文のスコープもある。ブロックとは関数の中で複数の文を束ねて1つの文として扱う機能だ。覚えているだろうか。

void f()
{ // 関数スコープ

    { // 外側のブロックスコープ
        { // 内側のブロックスコープ
        }
    }
}

スコープは{に始まり}に終わる。

なぜスコープという概念について説明したかというと、宣言された名前が有効な範囲は、宣言された最も内側のスコープの範囲だからだ。

namespace ns
{// aの所属するスコープ
    int a {} ;

    void f()
    { // bの所属するスコープ
        int b {} ;

        { // cの所属するスコープ
            int c {} ;
        }// cの範囲終わり

        
    }// bの範囲終わり

} // aの範囲終わり

名前が有効な範囲は、宣言された最も内側のスコープだ。

外側のスコープで宣言された名前は内側のスコープで使える。

void f()
{
    int a {} ;
    {// 新たなスコープ
        a = 0 ;
    }
}

その逆はできない。

void f()
{
    { int a {} ; }
    // エラー
    a = 0 ;
}

名前空間も同じだ。

// グローバル名前空間スコープ

namespace ns {
    int a {} ;
    void f()
    {
        a = 0 ; // OK
    }
} // 名前空間nsのスコープの終了

int main()
{
    // エラー
    a = 0 ;
    // OK 
    ns::a ;
}

名前空間スコープと関数スコープには違う点もあるが、名前の有効な範囲としては同じスコープだ。

外側のスコープで宣言された名前と同じ名前を内側で宣言すると、内側の名前が外側の名前を隠す。

// グローバル名前空間のf
auto f =  []()
{ std::cout << 1 ; } ;

int main()
{
    f() ; // 1

    // 関数mainのf
    auto f = []()
    { std::cout << 2 ; } ;

    f() ; // 2

    {
        f() ; // 2

        // ブロックのf
        auto f = []()
        { std::cout << 3 ; } ;
        f() ; // 3
    }

    f() ; // 2
}

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

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

イテレーターの基礎

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

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    int x = v.at(2) ; // 3
    v.at(2) = 0 ;
    // vは{1,2,0,4,5}
}

この章ではvectorの要素にアクセスする方法としてイテレーター(iterator)を学ぶ。

イテレーターの取得方法

イテレーターはstd::begin(v)で取得する。vvectorの変数だ。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;
    auto i = std::begin(v) ;
}

イテレーターの参照する要素に対する読み書き

イテレーターはvectorの先頭の要素を指し示している。イテレーターの指し示す要素を参照するには*を使う。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    auto i = std::begin(v) ;

    int x = *i ; // 1

    *i = 0 ;
    // vは{0,2,3,4,5} 
}

*iを読み込むと指し示す要素の値を読むことができ、*iに代入をすると指し示す要素の値を変えることができる。

イテレーターの参照する要素を変更

現在指している要素の次の要素を指すように変更するには++を使う。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    auto i = std::begin(v) ;

    *i ; // 1
    ++i ;
    *i ; // 2
    ++i ;
    *i ; // 3
}

現在指している要素の前の要素を指すように変更するには--を使う。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    auto i = std::begin(v) ;

    *i ; // 1
    ++i ;
    *i ; // 2
    --i ;
    *i ; // 1
}

vectorの全要素を先頭からイテレーターでアクセスするには、要素数だけ++iすればよいことになる。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;
    auto iter = std::begin(v) ;

    for ( std::size_t i = 0 ; i != std::size(v) ; ++i, ++iter )
    {
        std::cout << *iter << "\n"s ;
    }
}

これは動く。ただしもっとマシな方法がある。イテレーターの比較だ。

イテレーターの比較

イテレーターは比較できる。同じ順番の要素を指すイテレーターは等しく、そうではないイテレーターは等しくない。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    auto x = std::begin(v) ;
    auto y = x ;

    // x, yは0番目の要素を指す

    bool b1 = (x == y) ; // true
    bool b2 = (x != y) ; // false

    ++x ; // xは1番目の要素を指す。

    bool b3 = (x == y) ; // false
    bool b4 = (x != y) ; // true
}

最後の次の要素へのイテレーター

std::begin(v)vectorの変数vの最初の要素を指し示すイテレーターを取得する。

std::end(v)vectorの変数vの最後の次の要素を指し示すイテレーターを取得する。

int main()
{
    std::vector<int> v = { 1,2,3,4,5 };

    // 最後の次の要素を指し示すイテレーター
    auto i = std::end(v) ;
}

最後の次の要素とは何か。あるvector<int>の変数の中身が{1,2,3,4,5}のとき、最初の0番目の要素の値は1だ。最後の4番目の要素の値は5だ。最後の次の要素とは、値が5の最後の要素の次の要素だ。そのような要素は実際には存在しないが、std::endは概念として最後の次の要素を返す。

最後の次の要素を指し示すイテレーターに対して、*で要素にアクセスを試みるとエラーになる。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    auto i = std::end(v) ;

    *i ; // エラー
}

最後の次の要素を++しようとするとエラーになる。--することはできる。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;
    auto i = std::end(v) ;

    --i ;   // 最後の要素を指す
    *i ;    // 5
    ++i ;   // 最後の次の要素を指す
    *i ;    // エラー
}

実際には存在しない最後の次の要素を指し示すイテレーターは何の役に立つのか。答えはイテレーターの比較だ。

実際には存在しない最後の次の要素を指すイテレーターに'*'を使って要素にアクセスするのはエラーだが、イテレーター同士の比較はできる。すでに説明したように、イテレーターの比較は同じ要素を指す場合はtrue、違う要素を指す場合はfalseになる。

int main()
{
    std::vector<int> v = {1,2,3} ;

    // xは最初の要素を指す
    auto x = std::begin(v) ;
    // yは最後の次の要素を指す
    auto y = std::end(v) ;


    x == y ; // false
    ++x ; // xは最初の次の要素を指す
    x == y ; // false
    ++x ; // xは最後の要素を指す
    x == y ; // false
    ++x ; // xは最後の次の要素を指す
    x == y ; // true
}

std::endで取得する最後の次の要素を指すイテレーターと比較することで、イテレーターが最後の次の要素を指し示す状態に到達したことを判定できる。

ということは、vectorの要素を先頭から最後まで順番に出力するプログラムは、以下のように書ける。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    for ( auto iter = std::begin(v), last = std::end(v) ;
          iter != last ; ++iter )
    {
        std::cout << *iter << "\n"s ;
    }
}

なんでもイテレーター

イテレーターというのは要素にアクセスする回りくどくて面倒な方法に見える。イテレーターという面倒なものを使わずに、vector::at(i)i番目の要素にアクセスする方が楽ではないか。そう考える読者もいるだろう。イテレーターの利点はその汎用性にある。イテレーターの作法に従うことで、さまざまな処理が同じコードで書けるようになるのだ。

たとえば、vectorの要素を先頭から順番に出力する処理を振り返ってみよう。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    for ( std::size_t i = 0 ; i != std::size(v) ; ++i )
    {
        std::cout << v.at(i) << "\n"s ;
    }
}

このコードはvectorにしか使えないコードだ。イテレーターで書き直してみよう。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    for ( auto iter = std::begin(v), last = std::end(v) ;
          iter != last ; ++iter )
    {
        std::cout << *iter << "\n"s ;
    }
}

そして、この要素を先頭から出力する処理を関数にしてみよう。

auto output_all = []( auto first, auto last )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        std::cout << *iter << "\n"s ;
    }
} ;

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    output_all( std::begin(v), std::end(v) ) ;
}

この関数output_allvector以外のイテレーターにも対応している。C++にはさまざまなイテレーターがある。例えば標準入力から値を受け取るイテレーターがある。さっそく使ってみよう。

int main()
{
    std::istream_iterator<int> first( std::cin ), last ;

    output_all( first, last ) ;
}

このプログラムは標準入力からint型の値を受け取り、それをそのまま標準出力する。

C++にはほかにも、カレントディレクトリーにあるファイルの一覧を取得するイテレーターがある。

int main()
{
    std::filesystem::directory_iterator first("./"), last ;

    output_all( first, last ) ;
}

関数output_allのコードは何も変えていないのに、さまざまなイテレーターに対応できる。イテレーターというお作法にのっとることで、さまざまな処理が可能になるのだ。

これは出力にも言えることだ。関数output_allstd::coutに出力していた。これをイテレーターに対する書き込みに変えてみよう。

auto output_all = []( auto first, auto last, auto output_iter )
{
    for ( auto iter = first ; iter != last ; ++iter, ++output_iter )
    {
        *output_iter = *iter ;
    }
} ;

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

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

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

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;
    
    output_all( std::begin(v), std::end(v),
                std::ostream_iterator<int>(std::cout) ) ;
}

vectorも出力先にできる。つまりvectorのコピーだ。

int main()
{
    std::vector<int> source = {1,2,3,4,5} ;
    std::vector<int> destination(5) ;

    output_all( std::begin(source), std::end(source), std::begin( destination ) ) ;
}

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

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

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

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

たとえば、{1,2,3,4,5}という順序の値の集合があった場合、イテレーターは最初の要素である1と最後の1つ次の要素である5の次の架空の要素を指し示す。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    auto i = std::begin(v) ;
    auto j = std::end(v) ;
}

このようにして範囲を表現することを、半閉鎖(half-closed)とか、[i,j)などと表現する。

この状態から{2,3,4,5}のような値の集合を表現したい場合、イテレーターiをインクリメントすればよい。

++i ;

これで[i,j){2,3,4,5}になった。

このような範囲の表現方法に疑問を感じる読者もいるだろう。なぜ最後の次の要素という本来存在しない架空の要素をあたかも参照しているかのようなイテレーターが必要なのか。最後の要素を参照するのではだめなのか。

そのような範囲の表現方法は、閉鎖(closed)とか[i,j]などと表現する。

実はこの方法はvectorの要素の順番を指定する方法と同じなのだ。

{1,2,3,4,5}と5個の順序ある要素からなるvectorでは、最初の要素は0番目で、最後の要素は4番目だ。1番目から5番目ではない。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    v.at(0) ; // 最初の要素: 1
    v.at(4) ; // 最後の要素: 5
}

ではなぜなのか。なぜvectorでは\(n\)個の要素の順番を0番目から\(n-1\)番目として表現するのか。

実はC++に限らず、現在使われているすべてのプログラミングはインデックスを0から始めている。かつてはインデックスを1から始める言語も存在したが、そのような言語はいまは使われていない。

この疑問はエドガー・ダイクストラ(Edsger Wybe Dijkstra)が“Why numbering should start at zero”(EWD831)で解説している。

2, 3, …, 12の範囲の自然数を表現するのに、慣習的に以下の4つの表記がある。

\[ a) 2 \le i \lt 13 \]

\[ b) 1 \lt i \le 12 \]

\[ c) 2 \le i \le 12 \]

\[ d) 1 \lt i \lt 13 \]

C++のイテレーターはa)を元にしている。

この4つのうち、a)とb)は上限から下限を引くと、範囲にある自然数の個数である11になる。

この性質はとても便利なのでC++でも、イテレーター同士の引き算ができるようになっている。イテレーターi, j(\(i \le j\))でj - iをした結果はイテレーターの範囲の要素の個数だ。

int main()
{
    std::vector<int> v = {2,3,4,5,6,7,8,9,10,11,12} ;

    auto i = std::begin(v) ;
    auto j = std::end(v) ;

    // 11
    // イテレーターの範囲の要素の個数
    std::cout << j - i << "\n"s ;

    ++i ; // 先頭の次の要素を指す
    // 10
    std::cout << j - i ; 
}

a)とb)はどちらがいいのだろうか。b)を元にイテレーターを設計すると以下のようになる。

// b)案を採用する場合
int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 最初の1つ前の架空の要素を指す
    auto i = std::begin(v) ;
    // 最後の要素を指す
    auto j = std::end(v) ;

    // 最初の要素を指すようにする。
    ++i ;

    // iが最後の要素を指すとループを抜ける
    for ( ; i != j ; ++i )
    {
        std::cout << *i ;
    }
    // 最後の要素を処理する
    std::cout << *i ;
    

}

a)の方がよい。

// a)案を採用する場合
int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 最初の要素を指す
    auto i = std::begin(v) ;
    // 最後の次の要素を指す
    auto j = std::end(v) ;

    // iが最後の次の要素を指すとループを抜ける
    for ( ; i != j ; ++i )
    {
        std::cout << *i ;
    }

    // すべての要素について処理を終えている
}

b)案では末尾から先頭まで後ろ向きに要素を一巡する操作はやりやすいが、実際には先頭から末尾まで一巡する操作の方が多い。

C++では要素の順番を数値で指し示すとき、最初の要素は0番目であり、次の要素は1番目であり、\(N\)個目の要素は\(N-1\)番目になっている。この数値で指し示すことを添字とかインデックスというがなぜ最初の要素を1番目にしないのか。

C++ではさまざまなところでa)を採用している。これを添字に適用すると、最初の要素が1番目から始まる場合、\(N\)個の要素を参照する添字の範囲は\(1 \le i \lt N+1\)になる。そのような場合、以下のようなコードになる。

// 最初の要素が1番目の場合
int main()
{
    // 5個の要素を持つvector
    std::vector<int> v = {1,2,3,4,5} ;

    // iの値の範囲は1から5まで
    for ( std::size_t i = 1 ; i < 6 ; ++i )
    {
        std::cout << v.at(i) ;
    }    
}

要素数は5個なのに6が出てくる。最初の要素が0番目の場合、\(N\)個の要素を参照する添字の範囲は\(0 \le i \lt N\)になる。

// 最初の要素が0番目の場合
int main()
{
    // 5個の要素を持つvector
    std::vector<int> v = {1,2,3,4,5} ;

    // iの値の範囲は0から5まで
    for ( std::size_t i = 0 ; i < 5 ; ++i )
    {
        std::cout << v.at(i) ;
    }    
}

一貫性のために最初の要素は0番目となっている。

また、空の集合にも対応できる。

int main()
{
    // 空
    std::vector<int> v ;

    // 空なので何も出力されない
    for (   auto i = std::begin(v), j = std::end(v) ;
            i != j ; ++i )
    {
        std::cout << *i ;
    }
}

変数vは空なのでi != jfalseとなり、for文の中の文は一度も実行されない。

lvalueリファレンスとconst

ポップカルチャーリファレンスというのは流行の要素をさり気なく作品中に取り入れることで、流行作品を知っている読者の笑いを誘う手法である
-- キャプテン・オブビウス、ポップカルチャーリファレンスについて

lvalueリファレンス

変数に変数を代入すると、代入元の値が代入先にコピーされる。代入先の値を変更しても、コピーされた値が変わるだけで、代入元にはいっさい影響がない。

int main()
{
    int a = 1 ;
    int b = 2 ;

    b = a ;
    // b == 1

    b = 3 ;
    // a == 1
    // b == 3
}

これは関数も同じだ。

void assign_3( int x )
{
    x = 3 ;
}

int main()
{
    int a = 1 ;
    assign_3( a ) ;

    // a == 1
}

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

int main()
{
    int a = 1 ;
    int & ref = a ;

    ref = 3 ;

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

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

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

int main()
{
    // エラー
    int & ref ;
}

lvalueリファレンスは関数でも使える。

void f( int & x )
{
    x = 3 ;
}

int main()
{
    int a = 1 ;
    f( a ) ;

    // a == 3
}

選択ソートで2つの変数の値を交換する必要があったことを覚えているだろうか。

int main()
{
    std::vector<int> v = {3,2,1,4,5} ;

    // 0番目と2番目の要素を交換したい
    auto temp = v.at(0) ;
    v.at(0) = v.at(2) ;
    v.at(2) = temp ;
}

いちいち交換のために別の変数tempを作って3回代入を書くのは面倒だ。これを関数にしてしまいたい。

// 値を交換
swap( v.at(0), v.at(2) ) ;

このような関数swapは普通に書くことはできない。

// この実装は正しくない
auto swap = []( auto a, auto b )
{
    auto temp = a ;
    a = b ;
    b = temp ;
} ;

この実装では、変数は単にコピーされるだけなので、関数の呼び出し元には何の影響もない。

これをlvalueリファレンスに変えると、関数の呼び出し元の変数の値を交換する関数swapが作れる。

// lvalueリファレンス
auto swap = []( auto & a, auto & b )
{
    auto temp = a ;
    a = b ;
    b = temp ;
} ;

C++の標準ライブラリにはstd::swapがあるので、読者はわざわざこのような関数を自作する必要はない。

int main()
{
    int a = 1 ;
    int b = 2 ;

    std::swap( a, b ) ;

    // a == 2
    // b == 1
}

ところで、この章では一貫してlvalueリファレンスと書いているのに気が付いただろうか。lvalueとは何なのか、lvalueではないリファレンスはあるのか。その疑問はあとの章で解決する。

const

値を変更したくない変数は、constを付けることで変更を禁止できる。

int main()
{
    int x = 0 ;
    x = 1 ; // OK、変更できる

    const int y = 0 ;
    y = 0 ; // エラー、変更できない。
}

constはちょっと文法が変わっていて混乱する。例えば、const intでもint constでも意味が同じだ。

int main()
{
    // 意味は同じ
    const int x = 0 ;
    int const y = 0 ;
}

constlvalueリファレンスと組み合わせることができる。

int main()
{
    int x = 0 ;

    int & ref = x ;
    // OK
    ++ref ;

    const int & const_ref = x ;

    // エラー
    ++const_ref ;
}

constは本当に文法が変わっていて混乱する。const int &int const &は同じ意味だが、int & constはエラーになる。

int main()
{
    int a = 0 ;

    // OK、意味は同じ
    const int & b = a ;
    int const & c = a ;

    // エラー
    int & const d = a ;
}

これはとても複雑なルールで決まっているので、こういうものだとあきらめて覚えるしかない。

constが付いていない型のオブジェクトをconstlvalueリファレンスで参照することができる。

int main()
{
    // constの付いていない型のオブジェクト
    int x = 0 ;

    // OK
    int & ref = x ;
    // OK、constは付けてもよい
    const int & cref = x ;
}

constの付いている型のオブジェクトをconstの付いていないlvalueリファレンスで参照することはできない。

int main()
{
    // constの付いている型のオブジェクト
    const int x = 0 ;

    // エラー、constがない
    int & ref = x ;

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

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

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

void f( std::vector<int> v )
{
    std::cout << v.at(1234) ;
}

int main()
{
    // 10000個の要素を持つvector
    std::vector<int> v(10000) ;

    f( v ) ;
}

なぜかというと、関数の引数に渡すときに、変数vはコピーされるからだ。

リファレンスを使うと不要なコピーをしなくて済む。

void f( std::vector<int> & v )
{
    std::cout << v.at(1234) ;
}

しかし、リファレンスで受け取ると、うっかり変数を変更してしまった場合、その変更が関数の呼び出し元に反映されてしまう。

// 値を変更するかもしれない
void f( std::vector<int> & v ) ;

int main()
{
    // 要素数10000のvector
    std::vector<int> v(10000) ;

    f(v) ;

    // 値は変更されているかもしれない
}

このとき、constlvalueリファレンスを使うと、引数に取った値を変更しないことを保証できる。

void f( std::vector<int> const & v ) ;

アルゴリズム

アルゴリズムは難しい。アルゴリズム自体の難しさに加え、アルゴリズムを正しくコードで表記するのも難しい。そこでC++ではアルゴリズム自体をライブラリにしている。ライブラリとしてのアルゴリズムを使うことで、読者はアルゴリズムを自前で実装することなく、すでに正しく実装されたアルゴリズムを使うことができる。

for_each

例えばvectorの要素を先頭から順番に標準出力するコードを考えよう。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    for (
        auto i = std::begin(v),
             j = std::end(v) ;
        i != j ;
        ++i  )
    {
        std::cout << *i ;
    }
}

このコードを書くのは難しい。このコードを書くには、イテレーターで要素の範囲を取り、ループを実行するごとにイテレーターを適切にインクリメントし、イテレーターが範囲内であるかどうかの判定をしなければならない。

アルゴリズムを理解するだけでも難しいのに、正しくコード書くのはさらに難しい。例えば以下はコンパイルが通る完全に合法なC++だが間違っている。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    for (
        auto i = std::begin(v),
             j = std::end(v) ;
        i == j ;
        ++i  )
    {
        std::cout << i ;
    }
}

間違っている箇所がわかるだろうか。

まず比較の条件が間違っている。i != jとなるべきところがi == jとなっている。出力する部分も間違っている。イテレーターiが指し示す値を得るには*iとしなければならないところ、単にiとしている。

毎回このようなイテレーターのループをするfor文を書くのは間違いの元だ。ここで重要なのは、要素のそれぞれに対してstd::cout << *i ;を実行するということだ。要素を先頭から末尾まで順番に処理するというのはライブラリにやってもらいたい。

そこでこの処理を関数に切り出してみよう。イテレーター[first,last)を渡すと、イテレーターを先頭から末尾まで順番に処理してくれる関数は以下のように書ける。

auto print_all = []( auto first, auto last )
{
    // ループ
    for ( auto iter = first ; iter != last ; ++iter )
    {
        // 重要な処理
        std::cout << *iter ;
    }
} ; 

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    print_all( std::begin(v), std::end(v) ) ;
}

関数print_allは便利だが、重要な処理がハードコードされている。例えば要素の集合のうち100以下の値だけ出力したいとか、値を2倍して出力したいとか、値を出力するたびに改行を出力したいという場合、それぞれに関数を書く必要がある。

// 値が100以下なら出力
auto print_if_le_100 = []( auto first, auto last )
{
    for ( auto iter = first ; iter != last ; ++iter )
    { // 特別な処理
        if ( *iter <= 100 )
            std::cout << *iter ;
    }
} ;


// 値を2倍して出力
auto print_twice = []( auto first, auto last )
{
    for ( auto iter = first ; iter != last ; ++iter )
    { // 特別な処理
        std::cout << 2 * (*iter) ;
    }
} ;


// 値を出力するたびに改行を出力
auto print_with_newline = []( auto first, auto last )
{
    for ( auto iter = first ; iter != last ; ++iter )
    { // 特別な処理
        std::cout << *iter << "\n"s ;
    }
} ;

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

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

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

ところで、関数は変数に代入できる。

int main()
{
    // 変数に代入された関数
    auto print = []( auto x ) { std::cout << x ; } ;

    // 変数に代入された関数の呼び出し
    print(123) ;
}

変数に代入できるということは、関数の引数として関数に渡せるということだ。

int main()
{
    // 関数を引数に取り呼び出す関数
    auto call_func = []( auto func )
    {
        func(123) ;
    } ;

    // 引数を出力する関数
    auto print = []( auto x ) { std::cout << x ; } ;

    call_func( print ) ;

    // 引数を2倍して出力する関数
    auto print_twice = []( auto x ) { std::cout << 2*x ; } ;

    call_func( print_twice ) ;
}

すると、要素ごとの特別な処理をする関数を引数で受け取り、要素ごとに関数を適用する関数を書くとどうなるのか。

auto for_each = []( auto first, auto last, auto f )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        f( *iter ) ;
    }
} ;

この関数はイテレーターをループで回す部分だけを実装していて、要素ごとの処理は引数に取った関数に任せている。さっそく使ってみよう。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 引数を出力する関数
    auto print_value = []( auto value ) { std::cout << value ; } ;

    for_each( std::begin(v), std::end(v), print_value ) ;

    // 引数を2倍して出力する関数
    auto print_twice = []( auto value ) { std::cout << 2 * value ; } ;

    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 ) ;
}

関数は変数に代入しなくても使えるので、上のコードは以下のようにも書ける。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 引数を出力する
    for_each( std::begin(v), std::end(v),
        []( auto value ) { std::cout << value ; } ) ;

    // 引数を2倍して出力する
    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 ; } ) ;
}

わざわざfor文を書かずに、問題の本質的な処理だけを書くことができるようになった。

このイテレーターを先頭から末尾までループで回し、要素ごとに関数を呼び出すという処理はとても便利なので、標準ライブラリにはstd::for_each( first, last, f)がある。使い方は同じだ。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    std::for_each( std::begin(v), std::end(v),
        []( auto value ) { std::cout << value ; } ) ;
}

C++17の時点ではまだ使えないが、将来のC++では、イテレーターを渡さずに、vectorを直接渡すことができるようになる予定だ。

// C++20予定

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    std::for_each( v, []( auto value ) { std::cout << value ; } ) ;
}

ところでもう一度for_eachの実装を見てみよう。

auto for_each = []( auto first, auto last, auto f )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        f( *iter ) ;
    }
} ;

f(*iter)がとても興味深い。もし関数fがリファレンスを引数に取っていたらどうなるだろうか。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 引数をリファレンスで取って2倍にする関数
    auto twice = [](auto & value){ value = 2 * value ; } ;

    std::for_each( std::begin(v), std::end(v), twice ) ;

    // 引数を出力する関数
    auto print = [](auto & value){ std::cout << value << ", "s ; } ;

    // 2, 4, 6, 8, 10, 
    std::for_each( std::begin(v), std::end(v), print ) ;
}

元のvectorを書き換えることもできる。

all_of/any_of/none_of

ほかのアルゴリズムも実装していくことで学んでいこう。

all_of(first, last, pred)は、[first,last)の間のイテレーターiterのそれぞれに対して、pred(*iter)がすべてtrueを返すならばtrue、そうではないならばfalseを返すアルゴリズムだ。

このall_ofは要素がすべて条件を満たすかどうかを調べるのに使える。

// 要素がすべて偶数かどうか調べる関数
auto is_all_of_odd = []( auto first, auto last )
{
    return std::all_of( first, last,
        []( auto value ) { return value % 2 == 0 ; } ) ;
} ;

// 要素がすべて100以下かどうか調べる関数
auto is_all_of_le_100 = []( auto first, auto last )
{
    return std::all_of( first, last,
        []( auto value ) { return value <= 100; } ) ;
} ;

ところで、もし要素がゼロ個の、つまり空のイテレーターを渡した場合どうなるのだろうか。

int main()
{
    // 空のvector
    std::vector<int> v ;

    bool b = std::all_of( std::begin(v), std::end(v),
        // 特に意味のない関数
        [](auto value){ return false ; } ) ;
}

この場合、all_oftrueを返す。

実装は以下のようになる。

auto all_of = []( auto first, auto last, auto pred )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        if ( pred( *iter ) == false )
            return false ;
    }

    return true ;
} ;

[first,last)が空かどうかを確認する必要はない。というのも、空であればループは一度も実行されないからだ。

any_of(first, last, pred)[first,last)の間のイテレーターiterそれぞれに対して、pred(*iter)が1つでもtrueならばtrueを返す。空の場合、すべてfalseの場合はfalseを返す。

any_ofは要素に1つでも条件を満たすものがあるかどうかを調べるのに使える。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 要素に1つでも3が含まれているか?
    // true
    bool has_3 = std::any_of( std::begin(v), std::end(v),
        []( auto x ) { return x == 3 ;}  ) ;

    // 要素に1つでも10が含まれているか?
    // false
    bool has_10 = std::any_of( std::begin(v), std::end(v),
        []( auto x ) { return x == 10 ;}  ) ;
}

これも実装してみよう。

auto any_of = []( auto first, auto last, auto pred )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        if ( pred( *iter ) )
            return true ;
    }
    return false ;
} ;

none_of(first, last, pred)[first,last)の間のイテレーターiterそれぞれに対して、pred(*iter)がすべてfalseならばtrueを返す。空の場合はtrueを返す。それ以外はfalseを返す。

none_ofはすべての要素が条件を満たさない判定に使える。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 値は100か?
    auto is_100 = [](auto x){ return x == 100 ; } ;

    bool b = std::none_of( std::begin(v), std::end(v), is_100 ) ;
}

これも実装してみよう。

auto none_of = []( auto first, auto last, auto pred )
{
    for ( auto iter = first ; first != last ; ++iter )
    {
        if ( pred(*iter) )
            return false ;
    }
    return true ;
} ;

find/find_if

find( first, last, value )はイテレーター[first,last)からvalueに等しい値を見つけて、そのイテレーターを返すアルゴリズムだ。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 3を指すイテレーター
    auto pos = std::find( std::begin(v), std::end(v), 3 ) ;

    std::cout << *pos ;
}

要素が見つからない場合はlastが返る。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    auto pos = std::find( std::begin(v), std::end(v), 0 ) ;

    if ( pos != std::end(v) )
    {
        std::cout << "Found."s ; 
    }
    else
    {
        std::cout << "Not found."s ;
    }
}

イテレーターがlastかどうかは実際にlastと比較すればよい。

アルゴリズムを理解するには、自分で実装してみるとよい。さっそくfindを実装してみよう。

auto find = []( auto first, auto last, auto const & value )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        // 値を発見したらそのイテレーターを返す
        if ( *iter == value )
            return iter ;
    }
    // 値が見つからなければ最後のイテレーターを返す
    return last ;
} ;

valueauto const & valueになっているのは、リファレンスによってコピーを回避するためと、変更が必要ないためだ。しかし、intdoubleのような単純な型については、わざわざconstlvalueリファレンスを使う必要はない。

find_if(first, last, pred)はイテレーター[first,last)から、要素を関数predに渡したときにtrueを返す要素へのイテレーターを探すアルゴリズムだ。

関数predについてはもう少し解説が必要だ。predとはpredicateの略で、以下のような形をしている。

auto pred = []( auto const & value ) -> bool
{
    return true ;
} ;

関数predは値を1つ引数に取り、bool型を返す関数だ。

さっそく使ってみよう。

int main()
{
    std::vector<int> v = {1,3,5,7,9,11,13,14,15,16} ;

    // 偶数ならばtrueを返す
    auto is_even = []( auto value )
    {
        return value % 2 == 0 ;
    } ;
    // 奇数ならばtrueを返す
    auto is_odd = []( auto value )
    {
        return value % 2 == 1 ;
    } ;

    // 最初の偶数の要素
    auto even = std::find_if( std::begin(v), std::end(v), is_even ) ;
    // 最初の奇数の要素
    auto odd = std::find_if( std::begin(v), std::end(v), is_odd ) ;
}

実装はどうなるだろうか。

auto find_if = []( auto first, auto last, auto pred )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        // predがtrueを返した最初のイテレーターを返す
        if ( pred( *iter ) )
            return iter ;
    }

    return last ;
} ;

値との比較が関数になっただけだ。

つまりある値と比較する関数を渡したならば、find_iffindと同じ動きをするということだ。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 引数が3の場合にtrueを返す関数
    auto is_3 = []( auto x ) { return x == 3 ; } ;

    // 最初に関数がtrueを返す要素へのイテレーターを探すfind_if
    auto i = std::find_if( std::begin(v), std::end(v), is_3 ) ;

    // 最初に3と等しい要素へのイテレーターを返すfind
    auto j = std::find( std::begin(v), std::end(v), 3 ) ;

    // 同じイテレーター
    bool b = (i == j) ;
}

実は、関数は特別な[=]を使うことで、関数の外側の値をコピーして使うことができる。

int main()
{
    int value = 123 ;

    auto f = [=]{ return value ; } ;

    f() ; // 123
}

特別な[&]を使うことで、関数の外側の値をリファレンスで使うことができる。

int main()
{
    int value = 123 ;

    auto f = [&]{ ++value ; } ; 

    f() ;
    std::cout << value ; // 124
}

ということは、findfind_ifで実装することもできるということだ。

auto find = []( auto first, auto last, auto value )
{
    return std::find_if( first, last,
        [&]( auto elem ) { return value == elem ; } ) ;
} ;

count/count_if

count(first, last, value)[first,last)の範囲のイテレーターiから*i == valueになるイテレーターiの数を数える。

countは指定した値と同じ要素の数を数える関数だ。

int main()
{
    std::vector<int> v = {1,2,1,1,3,3} ;

    // 3
    auto a = std::count( std::begin(v), std::end(v), 1 ) ;
    // 1
    auto b = std::count( std::begin(v), std::end(v), 2 ) ;
    // 2
    auto c = std::count( std::begin(v), std::end(v), 3 ) ;
}

実装してみよう。

auto count = []( auto first, auto last, auto value )
{
    auto counter = 0u ;
    for ( auto i = first ; i != last ; ++i )
    {
        if ( *i == value )
            ++counter ;  
    }
    return counter ;
} ;

count_if(first, last, pred)[first, last)の範囲のイテレーターiからpred(*i) != falseになるイテレーターiの数を返す。

count_ifは要素を数える対象にするかどうかを判定する関数を渡せるcountだ。

int main()
{
    std::vector<int> v = {1,2,1,1,3,3} ;

    // 奇数の数: 5
    auto a = std::count_if( std::begin(v), std::end(v),
        [](auto x){ return x%2 == 1 ; } ) ;

    // 偶数の数: 1
    auto b = std::count_if( std::begin(v), std::end(v),
        [](auto x){ return x%2 == 0 ; } ) ;

    // 2以上の数: 3
    auto c = std::count_if( std::begin(v), std::end(v),
        [](auto x){ return x >= 2 ; } ) ;
}

実装してみよう。

auto count = []( auto first, auto last, auto pred )
{
    auto counter = 0u ;
    for ( auto i = first ; i != last ; ++i )
    {
        if ( pred(*i) != false )
            ++counter ;  
    }
    return counter ;
} ;

equal

これまでのアルゴリズムは1つのイテレーターの範囲だけを扱ってきた。アルゴリズムの中には複数の範囲を取るものもある。

equal(first1, last1, first2, last2)[first1, last1)[first2, last2)が等しい場合にtrueを返す。「等しい」というのは、要素の数が同じで、各要素がそれぞれ等しい場合を指す。

int main()
{
    std::vector<int> a = {1,2,3,4,5} ;
    // aと等しい
    std::vector<int> b = {1,2,3,4,5} ;
    // aと等しくない
    std::vector<int> c = {1,2,3,4,5,6} ;
    // aと等しくない
    std::vector<int> d = {1,2,2,4,6} ;

    // true
    bool ab = std::equal(
        std::begin(a), std::end(a),
        std::begin(b), std::end(b) ) ;

    // false
    bool ac = std::equal(
        std::begin(a), std::end(a),
        std::begin(c), std::end(c) ) ;

    // false
    bool ad = std::equal(
        std::begin(a), std::end(a),
        std::begin(d), std::end(d) ) ;
}

実装は、まず要素数を比較し、等しくなければfalseを返す。次に各要素を1つずつ比較し、途中で等しくない要素が見つかればfalseを、最後まで各要素が等しければtrueを返す。

イテレーターの範囲[first, last)の要素数はlast-firstで取得できる。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 最初の要素
    auto first = std::begin(v) ;
    // 最後の1つ次の要素
    auto last = std::end(v) ;

    // 要素数: 5
    auto size = last - first ;

    // 最初の次の要素
    auto next = first + 1 ;

    // 4
    auto size_from_next = last - next ;
}

last-firstという表記はわかりにくいので、C++にはdistance(first, last)というライブラリが用意されている。

auto distance = []( auto first, auto last )
{
    return last - first ;
} ;

これを使えばわかりやすく書ける。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;

    // 最初の要素
    auto first = std::begin(v) ;
    // 最後の1つ次の要素
    auto last = std::end(v) ;

    // 要素数: 5
    auto size = std::distance( first, last ) ;

    // 4
    auto size_from_next = std::distance( first + 1, last ) ;
}

あとは実装するだけだ(この実装は最も効率のいい実装ではない。理由についてはイテレーターの章を参照)。

auto equal = []( auto first1, auto last1, auto first2, auto last2)
{
    // 要素数が等しいことを確認
    auto size1 = std::distance( first1, last1 ) ;
    auto size2 = std::distance( first2, last2 ) ;

    if ( size1 != size2 )
        // 要素数が等しくなかった
        return false ;

    // 各要素が等しいことを確認
    for (   auto i = first1, j = first2 ;
            i != last1 ; ++i, ++j )
    {
        if ( *i != *j )
            // 等しくない要素があった
            return false ;
    }
    // 各要素がすべて等しかった
    return true ;
} ;

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

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

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

int main()
{
    std::vector<double> v = {1.3, 2.2, 3.0, 4.9, 5.7} ;
    std::vector<double> w = {1.9, 2.4, 3.8, 4.5, 5.0} ;

    // 小数点以下は誤差として切り捨てる比較
    auto comp = []( auto a, auto b )
    {
        return std::floor(a) == std::floor(b) ;
    } ;

    bool b = std::equal(
        std::begin(v), std::end(v),
        std::begin(w), std::end(w),
        comp ) ;
}

std::floor(x)は浮動小数点数xの小数点数以下を切り捨てた結果を返す関数だ。floor(0.999)0.0に、floor(1.999)1.0になる。

本書をここまで読んできた読者であれば実装は自力でできるだろう。

search( first1, last1, first2, last2)はイテレーター[first2, last2)の範囲で示された連続した要素の並びがイテレーター[first1, last1)の範囲に存在すればtrue、そうでない場合はfalseを返す。

こう書くと難しいが、例を見るとわかりやすい。

int main()
{
    std::vector<int> v1 = {1,2,3,4,5,6,7,8,9} ;
    std::vector<int> v2 = {4,5,6} ;

    // true
    bool a = std::search( std::begin(v1), std::end(v1), std::begin(v2), std::end(v2) ) ;

    std::vector<int> v3 = {1,3,5} ;
    // false 
    bool a = std::search( std::begin(v1), std::end(v1), std::begin(v3), std::end(v3) ) ;
}

この例では、v1の中にv2と同じ並びの{4,5,6}が存在するのでtruev3と同じ並びの{1,3,5}は存在しないのでfalseになる。

searchの実装例はいまの読者にはまだ理解できない。equalsearchを効率的に実装するにはイテレーターの詳細な理解が必要だ。

copy

これまでのアルゴリズムはfor_eachを除き要素の変更をしてこなかった。copyは要素の変更をするアルゴリズムだ。

イテレーターi*iで参照する要素の値として使うことができるほか、*i = xで要素に値xを代入できる。

int main()
{
    std::vector<int> v = {1} ;

    auto i = std::begin(v) ;

    // 参照する要素を値として使う
    std::cout << *i ;
    // 参照する要素に値を代入する。
    *i = 2 ;
}

copy(first, last, result)はイテレーター[first, last)の範囲の値を、先頭から順番にイテレーターresultに書き込んでいくアルゴリズムだ。

int main()
{
    std::vector<int> source = {1,2,3,4,5} ;
    // 要素数5のvector
    std::vector<int> destination(5) ;

    std::copy( std::begin(source), std::end(source), std::begin(destination) ) ;

    // destinationの中身は{1,2,3,4,5}
}

これは実質的に以下のような操作をしたのと等しい。

int main()
{
    std::vector<int> source = {1,2,3,4,5} ;
    std::vector<int> destination(5) ;
    // 要素をそれぞれコピー 
   destination[0] = source[0] ;
   destination[1] = source[1] ;
   destination[2] = source[2] ;
   destination[3] = source[3] ;
   destination[4] = source[4] ;
   
}

イテレーターresultは先頭のイテレーターのみで末尾のイテレーターは渡さない。イテレーターresultはイテレーター[first, last)の範囲の要素数をコピーできるだけの要素数の範囲を参照していなければならない。

例えば以下の例はエラーになる。

int main()
{
    std::vector<int> source = {1,2,3,4,5} ;
    // 要素数3のvector
    std::vector<int> destination(3) ;

    // エラー
    std::copy( std::begin(source), std::end(source), std::begin(destination) ) ;
}

要素数が3しかないvectorに5個の要素をコピーしようとしている。

copyの戻り値は[first,last)の要素数だけ進めたイテレーターresultになる。これはつまり、result + (last - first)だ。

int main()
{
    std::vector<int> source = {1,2,3,4,5} ;
    std::vector<int> destination(5) ;
    
    auto first = std::begin(source) ;
    auto last = std::end(source) ;
    auto result = std::begin(destination) ;
    
    auto returned = std::copy( first, last, result ) ;

    // true
    bool b = (returned == (result + (last - first)) ;
}

ここで、last-firstsourceの要素数の5なので、result + 5copyの戻り値のイテレーターと等しい。

copyには[first,last)の範囲がresultから続く範囲とオーバーラップしてはいけないという制約がある。

オーバーラップというのは、同じ要素を参照しているという意味だ。

int main()
{
    std::vector<int> v = {1,2,3} ;

    // [first,last)とresultがオーバーラップしている
    std::copy( std::begin(v), std::end(v), std::begin(v) ) ;
}

オーバーラップした場合、copyの動作は保証されない。

実装例。

auto copy = []( auto first, auto last, auto result )
{
    for ( auto iter = first ; iter != last ; ++iter, ++result )
    { *result = *iter ; }

    return result ;
} ;

transform

transform(first, last, result, op)copyに似ているが、resultへのコピーが*result = *iter ;ではなく、*result = op(*iter) ;になる。opは関数だ。

以下が実装例だ。copyとほぼ同じだ。

auto transform = []( auto first, auto last, auto result, auto op )
{
    for ( auto iter = first ; iter != last ; ++iter, ++result )
    { *result = op(*iter) ; }

    return result ;
} ;

使い方はcopyと似ているが、値をコピーをする際に関数を適用することができる。

int main()
{
    std::vector<int> a = {1,2,3,4,5} ;

    std::vector<int> b(5) ;
    std::transform( std::begin(a), std::end(a), std::begin(b),
        [](auto x){ return 2*x ; } ) ;
    // bは{2,4,6,8,10}


    std::vector<int> c(5) ;
    std::transform( std::begin(a), std::end(a), std::begin(c),
        [](auto x){ return x % 3 ; } ) ;
    // cは{1,2,0,1,2}

    
    std::vector<bool> d(5) ;
    std::transform( std::begin(a), std::end(a), std::begin(d),
        [](auto x){ return x < 3 ; } ) ;
    // dは{true,true,false,false,false}
}

resultに代入されるのは関数opの戻り値だ。関数opは値を1つの引数で受け取り値を返す関数だ。

replace

replace(first, last, old_value, new_value)はイテレーター[first,last)の範囲のイテレーターが指す要素の値がold_valueに等しいものをnew_valueに置換する関数だ。

int main()
{
    std::vector<int> a = {1,2,3,3,4,5,3,4,5} ;
    std::replace( std::begin(a), std::end(a), 3, 0 ) ;
    // aは{1,2,0,0,4,5,0,4,5}
}

実装も簡単。

auto replace = []( auto first, auto last, auto old_value, auto new_value )
{
    for ( auto iter = first ; first != last ; ++iter )
    {
        if ( *iter == old_value )
            *iter = new_value ;
    }
} ;

fill

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

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;
    std::fill( std::begin(v), std::end(v), 0 ) ;
    // vは{0,0,0,0,0}
}

fill_n(first, n, value)はイテレーター[first, first+n)の範囲のイテレーターが参照する要素にvalueを代入する関数だ。

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;
    std::fill_n( std::begin(v), 5, 0 ) ;
    // vは{0,0,0,0,0}
}

実装例。

auto fill_n = []( auto first, auto n, auto value )
{
    for ( auto i = 0 ; i != n ; ++i, ++first )
    {
        *first = value ;
    }
} ;

generate

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

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

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

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

int main()
{
    std::vector<int> v = {1,2,3,4,5} ;
    auto gen_zero = []{ return 0 ; } ;
    std::generate_n( std::begin(v), 5, gen_zero ) ;
    // vは{0,0,0,0,0}
}

実装例は単純だ。

auto generate = []( first, last, gen )
{
    for ( auto iter = first ; iter != last ; ++iter )
    {
        *iter = gen() ;
    }
} ;

auto generate_n = []( first, n, gen )
{
    for ( auto i = 0u ; i != n ; ++i, ++iter )
    {
        *iter = gen() ;
    }
} ;

remove

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

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

1, 2, 3

この中から値2removeのように取り除く場合、以下のようになる。

1, 3, ?

removeは取り除くべき値の入った要素を、後続の値で上書きする。この場合、1番目の2を2番目の3で上書きする。2番目は不定な状態になる。これは、removeアルゴリズムは2番目がどのような値になるかを保証しないという意味だ。

以下のような数列で値2removeしたとき

1,2,2,3,2,2,4

以下のようになる。

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

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

auto last2 = remove( first, last, value ) ;

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

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

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

int main()
{
    std::vector<int> v = {1,2,3} ;

    auto last = std::remove( std::begin(v), std::end(v), 2 ) ;

    // "13"
    std::for_each( std::begin(v), last,
        [](auto x) { std::cout << x ; } ) ;

    std::vector<int> w = {1,2,2,3,2,2,4} ;

    auto last2 = std::remove( std::begin(w), std::end(w), 2 ) ;

    // "134"
    std::for_each( std::begin(w), last2,
        [](auto x) { std::cout << x ; } ) ;
   
}

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

int main()
{
    // 偶数の場合true、奇数の場合falseを返す関数
    auto is_even = []( auto x ) { return x%2 == 0 ; } ;

    std::vector v = { 1,2,3,4,5,6,7,8,9 } ;
    // 偶数を取り除く
    auto last = std::remove_if( std::begin(v), std::end(v), is_even ) ;

    // [ std::begin(v), last)は{1,3,5,7,9}
}

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

auto remove_if = []( auto first, auto last, auto pred )
{
    // removeする最初の要素
    auto removing = std::find_if( first, last, pred ) ;
    // removeする要素がなかった
    if ( removing == last )
        return last ;

    // removeする要素の次の要素
    auto remaining = removing ;
    ++remaining ;

    // removeする要素に上書きする
    for (  ; remaining != last ; ++remaining )
    {
        // 上書き元も取り除くのであればスキップ
        if ( pred( *remaining ) == false )
        {
            *removing = *remaining ;
            ++removing ;
        }

    }
    // 新しい終端イテレーター
    return removing ;
} ;

ラムダ式

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

auto function = []( auto value ) { return value } ;

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

基本

ラムダ式の基本の文法は以下のとおり。

[](){} ;

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

[]  // ラムダ導入子
()  // 引数リスト
{}  // 複合文
;   // 文末

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

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

void f( int x, double d ) { }

[]( int x, double d ) { } ;

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

[]( auto x ) { } ;

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

int main()
{
    auto f = []( auto x )
    { std::cout << x ; } ;

    f(0) ; // int
    f(1.0) ; // double
    f("hello"s) ; // std::string
}

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

[]()
{
    std::cout << "hello"s ;
    int x = 1 + 1 ;
} ;

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

1 + 1 ; // OK、式文
[](){} ; // OK、式文

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

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

void f( std::string x )
{
    std::cout << x ;
}

int main()
{
    f( "hello"s ) ;
    []( auto x ){ std::cout << x ; }( "hello"s ) ;
}

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

f               // 関数
( "hello"s ) ;  // 関数呼び出し

// ラムダ式
[]( auto x ){ std::cout << x ; }
( "hello"s ) ;  // 関数呼び出し

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

// 引数を取らないラムダ式
[](){} ;
// 引数リストは省略できる
[]{} ;

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

// int
[]{ return 0 ; } ;
// double
[]{ return 0.0 ; } ;
// std::string
[]{ return "hello"s ; } ;

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

[]( bool b )
{
    if ( b )
        return 0 ;
    else
        return 0.0 ;
} ;

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

[]( bool b ) -> int
{
    if ( b )
        return 0 ;
    else
        // doubleからintへの変換
        return 0.0 ;
} ;

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

// int
auto f() { return 0 ; }

// 戻り値の型の明示的な指定
auto f() -> int { return 0 ; }

キャプチャー

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

void f()
{
    // ローカル関数
    auto message = "hello"s ;

    [=](){ std::cout << message ; } ;
}

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

コピーキャプチャー

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

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

int main()
{
    int x = 0 ;
    // コピーキャプチャー
    [=]{ return x ; } ;
}

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

int main()
{
    int x = 0 ;
    // エラー
    [=]{ x = 0 ; } ;
}

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

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

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

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

int main()
{
    int x = 0 ;
    // 通常の変数
    int y = x ;

    // 変数を変更
    y = 1 ;
    // xの値は変わらない

    // リファレンス
    int & ref = x ;

    // リファレンスを変更
    ref = 1 ;
    // xの値が変わる
}

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

int main()
{
    int x = 0 ;
    [&] { return x ; } ;
}

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

int main()
{
    int x = 0 ;
    auto f = [&]{ ++x ; } ;

    f() ; // x == 1
    f() ; // x == 2
    f() ; // x == 3
}

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

クラスの基本

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

クラスとは何か。クラスにはさまざまな機能があるが、最も基本的な機能としては以下の2つがある。

この章はクラスの数ある機能のうち、この2つの機能だけを説明する。

変数をまとめる

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

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

int main()
{
    // 表現
    int point_x = 0;
    int point_y = 0;
}

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

int main()
{
    int x1 = 0 ;
    int y1 = 0 ;

    int x2 = 0 ;
    int y2 = 0 ;

    int x3 = 0 ;
    int y3 = 0 ;
}

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

int main()
{
    std::vector<int> xs ;
    std::vector<int> ys ;

    // xs.at(i)とys.at(i)は同じ点のための変数

    int x {} ;
    int y {} ;
    while ( std::cin >> x >> y )
    {
        xs.push_back(x) ;
        ys.push_back(y) ;
    }
}

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

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

struct point
{
    int x = 0 ;
    int y = 0 ;
} ;

int main()
{
    point p ;

    std::cout << p.x << p.y ;
}

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

point p1 ;
point p2 ;
point p3 ;

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

struct point
{
    int x = 0 ;
    int y = 0 ;
} ;

int main()
{
    std::vector<point> ps ;

    int x { } ;
    int y { } ;

    while( std::cin >> x >> y )
    {
        ps.push_back( point{ x, y } ) ;
    }    
}

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

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

struct class_name 
{

} ;

変数は{}の中に書く。

struct S
{
    int a = 0 ;
    double b = 0.0 ;
    std::string c = "hello"s ;
} ;

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

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

// 名前と年齢を表現するクラスPerson
struct Person
{
    std::string name ;
    int age ;
} ;

int main()
{
    Person john ;
    john.name = "john" ;
    john.age = 20 ;
}

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

struct S
{
    // これは変数ではない
    int data ;
} ;

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

struct S
{
    int data ;
} ;

int main()
{
    S s1 ; // 変数
    // オブジェクトs1に紐付いたストレージ
    s1.data = 0 ;

    S s2 ;
    // 別のストレージ
    s2.data = 1 ; 

    // false
    bool b = s1.data == s2.data ;
}

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

struct S
{
    int x ;
    int y ;
    int z ;
} ;

int main()
{
    S s { 1, 2, 3 } ;
    // s.x == 1
    // s.y == 2
    // s.z == 3
}

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

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

struct S { int a ; double b ; std::string c ; } ;

int main()
{
    S a{123, 1.23, "123"} ;
    // データメンバーがそれぞれコピーされる
    S b = a ;
}

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

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

int main()
{
    int num = 1 ;
    int denom = 2 ;

    // 出力
    std::cout << static_cast<double>(num) / static_cast<double>(denom) ;
}

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

struct fractional
{
    int num ;
    int denom ;
} ;

int main()
{
    fractional x{1, 2} ;

    // 出力
    std::cout << static_cast<double>(x.num) / static_cast<double>(x.denom) ;
}

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

double value( fractional & x )
{
    return static_cast<double>(x.num) / static_cast<double>(x.denom) ;
}

int main()
{
    fractional x{ 1, 2 } ;
    std::cout << value( x ) ;
}

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

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

struct S
{
    void member_function( int x )
    {
        return x ;
    }
} ;

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

struct fractional
{
    int num ;
    int denom ;

    double value()
    {
        return static_cast<double>(num) / static_cast<double>(denom) ;
    }
} ;

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

int main()
{
    fractional x{ 1, 2 } ;
    std::cout << x.value() ;
}

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

struct S
{
    int x ;
    void print()
    {
        std::cout << x ;
    }
} ;

int main()
{
    S s1(1) ;
    s1.print() ; // 1

    S s2(2) ;
    s2.print() ; // 2
}

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

void print( S & s )
{
    std::cout << s.x ;
}

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

struct S
{
    void f() { }
} ;

int main()
{
    f() ; // エラー
    S s ;
    s.f() ; // OK
}

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

struct X
{
    int data ;
    void f()
    {
        data = 3 ;
    }
} ;

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

struct fractional
{
    int num ;
    int denom ;

    void set( int num_ )
    {
        num = num_ ;
        denom = 1 ;
    }
    void set( int num_, int denom_ )
    {
        num = num_ ;
        denom = denom_ ;
    }
} ;

int main()
{
    fractional x ;


    x.set(5) ;
    // x.num == 5
    // x.denom == 1

    x.set( 2, 3 ) ;
    // x.num == 2
    // x.denom == 3
}

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

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

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

struct S
{
    int x ;
    void f( int x )
    {
        x = x ;
    }
} ;

int main()
{
    S s{0} ;
    s.f(1) ;

    std::cout << s.x ;
}

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

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

// グローバル名前空間スコープ
int x ;

struct S
{
    // クラススコープ
    int x ;

    void f( int x )
    {
        // 関数のブロックスコープ
        x = x ;
    }
} ;

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

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

int x ;

struct S
{
    int x ;

    void f( int x )
    {
        // 関数のブロックスコープのx
        x = 0 ;
        // クラススコープのx
        S::x = 0 ;
        // グローバル名前空間のスコープ
        ::x = 0 ;
    }
} ;

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

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

struct fractional
{
    int num ;
    int denom ;

    void set( int num, int denom )
    {
        fractional::num = num ;
        fractional::denom = denom ;
    }
}

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

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

int main()
{
    int a = 1 ;
    int b = a + a ;
    int c = a + b ;
}

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

struct fractional
{
    int num ;
    int denom ;
} ;

fractional add( fractional & l, fractional & r )
{
    // 分母が同じなら
    if ( l.denom == r.denom )
        // 単に分子を足す
        return fractional{ l.num + r.num, l.denom } ;

    // 分母を合わせて分子を足す
    return fractional{ l.num * r.denom + r.num * l.denom, l.denom * r.denom } ;
}

int main()
{
    fractional a{1,1} ;
    fractional b = add(a, a) ;
    fractional c = add(a, b) ;
}

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

int main()
{
    fractional a = 1 ;
    fractional b = a + a ;
    fractional c = a + b ;
}

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

より自然な初期化

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

int a = 0 ;
int b(0) ;
int c{0} ;

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

struct fractional
{
    int num ;
    int denom ;

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

int main()
{
    fractional a = 1 ;
    fractional b = 2 ;
}

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

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

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

struct class_name
{
    int data_member ;

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

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

struct class_name
{
    int x ;
    class_name( int x )
        : x(x) { }
} ;

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

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

struct class_name
{
    int x ;
    class_name( int x )
    {
        class_name::x = x ;
    }
} ;

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

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

int main()
{
    S a(1) ;
    S b(2) ;
    S c(3) ;
}

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

123

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

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

struct S
{
    // デストラクター
    ~S()
    {
        // オブジェクトの破棄時に実行される
    }
} ;

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

int main()
{
    int a ;
    {
        int b ;
    // bが破棄される
    }
    int c ;
// cが破棄される
// aが破棄される
}

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

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

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

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

int main()
{
    S a(1) ;
    { S b(2) ; }
    S c(3) ;
}

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

constructed: 1
constructed: 2
destructed: 2
constructed: 3
destructed: 3
destructed: 1

この出力は以下のような意味だ。

  1. aが構築される
  2. bが構築される
  3. bが破棄される
  4. cが構築される
  5. cが破棄される
  6. aが破棄される

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

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

struct class_name
{
    class_name()
    {
        return ;
    }
} ;

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

struct fractional
{
    int num ;
    int denom ;

    fractional( int num )
        : num(num), denom(1)
    { }

    fractional( int num, int denom )
        : num(num), denom(denom)
    { }
} ;

int main()
{
    // fractional(int)が呼ばれる
    fractional a = 1 ;

    // fractional(int,int)が呼ばれる
    fractional b(1, 2) ;
    fractional c{1, 2} ;
}

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

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

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

void f( int x = 0 )
{ }

int main()
{
    f() ;  // f(0)
    f(1) ; // f(1)
}

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

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

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

int main()
{
    // 123は仮引数xに対する実引数
    f( 123 ) ;
}

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

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

// OK
void f( int x, int y = 0, int z = 0 ) { }
// エラー
// zにデフォルト実引数がない
void g( int x, int y = 0, int z ) { }

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

void f( int x = 0, int y = 0, int z = 0) { }

int main()
{
    // エラー
    f( 1, , 2 ) ;
}

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

struct fractional
{
    int num ;
    int denom ;

    fractional( int num, int denom = 1 )
        : num(num), denom(denom)
    { }
} ;

int main()
{
    fractional a = 1 ;
    fractional b(1,2) ;
    fractional c{1,2} ;
}

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

struct fractional
{
    int num ;
    int denom ;

    fractional( int num, int denom )
        : num(num), denom(denom)
    { }

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

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

struct S
{
    S()
        : S(1)
    {
        std::cout << "delegating constructor\n" ;
    }

    S( int n )
    {
        std::cout << "constructor\n" ;
    }
} ;

int main()
{
    S s ;
}

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

constructor
delegating constructor

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

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

struct A { } ;
struct B { B(int) { } } ;

int main()
{
    A a ; // OK
    B b ; // エラー
}

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

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

struct B
{
    B() { }
    B( int x ) { }
} ;

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

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

struct B
{
    B( int x = 0 ) { }
} ;

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

// 人間クラス
// 必ず名前が必要
struct person
{
    std::string name
    person( std::string name )
        : name(name) { }
} ;

自然な演算子

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

int main()
{
    int a = 1 ;
    int b = 1 ;
    a + b ;
    a - b ;
    a * b ;
    a / b ;
}

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

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

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

struct fractional
{
    int num ;
    int denom ;

// コンストラクターなど
} ;

fractional add( fractional & l, fractional & r )
{
    // 分母が同じなら
    if ( l.denom == r.denom )
        // 単に分子を足す
        return fractional{ l.num + r.num, l.denom } ;

    // 分母を合わせて分子を足す
    return fractional{ l.num * r.denom + r.num * l.denom, l.denom * r.denom } ;
}

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

int main()
{
    fractional a{1,2} ;
    fractional b{1,3} ;

    auto c = add(a, b) ;
}

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

auto c = a + b ;

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

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

fractional operator +( fractional & l, fractional & r )
{
    // 分母が同じなら
    if ( l.denom == r.denom )
        // 単に分子を足す
        return fractional{ l.num + r.num, l.denom } ;
    else
        // 分母を合わせて分子を足す
        return fractional{ l.num * r.denom + r.num * l.denom, l.denom * r.denom } ;
}

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

auto c = a + b ;

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

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

fractional operator -( fractional & l, fractional & r ) ;
fractional operator *( fractional & l, fractional & r ) ;
fractional operator /( fractional & l, fractional & r ) ;

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

fractional operator -( fractional & l, fractional & r )
{
    // 分母が同じ
    if ( l.denom == r.denom )
        return fractional{ l.num - r.num, l.denom } ;
    else
        return fractional{ l.num * r.denom - r.num * l.denom, l.denom * r.denom } ;
}

掛け算と割り算は楽だ。

fractional operator *( fractional & l, fractional & r )
{
    return fractional{ l.num * r.num, l.denom * r.denom } ;
}

fractional operator /( fractional & l, fractional & r )
{
    return fractional{ l.num * r.denom, l.denom * r.num } ;
}

演算子のオーバーロード

二項演算子

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

a + b ;
a - b ;
a * b ;
a / b ;

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

struct S { } ;

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

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

struct S { } ;

int operator +( S, S ) { return 0 ; }
void operator -( S, S ) { }

int main()
{
    S s ;
    int x = s + s ;
    s - s ; // 戻り値はない
}

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

struct S { } ;

// S f( S, S )のようなもの
S operator + ( S, S ) { }

int main()
{
    S s ;
    // f(s,s)のようなもの
    operator +(s,s) ;
}

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

int operator +( int, int ) ;
int operator +( int, double ) ;

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

a + b ;

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

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

struct X { } ;
struct Y { } ;

void operator +( X, Y ) { }

int main()
{
    X x ;
    Y y ;

    // OK
    x + y ;

    // エラー
    // operator +(Y,X)は存在しない
    y + x ;
}

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

void operator +(Y,X) { }

も必要だ。

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

struct S { } ;

// 引数名はさまざま
S operator +( S const & left, S const & right )
{

}

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

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

int main()
{
    int x = 0 ;
    x = 1 ; // OK

    int const y = 0 ;
    y = 0 ; // エラー
}

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

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

int main()
{
    int a = 1 ;
    int b = 1 ;

    // a, bは書き換わらない
    int c = a + b ;
}

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

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

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

struct IntLike{ int data ;} ;

IntLike operator + ( IntLike const & l, IntLike const & r )
{
    return IntLike{ l.data + r.data }
}

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

単項演算子

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

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

int main()
{
    int x = 1 ;
    +x ; //  1: operator +
    -x ; // -1: operator -
}

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

struct IntLike{ int data ;} ;

IntLike operator +( IntLike const & obj )
{
    return obj ;
}

IntLIke operator -( IntLike const & obj )
{
    return IntLike{ -obj.data } ;
}

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

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

int main()
{
    int i = 0 ;
    ++i ;
    i++ ;

    --i ;
    i-- ;
}

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

int i = 0 ;
++i ;   // 1
i ;     // 1

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

int i = 0 ;
i++ ;   // 0
i ;     // 1

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

int i = 0 ;
++i = 0 ;   // iは0
++++i ;     // iは2

i++ = 0 ;   // エラー
i++++ ;     // エラー

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

struct IntLike { int data ; } ;

IntLike & operator ++( IntLike & obj )
{
    ++obj.data ;
    return obj ;
}
IntLike & operator --( IntLike & obj )
{
    --obj.data ;
    return obj ;
}

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

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

struct S { } ;

void operator ++( S const & s )
{
    std::cout << "increment!\n" ;
}

int main()
{

    S s ;
    ++s ;
}

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

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

struct IntLike { int data ; } ;

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

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

struct S { } ;

// 前置演算子
void operator ++( S ) ;
// 後置演算子
void operator ++( S, int ) ;

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

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

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

例えば、

S s ;
s + s ;

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

struct S { }
S operator + ( S const &, S const & ) ;

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

struct S
{
    S operator +( S const & right )
    {
        return S{} ;
    }
} ;

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

struct IntLike
{
    int data ;

    IntLike operator +( IntLike const & right )
    {
        return IntLike { data + right.data } ;
    }
} ;

int main()
{
    IntLike a(1) ;
    IntLike b(2) ;

    IntLike c = a + b ;
}

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

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

IntLike c = a.operator +( b ) ;

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

struct S
{
    void plus( S const & other ) { }
    void operator +( S const & other ) { }
} ;

int main()
{
    S a ;
    S b ;

    // これはメンバー関数呼び出し
    a.plus(b) ;
    // これもメンバー関数呼び出し
    a.operator +(b) ;
    // 同じくメンバー関数呼び出し
    a + b ;
}

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

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

std::array

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

int main()
{
    // int型の値を10個保持するクラス
    std::vector<int> v(10) ;

    // 0番目の値を1に
    v.at(0) = 1 ;

    // イテレーターを取る
    auto i = std::begin(v) ;
}

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

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

int main()
{
    // int型の値を10個保持するクラス
    std::array<int, 10> a ;

    // 0番目の値を1に
    a.at(0) = 1 ;

    // イテレーターを取る
    auto i = std::begin(a) ;
}

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

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

int main()
{
    std::size_t N{} ;
    std::cin >> N ;

    // 要素数N
    std::vector<int> v(N) ;
}

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

int main()
{
    std::size_t N{} ;
    std::cin >> N ;

    // エラー
    std::array< int, N > a ;
}

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

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

arraypush_backresizeも提供していない。

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

int main()
{
    std::array<int, 10> a ;

    // どちらも0番目の要素に1を代入
    a.at(0) = 1 ;
    a[0] = 1 ;

    // どちらも0番目の要素を標準出力
    std::cout << a.at(0) ;
    std::cout << a[0] ;
}

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

int main()
{
    // 10個の要素を持つ
    // 0番目から9番目までが妥当な範囲
    std::array<int, 10> a ;

    // エラー処理が行われる
    // プログラムは終了する
    a.at(10) = 0 ;
    // 何が起こるかわからない
    a[10] = 0 ;
}

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

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

// 常にゼロを返すクラス
// 何を書き込んでもゼロを返す
struct null_array
{
    int dummy ;
    // 引数は無視
    int & operator [] ( std::size_t )
    {
        dummy = 0 ;
        return dummy ;
    }
} ;

int main()
{
    null_array a ;

    // 0
    std::cout << a[0] ;
    // 0
    std::cout << a[999] ;

    a[100] = 0 ;
    // 0
    std::cout << a[100] ;
}

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

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

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

プログラマーの三大美徳

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

読者はプログラマーの三大美徳である、怠惰、短気、傲慢を会得すべきである。

第2版の巻末の用語集では、以下のような定義が与えらた。

怠惰

プログラマーは労力を削減するための労力を惜しまないこと。怠惰のために書いたプログラムは他人にも便利であり、そしてドキュメントを書くことにより自ら他人の質問に答えずに済むようにすること。これがプログラマーの第一の美徳である。これが本書の書かれた理由である。

短気

コンピューターが怠惰であるときにプログラマーが感ずる怒り。短気によって書かれたプログラムは、単に労力を削減するばかりではなく、事前に解決しておく。少なくとも、すでに解決済みのように振る舞う。これがプログラマーの第二の美徳である。

傲慢

ゼウスも罰したもう過剰なまでの驕り。他人がそしりを入れられぬほどのプログラムを書く推進剤。これがプログラマーの第三の美徳である。

これから学ぶarrayを実装するためのC++の機能を学ぶときに、このプログラマーの三大美徳のことを頭に入れておこう。

配列

ナイーブなarray実装

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

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

struct array_int_3
{
    int m0 ;
    int m1 ;
    int m2 ;
} ;

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

struct array_int_3
{
    int m0 ; int m1 ; int m2 ;

    int & operator []( std::size_t i )
    {
        switch(i)
        {
            case 0 :
                return m0 ;
            case 1 :
                return m1 ;
            case 2 :
                return m2 ;
            default :
                // 間違った引数
                // 強制終了
                std::abort() ;
        }
    }
} ;

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

配列

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

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

int a[10] ;

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

double b[5] ;

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

int main()
{
    std::size_t size ;
    std::cin >> size ;
    // エラー
    int a[size] ;
}

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

int a[5] = {1,2,3,4,5} ;
double b[3] = {1.0, 2.0, 3.0 } ;

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

int main()
{
    int a[5] = {1,2,3,4,5} ;

    // 4
    std::cout << a[3] ;

    a[2] = 0 ;
    // {1,2,0,4,5}
}

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

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

int main()
{
    auto print = [](auto s){ std::cout << s << "\n"s ; } ;
    int a[5] ;
    print( sizeof(a) ) ;
    print( sizeof(int) * 5 ) ;

    double b [5] ;
    print( sizeof(b) ) ;
    print( sizeof(double) * 5 ) ;
}

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

int main()
{
    auto print = [](auto s){ std::cout << s << "\n"s ; } ;
    std::array<int, 5> a ;

    // aのバイト数
    print( sizeof(a) ) ;
    // 要素数: 5
    print( a.size() ) ;

}

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

int a[5] ;

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

配列のストレージ上のイメージ図

□1つが1バイトのストレージ
     1番目のint
    |--|
□□□□□□□□□□□□□□□□□□□□
|--|            |--|
 0番目のint       4番目のint

fig/fig22-01.png

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

int main()
{
    int a[5] = {1,2,3,4,5} ;
    // エラー、コピーできない
    int b[5] = a ;

    std::array<int, 5> c = {1,2,3,4,5} ;
    // OK、コピーできる
    std::array<int, 5> d = c ;
}

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

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

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

struct array_int_10
{
    int storage[10] ;
} ;

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

struct array_int_3 { int storage [3] ; } ;

int main()
{
    array_int_3 a = { 0,1,2 } ;

    array_int_3 b = a ;
    // b.storage[0] == a.storage[0] 
    // b.storage[1] == a.storage[1] 
    // b.storage[2] == a.storage[2] 
}

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

struct array_int_3
{
    int storage[3] ;

    array_int_3( array_int_3 const & other )
    {
        std::copy(
            std::begin(other.storage), std::end(other.storage),
            std::begin(storage)
        ) ;

    }
}

operator []も実装しよう。

struct array_int_10
{
    int storage[10] ;

    int & operator [] ( std::size_t i )
    {
        return storage[i] ;
    }
} ;

int main()
{
    array_int_10 a = {0,1,2,3,4,5,6,7,8,9} ;
    a[3] = 0 ;
    std::cout << a[6] ;
}

std::arrayにはまださまざまなメンバーがある。1つずつ順番に学んでいこう。

テンプレート

問題点

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

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

// 標準ライブラリ
std::array<int, 10> a ;
// 我々のクラス
array_int_10 a ;

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

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

struct array_int_1
{
    int storage[1] ;
    int & operator []( std::size_t i )
    { return storage[i] ; }
} ;

// array_int_2, array_int_3, ...

struct array_int_10000
{
    int storage[10000] ;
    int & operator []( std::size_t i )
    { return storage[i] ; }
} ;

struct array_double_1
{
    double storage[1] ;
    double & operator []( std::size_t i )
    { return storage[i] ; }
} ;

// array_double_2, array_double_3, ...

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

関数の引数

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

int one_twice()
{
    return 1 * 2 ;
}

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

int two_twice()
{
    return 2 * 2 ;
}

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

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

int twice( int n )
{
    return n * 2 ;
}

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

関数のテンプレート引数

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

int twice( int n )
{
    return n * 2 ;
}

double twice( double n )
{
    return n * 2.0 ;
}

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

short twice( short n )
{
    return n * 2 ;
}

long twice( long n )
{
    return n * 2 ;
}

long long twice( long long n )
{
    return n * 2 ;
}

float twice( float n )
{
    return n * 2 ;
}

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

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

int twice( int n )
{
    return n * 2 ;
}

unsigned int twice( unsigned int n )
{
    return n * 2 ;
}

// short, long, long longに対しても同様

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

// 多倍長整数クラス
// unsigned long longが256個分の整数の実装
struct bigint
{
    unsigned long long storage[256] ;
} ;

bigint operator * ( bigint const & right, int )
{
    return // 実装
}

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

bigint twice( bigint n )
{
    return n * 2 ;
}

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

T twice( T n )
{
    return n * 2 ;
}

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

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

テンプレート

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

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

template < typename T >
    宣言 

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

template < typename T >
T twice( T n )
{
    return n * 2 ;
}

int main()
{
    twice( 123 ) ;  // int
    twice( 1.23 ) ; // double 
}

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

template < typename T >
T f( T n )
{
    T x = n ;
}

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

// テンプレートはテンプレート引数template_parameterを取る
template < typename template_parameter >
// 関数は引数function_parameterを取る
// 引数の型はtemplate_parameter
void f( template_parameter function_parameter )
{
}

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

template < typename T >
void f( T const & x )
{
    std::cout << x ;
}

int main()
{
    // Tはint
    f( 0 ) ;
    // Tはdouble
    f( 0.0 ) ;
    // Tはstd::string
    f( "hello"s ) ;
}

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

template < typename T >
void f( T const & x )
{
    std::cout << x ;
}

int main()
{
    // Tはint
    f<int>(0) ;

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

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

template < int N >
void f()
{
    std::cout << N ;
}

int main()
{
    // Nは0
    f<0>() ;
    // Nは123
    f<123>() ;
}

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

template < int N >
void f() { }

int main()
{
    // OK
    f<1+1>() ;

    int x{} ;
    std::cin >> x ;
    // エラー
    f<x>() ;
}

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

template < std::size_t N >
void f()
{
    int buffer[N] ;
}

int main()
{
    // 配列bufferのサイズは10
    f<10>() ;
    // サイズは12
    f<12>() ;
}

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

template < typename vec >
void f( vec & v )
{
    v.push_back(0) ;
}

int main()
{
    std::vector<int> a ;
    // OK
    f( a ) ;
    std::vector<double> b ;
    // OK
    // intからdoubleへの変換
    f( b ) ;

    std::vector<std::string> c ;
    // エラー
    // intからstd::stringに変換はできない
    f( c ) ;

    // エラー
    // int型はメンバー関数push_backを持っていない
    f( 0 ) ;
}

クラステンプレート

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

template < typename T > // テンプレート
void f( ) ; // 関数

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

template < typename T > // テンプレート
struct S { } ; // クラス

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

template < typename T, T N >
T value()
{
    return N :
}

int main()
{
    value<int, 1>() ;
    value<short, 1>() ;
}

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

template < typename T, std::size_t N >
struct array
{
    T storage[N] ;

    T & operator [] ( std::size_t i )
    {
        return storage[i] ;
    }
} ;

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

arrayをさらに実装

'std::array'をもっと実装していこう。前章では以下のような簡単な'array'を実装した。

template < typename T, std::size_t N >
struct array
{
    T storage[N] ;

    T & operator [] ( std::size_t i )
    {
        return storage[i] ;
    }
} ;

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

ネストされた型名

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

int main()
{
    using number = int ;
    number x = 123 ;
}

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

struct S
{
    using number = int ;
    number data ;
} ;

int main()
{
    S s{123} ;

    S::number x = s.data ;
}

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

template < typename T, std::size_t N >
struct array
{
    using value_type = T ;
    using reference = T & ;

    using size_type = std::size_t ;

    value_type storage[N] ;

    reference operator [] ( size_type i )
    {
        return storage[i] ;
    }
} ;

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

int main()
{
    using array_type = std::array<int, 5> ;
    array_type a = {1,2,3,4,5} ;
    array_type::value_type x = 0 ;
    array_type::reference ref = a[0] ;
}

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

int main()
{
    using array_type = std::array<int, 5> ;
    array_type a = {1,2,3,4,5} ;
    auto x = 0 ;
    auto ref = a[0] ;
}

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

要素数の取得: size()

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

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

int main()
{
    std::array<int, 5> a ;
    a.size() ; // 5

    std::array<int, 10> b ;
    b.size() ; // 10
}

さっそく実装しよう。

template < typename T, std::size_t N >
struct array
{
    using size_type = std::size_t ;

    size_type size() ;
    // ... 省略
} ;

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

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

// 関数の宣言
void f() ;
// 関数の定義
void f() { }

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

// クラスの宣言
struct S
{
    // メンバー関数の宣言
    void f() ;
} ;

// メンバー関数の定義
void S::f() { }

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

メンバー関数のconst修飾

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

int main()
{
    int x = 0 ;
    x = 1 ;
    int const cx = 0 ;
    cx = 0 ; // エラー
}

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

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

template < typename Array >
void print( Array & c )
{
    for ( std::size_t i = 0 ; i != c.size() ; ++i )
    {
        std::cout << c[i] ;
    }
}

int main()
{
    std::array<int, 5> a = {1,2,3,4,5} ;
    print( a ) ;
}

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

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

template < typename Container >
void print( Container const & c )
{
    for ( std::size_t i = 0 ; i != c.size() ; ++i )
    {
        std::cout << c[i] ;
    }
}

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

int main()
{
    array<int, 5> a = {1,2,3,4,5} ;

    print( a ) ; // エラー
}

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

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

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

struct S
{
    int data {} ;
    void f()
    {
        ++data ;
    }
} ;

int main()
{
    S s ;
    s.f() ; // s.dataを変更
}

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

int main()
{
    S s ;
    S const & ref = s ;

    ++ref.data ;  // エラー
    ref.f() ;     // エラー
}

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

struct S
{
    int data {} ;
    void f()
    {
        // 何もしない
    }
} ;

int main()
{
    S const s ;
    s.f() ; // エラー
}

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

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

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

struct S
{
    void f() const
    { }
} ;

int main()
{
    S s ;
    s.f() ; // OK

    S const cs ;
    cs.f() ; // OK

}

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

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

struct S
{
    void f() { }        // 1
    void f() const { }  // 2
} ;

int main()
{
    S s ;
    s.f() ;     // 1

    S const cs ;
    cs.f() ;    // 2
}

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

struct S
{
    int data {} ;
    // データメンバーへのリファレンスを返す
    int & get()
    {
        return data ;
    }
} ;

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

struct S
{
    int data {} ;
    int & get()
    {
        return data ;
    }

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

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

template < typename T, std::size_t N >
struct array
{
    T storage[N] ;

    using reference = T & ;
    using const_reference = T const & ;

    // 非const版
    reference operator [] ( std::size_t i )
    {
        return storage[i] ;
    }
    // const版
    const_reference operator [] ( std::size_t i ) const
    {
        return storage[i] ;
    }
} ;

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

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

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

int main()
{
    std::array<int, 5> a = {1,2,3,4,5} ;

    int & f = a.front() ;   // 1
    int & b = a.back() ;    // 5
}

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

template < typename T, std::size_t N >
struct array
{
    T storage[N] ;

    using reference = T & ;
    using const_reference = T const & ;

    reference front()
    { return storage[0] ; }
    const_reference front() const
    { return storage[0] ; }

    reference back()
    { return storage[N-1] ; }
    const_reference back() const
    { return storage[N-1] ; }

} ;

全要素に値を代入: fill

int main()
{
    std::array<int, 5> a = {1,2,3,4,5} ;
    a.fill(0) ;
    // aは{0,0,0,0,0}
}

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

template < typename T, std::size_t N >
struct array
{
    T storage[N] ;

    void fill( T const & u )
    {
        for ( std::size_t i = 0 ; i != N ; ++i )
        {
            storage[i] = u ;
        }
    }

} ;

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

void fill( T const & u )
{
    std::fill( begin(), end(), u ) ;
}

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

arrayのイテレーター

イテレーターの中身

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

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

int main()
{
    std::array<int, 5> a = {1,2,3,4,5} ;

    auto first = std::begin(a) ;
    auto last = std::end(a) ;
}

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

namespace std
{
    template < typename C >
    auto begin( C & c )
    { return c.begin() ; }

    template < typename C >
    auto begin( C const & c )
    { return c.begin() ; }

    template < typename C >
    auto end( C & c )
    { return c.end() ;}

    template < typename C >
    auto end( C const & c )
    { return c.end() ;}
}

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

さっそく確かめてみよう。

int main()
{
    std::array<int, 5> a = {1,2,3,4,5} ;

    auto iter = a.begin() ;
    std::cout << *iter ; // 1
    ++iter ;
    std::cout << *iter ; // 2
}

確かに動くようだ。

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

// イテレーターを表現するクラス
struct array_iterator { } ;

template < typename T, std::size_t N >
struct array
{
    // イテレーター型
    using iterator = array_iterator ;

    // イテレーターを返すメンバー関数
    iterator begin() ;
    iterator end() ;

    // その他のメンバー
} ;

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

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

int main()
{
    std::array<int, 5> a = {1,2,3,4,5} ;

    auto iter = a.begin() ;
    int x = *iter ; // 1
    *iter = 0 ;
    // aは{0,2,3,4,5}
}

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

array<int, 5> a = {1,2,3,4,5} ;
int x = a[0] ; // 1
a[0] = 0 ;

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

struct array_iterator_int_5_begin
{
    array<int, 5> & a ;

    array<int, 5>::reference operator *()
    {
        return a[0] ;
    }
} ;

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

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

template < typename Array >
struct array_iterator_begin
{
    Array & a ;

    array_iterator_begin( Array & a )
        : a( a ) { }

    // エラー
    // Array::referenceは型ではない
    Array::reference operator *()
    {
        return a[0] ;
    }
} ;

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

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

template < typename T >
void f()
{
    // typenameが必要
    typename T::Y x = 0 ;
}

struct S
{
    using Y = int ;
} ;

int main()
{
    // T = S
    // T::Y = int
    f<S>() ;
}

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

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

typename Array::reference
array_iterator_begin::operator * ()
{
    return a[0] ;
}

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

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

template < typename T, std::size_t N >
struct array
{
    T storage[N] ;
    // 黒魔術1: array
    using iterator = array_iterator_begin<array> ;
    iterator begin()
    // 黒魔術2: *this
    // 黒魔術3: iterator(*this)
    { return iterator(*this) ; }
} ;

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

template < typename A, typename B, typename C >
struct S
{
    void f()
    {
        // S<A,B,C>と同じ
        S s ;
    }
} ;

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

struct S
{
    int data {} ;
    // *thisはメンバー関数が呼ばれたSのオブジェクト
    S & THIS() { return *this ; } 
} ;

int main()
{
    S s1 ;
    
    s1.THIS().data = 123 ;
    // 123
    std::cout << s1.data ;

    S s2 ;
    s2.THIS().data = 456 ;
    // 456
    std::cout << s2.data ;
}

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

struct S
{
    int data {} ;
    void set(int x)
    {
        data = x ;
    }
} ;

int main()
{
    S object ;
    object.set(42) ;
}

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

struct S
{
    int data {} ;
} ;

void set( S & object, int x )
{
    object.data = x ;
}

int main()
{
    S ojbect ;
    set( object, 42 ) ;
}

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

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

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

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

struct S
{
    S() { }
    S( int ) { }
    S( int, int ) { }
} ;

int main()
{
    S a = S() ;
    S b = S(0) ;
    S c = S(1,2) ;

    S d = S{} ;
    S e = S{0} ;
    S f = S{1,2} ;
}

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

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

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

int main()
{
    std::array<int, 5> a = {1,2,3,4,5} ;

    auto iter = a.begin() ;
    *iter ; // 1
    ++iter ;
    *iter ; // 2
    --iter ;
    *iter ; // 1
}

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

*++iter ;
*++++iter ;

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

template < typename Array >
struct array_iterator
{
    Array & a ;

    array_iterator( Array & a )
        : a( a ) { }

    // 次の要素を指す
    array_iterator &