コンテンツへスキップ
Topos Uranos

要約

文を順に並べるだけのプログラムは、データがどうであれ、いつも同じことをする。データに応じて動くには、判断が必要である。このレッスンでは C++ の分岐を学ぶ。条件に応じて二つの道から一つを選ぶ if と else、いくつもの道から一つを選び、条件の順序によって各枝がどの場合を受け持つかが決まる else if の連鎖、入れ子の分岐とぶら下がり else の罠、そして名前のついた離散的な値から一つを選ぶ switch と enum class であり、後者ではある場合が抜けていたり、ある場合がそのあとの場合へ落ちたりするとコンパイラが警告する。これらすべてをまとめる概念は場合分けである。正しい分岐は入力の集合を、互いに重ならず全体を覆う場合に分け、各場合はその内部の一つのテストと、その各境界での一つのテストで確かめられる。

学習目標

  1. 仕様のすべての場合を受け持つ、単純な分岐、連鎖した分岐、入れ子の分岐を書く。
  2. if と else if の連鎖の場合が互いに重ならず網羅的であることを確かめ、場合ごとに一つのテスト入力を設計する。
  3. 離散的な値に対して enum class を使う switch を用い、場合が抜けているときにコンパイラが何を警告するかを説明する。
  4. ぶら下がり else と switch の場合間のフォールスルーを見つけ、修正する。

if と else

分岐の形

レッスン 問題からアルゴリズムへ は、あらゆるアルゴリズムを組み立てる三つの形、すなわち順次、分岐、反復を紹介した。ここまでのコースのプログラムは、第 1 単元で先取りして使ったいくつかの if を除けば、順次であった。このレッスンでは二つ目の形を C++ で書く。分岐は条件、つまりレッスン 関係と論理 で扱ったような bool 型の式を計算し、条件が真ならあるブロックを、偽なら別のブロックを実行する。

C++ のコード
if (条件) {    // 条件が真のときに実行される} else {    // 条件が偽のときに実行される}

条件は必ず丸括弧で囲む。else の部分は省略できる。省略すると、条件が偽のときには何もせず、プログラムは分岐のあとの文へ進む。どちらの場合も二つの道は最後に合流するので、分岐は外から見れば、入口と出口を一つずつもつ一つの文であり、レッスン 1.1 が求めたとおりである。

例ユークリッド除法の余り

レッスン 算術演算 は、a % b が被除数の符号をもつので、a=−7a = -7、b=2b = 2 のときその値は −1-1 になる一方、決して負にならないユークリッド除法の余りは 1 であることを示し、安全な直し方は余りが負のときにだけ bb を足すことだと予告した。プログラムは b>0b > 0 を仮定する。

C++ のプログラム
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    int r{a % b};    if (r < 0) {        r = r + b;    }    std::println("{} を {} で割ったユークリッド除法の余り:{}", a, b, r);}
プログラムへの入力
-7 2
出力
-7 を 2 で割ったユークリッド除法の余り:1

分岐が値を変えうるので、変数 r はもう定数ではない。図は二つの可能な道をたどる。

証明

  1. 実行は r がすでに計算された状態で分岐に着き、まず条件 r < 0 を計算する。ひし形は、道が分かれるプログラム中のただ一つの点である。

  2. 条件が真なら、たとえば −7-7 と 2 のときのように、if のブロックが実行され、r は −1-1 から 1 に変わる。

  3. 偽なら、たとえば 7 と 2 のときのように、ブロックは丸ごと飛ばされ、r は値を保つ。

  4. 二つの道は同じ点で合流し、そこから先、プログラムはどちらの場合も同じように進む。結果を書く文は必ず実行される。

else があると、分岐は互いに排他的な二つの動作から一つを選ぶ。レッスン 関係と論理 のうるう年の条件は bool を生み、分岐がそれをプログラムのふるまいに変える。

C++ のプログラム
#include <iostream>#include <print>​int main(){    int year{};    std::cin >> year;    const bool isLeap{(year % 4 == 0 && year % 100 != 0) || year % 400 == 0};    if (isLeap) {        std::println("{} 年はうるう年で、2 月は 29 日ある。", year);    } else {        std::println("{} 年はうるう年ではなく、2 月は 28 日ある。", year);    }}
プログラムへの入力
1900
出力
1900 年はうるう年ではなく、2 月は 28 日ある。

isLeap のように条件に名前をつけると、何を判断するかと、判断で何をするかが分けられる。

