C言語のswitchにbreakが必要な理由

LWP | C言語のswitchにbreakが必要な理由

LWP TECHNICAL ARTICLE | 174

C言語のswitchにbreakが必要な理由

fall-throughを「設計ミス」で片づけず、caseの構造とBCPLからの歴史で読み解く

Copyright © 2026 LWP 山中 一弘

本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。

記事要約

C言語のswitchは、breakを書かなければ次のcaseへ進みます。現代の感覚では、各caseが独立した分岐ブロックであり、処理が終われば自動的にswitchを抜ける方が自然に見えます。しかし初期Cのcaseは、処理の終端を表すブロックではなく、実行開始位置を示すラベルです。一致したラベルへ制御を移した後は、明示的に止めない限り、通常の逐次実行が続きます。

この仕様は、Cで白紙から発明されたものでも、熟練者向けの技巧を優先して単純に選ばれたものでもありません。Dennis Ritchieの言語史によれば、Bell Labsが学んだ初期BCPLには後のendcaseがなく、BとCではbreakをswitchの脱出にも使う進化が生じました。一方、Brian Kernighanは1974年のCチュートリアルで、fall-throughには複数のcaseをまとめられる利点と、break忘れを招く欠点があると整理しています。

結論は、Cのfall-throughは「歴史的に継承されたラベル構造」と「実用上の表現力」が重なって定着した仕様だということです。現在のコードでは、意図しないfall-throughを避け、必要な場合だけコメントやC23の[[fallthrough]]で意図を明示するのが安全です。

本記事の対象とゴール

想定読者

  • C言語のswitchで、なぜ各caseにbreakが必要なのか疑問を持った方

  • fall-throughを単なる古い仕様ではなく、制御構造として理解したい方

  • BCPL、B、Cのつながりから言語仕様の成り立ちを学びたい方

本記事で得られること

  • caseがブロックではなくラベルであることを、実行順序から説明できます。

  • CのbreakがBCPL、B、Cの歴史の中で定着した経緯を理解できます。

  • 意図的なfall-throughとbreak忘れを区別し、現代のCで安全に書く判断軸を持てます。

目次

  • はじめに なぜbreakが必要なのか

  • 第1章 caseは処理ブロックではなくラベルである

  • 第2章 BCPLからB、Cへ受け継がれた構造

  • 第3章 fall-throughが持つ二つの利点

  • 第4章 最大の欠点は意図が読めないこと

  • 第5章 現代のCでは意図を明示する

  • 第6章 言語仕様を歴史と構造の両方から読む

  • まとめ breakは歴史と設計の境界にある

はじめに なぜbreakが必要なのか

典型的なCのswitchは、次のように書きます。

switch (x) {
    case 1:
        foo();
        break;
 
    case 2:
        bar();
        break;
 
    default:
        baz();
        break;
}

ここで素朴な疑問が生じます。case 1の処理が終わったのなら、なぜ自動的にswitchを抜けないのでしょうか。

実際、breakを削ると挙動が変わります。

switch (x) {
    case 1:
        foo();
 
    case 2:
        bar();
        break;
}

x == 1なら、foo()の後にbar()も実行されます。これがfall-throughです。

この動きを理解する第一歩は、「fall-throughという特別な命令がある」と考えないことです。Cでは、一致したcaseから通常の逐次実行が始まり、breakがその流れをswitchの外へ移します。

第1章 caseは処理ブロックではなくラベルである

1.1 caseが決めるのは開始位置である

現代のプログラマは、caseを独立した分岐ブロックとして読みがちです。しかし初期Cの仕様では、caseとdefaultは文に付ける接頭ラベルです。

Dennis Ritchieの初期Cリファレンスマニュアルは、caseまたはdefaultの接頭辞そのものは制御の流れを変えないと説明しています。switch式と一致したcaseが見つかると、そのラベルに続く文へ制御が移ります。そこから先は、通常の文と同じように上から下へ実行されます。

switch (x) {
    case 1:
        A();
 
    case 2:
        B();
 
    case 3:
        C();
}

x == 2なら、概念的な実行順序は次のようになります。

case 2へ制御を移す
↓
B()を実行する
↓
次の文であるC()を実行する
↓
switchの末尾に到達する

次のcaseに到達したから特別に「落ちる」のではありません。ラベルは実行中の流れを止めないため、次の文へ進んだ結果として次のcaseを通過します。

1.2 特別な制御移動を行うのはbreakである

この構造では、breakの役割が明確になります。

case
  一致したときの開始位置を示す
 
break
  最も内側のswitchまたは反復文を終了する

したがって、Cのswitchは「各分岐が自動終了し、必要なら継続する」構造ではありません。「一致したラベルから実行を開始し、必要なら明示的に脱出する」構造です。

第2章 BCPLからB、Cへ受け継がれた構造

2.1 Cで突然発明された仕様ではない

Cの直接の祖先はBであり、BはBCPLから強い影響を受けています。

BCPL
Martin Richards
↓
B
Ken Thompson
↓
C
Dennis Ritchie

Brian KernighanのB言語チュートリアルには、すでにswitch、case、breakが登場します。そこでは、一致したcaseから実行が始まり、別のcaseへfall-throughでき、breakがswitchの直後へ制御を移すと説明されています。

つまり、Cのfall-throughを理解するには、Cだけを見て「なぜこの設計を選んだのか」と問うだけでは足りません。Cが受け継いだBの構造まで戻る必要があります。

2.2 初期BCPLには後のendcaseがなかった

Ritchieは『The Development of the C Language』で、Bell Labsの開発者が1960年代に学んだ初期BCPLには、後の版にあるendcaseがまだ存在しなかったと説明しています。その結果、BとCではbreakをswitchからの脱出にも使うようになりました。

Ritchieは、この違いを「意図的変更というより分岐した進化」と表現しています。これは、設計者が自動終了とfall-throughを白紙から比較し、fall-throughの方が優れていると結論した、という単純な歴史ではないことを示します。

Bell Labsが学んだ初期BCPL
  endcaseがまだない
↓
B
  caseはラベル
  breakでswitchを抜ける
↓
C
  基本構造を継承する

2.3 小さな言語と単純な実装という背景

Ritchieは、BCPL、B、Cを、システムプログラミング向けで、小さく記述でき、単純なコンパイラで翻訳しやすい言語群として説明しています。また、Bのコンパイラ設計にはメモリ制約が強く影響していました。

ただし、ここから「メモリを節約するためにfall-throughを採用した」と断定してはいけません。一次資料が直接述べているのは、初期BCPLにendcaseがなかったことと、B/Cのbreakが分岐した進化から生じたことです。小さな実装環境は言語全体の背景であり、fall-through単独の採用理由ではありません。

第3章 fall-throughが持つ二つの利点

3.1 複数の条件を一つの処理へまとめる

fall-throughの分かりやすい利点は、複数のcaseを同じ処理へまとめられることです。

switch (c) {
    case 'a':
    case 'A':
        handle_a();
        break;
}

このコードは、cが小文字のaでも大文字のAでも同じ処理を実行します。Kernighanは1974年のCチュートリアルで、まさに大文字と小文字を同じ処理へまとめる例をfall-throughの利点として示しています。

ここでは、最初のcaseに実行文がありません。二つのラベルが同じ文を指していると理解すると自然です。

3.2 複数の入口から共通処理へ合流する

もう一つの利点は、処理の途中から共通部分へ合流できることです。

switch (x) {
    case 1:
        special_for_1();
        /* case 2の共通処理へ続ける */
 
    case 2:
        common_for_1_and_2();
        break;
}

x == 1なら固有処理の後に共通処理を実行し、x == 2なら共通処理から始めます。Bのチュートリアルにも、複数の文字を同じ処理へまとめたり、ある操作から別の共通処理へ続けたりする実例があります。

ただし、共通処理が長い場合や再利用範囲が広い場合は、別関数へ切り出した方が読みやすくなることがあります。fall-throughは、重複を減らせるから常に優れているのではなく、局所的な合流を簡潔に表現できる選択肢です。

第4章 最大の欠点は意図が読めないこと

4.1 意図的な継続とbreak忘れが同じ形になる

次のコードだけを見て、作者の意図を確定できるでしょうか。

switch (x) {
    case 1:
        foo();
 
    case 2:
        bar();
        break;
}

考えられる意図は二つあります。

  • foo()の後にbar()も実行したい。

  • foo()の後で抜けるつもりだったが、breakを書き忘れた。

コードの見た目だけでは区別しにくい点が、fall-throughの最大の欠点です。Kernighanがこの仕様を、利点と欠点の両方を持つものと評価した理由もここにあります。

4.2 規模が大きくなるほど曖昧さの費用が増える

小さなプログラムでは、直後の数行を読めば意図を推測できるかもしれません。しかし、開発者が増え、保守期間が長くなり、静的解析やコードレビューを使う環境では、暗黙の意図は障害になります。

観点 暗黙のfall-throughで起きる問題
コードレビュー 意図的か欠落かを確認する必要がある
静的解析 警告すべき箇所と許容すべき箇所を判別しにくい
保守 後からcaseを追加したとき、実行範囲が変わりやすい
テスト 一見もっともらしい副作用が出て、発見が遅れることがある

仕様として表現力があることと、日常のコードで積極的に使うべきことは別問題です。

第5章 現代のCでは意図を明示する

5.1 通常はbreakまたはreturnで終了させる

各caseを独立した分岐として使うなら、break、return、その他の明確な制御移動で終了させます。