波括弧はいつも

C++ の文法では、ブロックの文が一つだけなら波括弧を省略できる。if (r < 0) r = r + b; は正しい。しかし波括弧がないと、分岐が支配するのは直後の一文だけであり、読み手が支配されていると思う範囲はインデントで決まるが、コンパイラはインデントを無視する。そうであれば、あとからの変更(二つ目の文を足す、一つ目をコメントにする)は、テキストにそれが表れないまま意味を変えうる。ここからコースの規則が導かれる。if や else のブロックには、文が一つだけでも必ず波括弧をつける。

あらゆる誤りの中で最も短いものが、この規則を裏づける。丸括弧のあとのセミコロンは空文であり、分岐はその空文を支配する。それに続くブロックはもう if に属さず、必ず実行される。

C++ のプログラム
#include <print>​int main(){    const int stock{12};    if (stock == 0);    {        std::println("在庫切れ");    }    std::println("在庫数:{}", stock);}
コンパイラの警告
main.cpp: In function ‘int main()’:main.cpp:6:20: warning: suggest braces around empty body in an ‘if’ statement [-Wempty-body]    6 |     if (stock == 0);      |                    ^main.cpp:6:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]    6 |     if (stock == 0);      |     ^~main.cpp:7:5: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’    7 |     {      |     ^
出力
在庫切れ在庫数:12

まだ 12 個残っているのに、プログラムは在庫切れを告げる。GCC は二つの警告を出す。-Wempty-body は空の本体を波括弧で囲むよう勧め、-Wmisleading-indentation は、ブロックがあたかも if に支配されているかのようにインデントされていると指摘する。もう一つの古典的な誤り、if (x == 0) のつもりの if (x = 0) は、レッスン コンパイル・実行・設定 で見た。代入の値は 0 で、それが false に変換され、-Wparentheses が警告する。どちらの場合もプログラムはコンパイルできる。警告がなければ、誤りは気づかれずに終わる。

波括弧にはもう一つの帰結がある。if のブロックの中で宣言した変数は、そこにしか存在しない。ある枝で計算した値を分岐のあとで使うなら、余りの例の r のように、変数を分岐の前で宣言する。スコープはレッスン 引数とスコープ で学ぶ。

条件演算子

分岐の二つの枝が値を選ぶだけのとき、C++ にはもっと短い形がある。条件 ? a : b は、条件が真なら a、偽なら b の値をとり、選ばれた要素だけを計算する。文である if と違い、条件演算子は式である。値と型をもち、どんな式でも置ける場所に置ける。たとえば定数の初期化であり、これは if では const をあきらめなければできない。

C++ のプログラム
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    const int larger{a > b ? a : b};    std::println("大きいほう:{}", larger);    std::println("{} {}", b, b == 1 ? "単位のみ" : "単位");    const auto mixed{a > b ? a : 0.5};    std::println("共通の型:{:.1f}", mixed);    std::println("括弧なし:{}", 10 + a > b ? 1 : 0);    std::println("括弧あり:{}", 10 + (a > b ? 1 : 0));}
プログラムへの入力
7 3
出力
大きいほう:73 単位共通の型:7.0括弧なし:1括弧あり:11

最初の二行は、コードをわかりやすくする使い方を示す。大きいほうを選ぶことと、数に応じて語句を選ぶことであり、文全体を二つの枝で繰り返さずにすむ。三行目は、どちらの枝が選ばれても式の型は一つだけであることを示す。二つの結果の型が異なれば、レッスン 算術演算 の通常の算術変換によって共通の型に変換される。ここでは int と double から double が得られ、選ばれた 7 は 7.0 として保存される。最後の二行は優先順位を示す。条件演算子は優先順位の最も低い演算子の一つなので、括弧がなければ条件は 10 + a > b となり、値は 11 ではなく 1 になる。条件演算子がより大きな式の一部になるときは、括弧で囲む。

値を選ぶ以上のことを求めたとたんに、この演算子はコードを見えにくくする。a > b ? (a > c ? a : c) : (b > c ? b : c) のように入れ子にすると、読みやすくする波括弧のないまま、入れ子の分岐を頭の中で組み立て直させることになる。ok ? std::println("はい") : std::println("いいえ") のように副作用のために使えば、それは変装した if である。コースの規則は、二つの短い値から選ぶためだけに、入れ子にせずに使うことであり、それ以外はすべて if で書く。