switch (command) {
    case CMD_START:
        start();
        break;
 
    case CMD_STOP:
        stop();
        break;
 
    default:
        report_unknown(command);
        break;
}

この形では、読者は各caseを独立して追えます。

5.2 意図的なfall-throughは注記する

意図的に次のcaseへ続けるなら、少なくともコメントで意図を示します。

switch (x) {
    case 1:
        special_for_1();
        /* fall through: case 2の共通処理も実行する */
 
    case 2:
        common_for_1_and_2();
        break;
}

C23では、意図的なfall-throughを表す標準属性[[fallthrough]]を使えます。

switch (x) {
    case 1:
        special_for_1();
        [[fallthrough]];
 
    case 2:
        common_for_1_and_2();
        break;
}

利用するコンパイラとC言語モードがC23の該当機能に対応しているかは確認が必要です。旧規格や既存コンパイラでは、処理系固有の属性や、静的解析が認識するコメントを使う場合があります。

5.3 まず重複ラベル、次に関数分割を検討する

意図的なfall-throughには、用途ごとに読みやすい代替があります。

  • 同じ処理をするだけなら、複数のcaseラベルを一つの文へ重ねます。

  • 共通処理が長いなら、関数へ切り出します。

  • 前段の処理を積み上げる必要があるときだけ、明示的なfall-throughを検討します。

「書けるか」ではなく、「読者が意図を一意に判断できるか」を基準にします。

第6章 言語仕様を歴史と構造の両方から読む

6.1 歴史だけでも、合理性だけでも説明しきれない

Cのfall-throughを「昔の設計ミス」とだけ呼ぶと、caseがラベルである構造と、複数入口をまとめられる利点を見落とします。逆に「柔軟で合理的な機能」とだけ評価すると、Ritchieが述べた歴史的経緯と、break忘れの危険を見落とします。

より正確な理解は、次の三段階です。

歴史的にラベル構造が受け継がれた
↓
実用上の利点が見いだされ、使われた
↓
大規模開発では曖昧さが欠点として強くなった

6.2 デフォルトの選び方は時代の価値観を映す

Cでは、通常は逐次実行が続き、終了したいときにbreakを明示します。より新しい設計では、通常は分岐ごとに終了し、続けたいときだけfall-throughを明示する方式が多く見られます。

違いは、機能の有無ではなく、どちらを既定にするかです。

Cの基本モデル
  既定:次の文へ進む
  例外:breakで抜ける
 
安全性を優先するモデル
  既定:分岐を終了する
  例外:継続を明示する

言語仕様は純粋な合理性だけで決まりません。祖先言語、既存実装、利用慣習、計算資源、保守規模などを引き継ぎながら形作られます。Cのswitchは、そのことを学ぶよい教材です。

まとめ breakは歴史と設計の境界にある

  • Cのcaseは独立した処理ブロックではなく、実行開始位置を示すラベルです。

  • 一致したcaseから通常の逐次実行が始まるため、止めなければ次のcaseへ進みます。

  • breakはfall-throughを無効にする飾りではなく、switchの外へ制御を移す文です。

  • BとCのbreakは、Bell Labsが学んだ初期BCPLに後のendcaseがなかった歴史と結び付いています。

  • fall-throughには複数条件の統合や共通処理への合流という利点があります。

  • 最大の欠点は、意図的な継続とbreak忘れが同じ形に見えることです。

  • 現代のCでは、通常は明確に終了し、必要なfall-throughだけ意図を注記します。

switchのbreakは、単なる古い文法上の癖ではありません。ラベルとしての構造、言語の系譜、そして安全性に対する価値観の変化が一か所に表れた仕組みです。

出典メモ

  • 元原稿: C言語_switch_break_fallthrough_歴史と設計_記事元ネタ.md

  • Dennis M. Ritchie, The Development of the C Language, Nokia Bell Labs: https://www.nokia.com/bell-labs/about/dennis-m-ritchie/chist.html

  • Brian W. Kernighan, Programming in C: A Tutorial (1974): https://www.lysator.liu.se/c/bwk-tutor.html

  • Brian W. Kernighan, A Tutorial Introduction to the Language B: https://www.bell-labs.com/usr/dmr/www/btut.pdf

  • Dennis M. Ritchie, C Reference Manual: https://www.bell-labs.com/usr/dmr/www/cman.pdf

  • ISO/IEC JTC1/SC22/WG14, C言語標準化作業部会とC23の案内: https://www.open-std.org/jtc1/sc22/wg14/

  • WG14 N2268, The fallthrough attribute: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2268.pdf

  • 取得・再確認日: 2026年8月17日

  • 資料中の短い英語表現は論点の特定に必要な範囲だけ示し、本文は日本語で要約した。

  • 「小さな実装環境がfall-through単独の採用理由だった」とは断定せず、言語全体の背景と直接の歴史的説明を分けた。