C++ 界隈では illegal/ill-formed/un○○ behavior だとか怖い単語が色々ありますよね。それらの関係について規格に潜水調査して参りました。でもまだ謎が残っているんです。謎解きに挑戦しませんか?
- この記事は [C++ Advent Calendar 2016] 24日目の記事です。そうです遅刻ですすみません
- 23日目は [メモリ領域とnew演算子の有無について – ろっさむ.com] でした。
- 25日目は [C++でStreamライブラリを書いた話 - Qiita] です。
要旨
C++ 警察1は色々な "脅し文句" を持っています。挙げてみましょう:
conforming (適合), well-formed program (適格プログラム), ill-formed program (不適格プログラム), undefined behavior (UB, 未定義の動作), unspecified behavior (未規定の動作), implementation-defined behavior (処理系定義の動作), locale-specific behavior (文化圏固有動作), observable behavior (外から見た動作), syntactic rules (構文規則), semantic rules (意味規則), diagnosable rules (診断対象規則), one definition rule (ODR, 単一定義規則), no diagnostics required (NDR, 診断を必要としない), conditionally-supported constructs (日本語不明), …
ナンダカヨクワカラナイこれらの単語を連呼されると (たとえ自分がそれらに抵触していなくても) 何か悪いことをした気分になってホイホイと任意同行に応じてしまいます。でも…C++ の法律規格をちゃんと知っていれば「規格の節○×・段落△□によれば ~ ウンタラカンタラ ~ 該当しません。それに "任意" 同行デスヨネ」とかなんとか言って逃れることができる…かもしれません。あるいは、心証を悪くして余計に目をつけられるかもしれません。
という訳でこれらの法律用語規格用語の関係を一目瞭然に見られる図を作り、それを眺めて C++ 警察に備えましょう2というのがこの記事の目的です。基本的に C++17 の原稿 N4618 を元にします。C言語の規格を参照する時は N1570 を参照します。それで作った図がこれです。
この図を中心にそれぞれの概念と解釈について駄文を連ねて行こうかなと思っています。
それから、×印が2つ見えますよね。これは、お宝の在処だとか、C++ 警察から身を潜めるためのアジトの場所だとか、そういうありがたい物ではありません。といって交番の地図記号でもありません。今回この地図を作る上で残ってしまった2つの謎の在処です。この謎についても最後に説明する予定です。
(ちょっと誰か謎の答えを教えて欲しいです)
謎についてお答えくださる方は間の駄文は飛ばして §11 に御出動下さいませ。
1. 何故 C++er は違法だとか未定義の動作だとかが好きなのか
プログラムの書き方は人それぞれです。他の人の書いたコードを見てなんかセンスが無いなあと思っても、人それぞれ事情もあるでしょうから《かくかくしかじかに書くべき》の様な押し付けはなかなかできません。
つきつめて行けば、畢竟、プログラムなんていうものは現実的な範囲で動けば良いのです。可搬でない書き方や間抜けな書き方をしていても書き捨てのプログラムなのかもしれません。間抜けなアルゴリズムを採用していたとしても速度を重視しないプログラムなのかもしれません。小難しい書き方で可読性や保守性を蹂躙していたとしても速度重視で何かトリックを使っているのかもしれません。とはいいつつ、往々にして変なプログラムは後々問題を起こす訳ですが、少なくとも問題を起こさず要求された仕事を正しくこなしている内はなかなか文句も言いづらいのです。
現に、その辺の OSS を見ると綺麗なコードもありますが、ヘンテコなコードも沢山あります。というかヘンテコなコードの方が多いです。でもその背景には人的・時間的資源の問題による妥協など諸々あるのだろうと思えば一から書き直せとは要求しにくいのです。また、折角動いているコードを大幅に書き換えると新しくバグを埋め込む可能性もあります。そんなこんなで我々にはもやもやと鬱憤が溜まっているのです。
その澱んだ鬱憤に行き先はあるのでしょうか。それがあるのです。C++ の規則を破っているコードや、(規則を破っていなくても) 未定義の動作を引き起こす可能性を孕んでいるコードです。普段はプログラムの書き方は人それぞれなので余り強く出られなくても、これらのコードに対しては我々は強力な鈍器 (C++ の規格) を持っているので強気で行けます。その刹那、C++er は恰も警察官か、はたまた裁判官か、法の番人となって、コードに鬱憤を叩き込むのです。
それを言うなら規格のある他の言語も同じなのでは? とお考えかもしれません。でも、よく考えてみて下さい。多くの言語では、規則を破っているコードだとか未定義の動作を起こすコードだとかが、叩いてくれと言わんばかりにその辺に漂っていたりはしません。普通は規則を破っているコードはコンパイルできないし、また未定義の動作も起こるケースが非常に限定されている (もしくは存在しない) からです。しかし C/C++ は事情が違います。C++ の規格には この規則は違反しても処理系がエラーや警告を出しても出さなくても良い (NDR・診断不要) だとか、処理系がいかなる動作をしても良い (UB・未定義動作) だとかいう変な項目が大量にあります。このせいで、怪しい C++ プログラムでも (或る処理系の上で) 表面上動いてしまうということがよくあるのです。これがその辺にサンドバッグがごろりんごろりんとしている理由です。
2. 用語一覧 兼 目次
筆の隨に話が変な方向に流れています…。閑話休題としてこの記事の構成と用語一覧をここに入れます。
この記事では規格の節・段落番号を青字で示します (例: §1.4/¶1)。また規格からの引用文は藍色で示します (例: This International Standard specifies requirements for implementations of the C ++ programming language.)。
この記事で説明する用語一覧は以下の通りです。日本語訳は JIS から取ってきます。リンクはこの記事内の対応する箇所に跳ぶためのものです。
この記事 | ISO/IEC 14882:2014 (C++14) |
JIS X 3014:2003 (プログラム言語C++) |
俗称 |
---|---|---|---|
§3 | conforming implementation | 適合する処理系 | |
§3 | diagnosable rules | 診断対象規則 | |
§3 | no diagnostic required, no diagnostic is required |
診断を必要としない、 診断の必要はない、etc. |
NDR |
§4.1 | syntactic rules | 構文規則 | |
§4.1 | semantic rules | 意味規則 | |
§4.4 | one definition rule (ODR) | 単一定義規則 | |
§5 | well-formed program | 適格プログラム | |
§5 | ill-formed program | 不適格プログラム | |
§6 | observable behavior | 外から見た動作 外から見える動作 |
|
§6, §7 | undefined behavior | 未定義の動作 | UB |
§6 | unspecified behavior | 未規定の動作 | |
§6 | implementation-defined behavior | 処理系定義の動作 | |
§6 | locale-specific behavior | 文化圏固有動作 | |
§8 | conditionally-supported | N/A |
※conditionally-supported は C++03 の時点では存在しなかったので、その翻訳である JIS に対応する概念は現れません。この記事では今後便宜のため「条件付き対応」などと呼称することにします。
日本語訳は何やら微妙なものが多いですね。外から見た動作
や 診断を必要としない
はもっと用語っぽくした方が良いのではという気がします (観測可能動作
とか 診断不要
なんていうのはどうかな)。それから ○○の動作
の「の
」もない方がそれっぽい気がします。
更に、検索してみると NDR の訳は以下のように発散していてまるで統一されていません。
JIS X 3014:2013 より
- 診断の必要はない
- 診断は不要とする
- 診断は要求されない
- 診断は,不要とする
- 診断は,必要としない
- 診断は,必要ない
- 診断を必要としない
- 診断メッセージは不要とする
- 診断メッセージは必要としない
- 診断メッセージは必要ない
- 診断メッセージは,必要としない
- 診断メッセージを出力する必要はない
- 診断メッセージを要求されない
- 診断情報を必要としない
まあいいや。
3. 処理系の適合性 (Implementation compliance)
用語一覧で挙げた概念は互いに依存し合っている上に規格の記述が不明瞭なので、互いの記述をよく観察して整合的な解釈を確立しなければなりません。その結果、何処から説明を始めても順序立てに行き詰まるということに気づいてしまいました。仕方がないので、俯瞰的に記述を見てもらって感覚で掴んでもらうしかありません。
取り敢えず、一番の要となりそうな §1.4 Implementation compliance (処理系の適合) の冒頭からいきなり見ることにします。先ず、引用と意訳を見てもらって一文ずつ見ていくことにしましょう。
§1.4/¶1 The set of diagnosable rules consists of all syntactic and semantic rules in this International Standard except for those rules containing an explicit notation that "no diagnostic is required" or which are described as resulting in "undefined behavior."
意訳: 診断対象規則は、この国際規格に載っているすべての構文規則と意味規則から、"診断不要"もしくは"未定義の動作"を起こすと明記されている規則を除いたものを指す。
いきなり規則 (rules) がどうとか言っていますね。取り敢えず次のことはご承知下さい。
- 規格はたくさんの規則の塊である。
しかしながら、いざ規格を読んで見れば分かりますが1つ1つ "これが規則である" とは書かれていません。GitHub にある規格原稿リポジトリ Wiki の Specification Style Guidelines を見ると、規格中で "the behavior is ○○" や "○○ shall ○○" などと書かれているものが個々の規則と捉えられそうです (実際、"診断不要" の指定がある場合はその直前に "○○ shall ○○" が必ずあります)。
- 規則とは (基本的に) "○○ shall ○○" の形で記述されている事柄と思われる。
但し、shall を使っていなくても「これは実質規則なのでは」という記述も色々あるので厳密には怪しいです。
改めて §1.4/¶1 に戻ってみますと以下のことが読み取れます。
- 規格に載っている規則は構文規則と意味規則に分類されると思われる。
- 規則によっては "診断不要" や "未定義の動作を起こす" などの性質が規格中で明記される。
- 診断不要とも未定義動作とも明記されていない規則を診断対象規則と呼ぶ。
先ず構文規則 (syntactic rules) と意味規則 (semantic rules) という語が目につきます。しかし、
- どの規則が構文規則でどの規則が意味規則なのか?
は規格中に一言として載っていません (これについては後で改めて取り扱います)。それどころか、
- 構文規則かつ意味規則な規則は存在するのか?
- 構文規則でも意味規則でもない規則は存在するのか?
も不明瞭です。この辺りの構文規則 vs 意味規則の解釈は実は冒頭の地図の左側の×印と関係して来るのですが、ここでは暫定的な決着をつけて置きます。
まず、通常はシンタックス (構文) とセマンティクス (意味論) は対立した概念として扱われるので、《構文規則でもあり意味規則でもある規則はない》と考えるのが自然です。次に、構文規則でも意味規則でもない規則が存在することを認めてしまうと、どの規則がそれに当たるのか全く書かれていない以上は "診断対象規則" が全く不明瞭になってしまいます。診断対象規則は続く段落で参照されるのでそれは不都合です。そんな訳で《構文規則でも意味規則でもない規則は存在しない》と信じていなければやっていられません (注意: これはこの記事§4の議論で覆します)。
結論として全ての規則は構文規則または意味規則の何れかに分類されるのではないかと考えられるわけです。診断不要と未定義の動作に関してはまた後で説明します。ここでは規則の属性としてそういうものがあるのだなぐらいに思って下さい。あと診断 (diagnostics) というのは、コンパイラがエラーメッセージや警告を出力すること(など)と思って下さい。ここまでくれば診断対象規則に関してはOKですね。
次の段落に行きます。
§1.4/¶2 Although this International Standard states only requirements on C ++ implementations, those requirements are often easier to understand if they are phrased as requirements on programs, parts of programs, or execution of programs. Such requirements have the following meaning:
意訳: この国際規格は C++ 処理系に対する要求しかしない。しかし、その要求内容を恰も「プログラム・プログラムの一部・プログラムの実行に対する要求」の形で記述するのが分かりやすい。そのような記述は実際は以下のように解釈される:
さて、この段落は C++警察にとっては衝撃的なことを述べているのではありませんかね。つまり規格は処理系――特に (続きにあるように) 適合する処理系 (conforming implementation)――に対して要求しているのであって、C++ プログラムに対して直接要求をしている訳ではないのです。実際、規格の文面には適合する処理系 (conforming implementation) という言葉は登場しますが、適合するプログラム (conforming program) という言葉は出てきません。ということは、規格を持ち出してプログラムが合法だ違法だと騒ぎ立てるのはお門違いなのでしょうか?
何となくこの構図は 刑法には「人を●してはいけない」とは書かれていない vs 書かれている に似ていますね…。丁度、プログラムが一般市民で、処理系が処罰を与える機関のようです。
つまり "プログラムは○○してはいけない" と書いても結局それがどういう意味なのかは処理系の動作を通してしか定義できないので処理系に対する要求の形を取っているが、運用上はやはりプログラムに対する要求も意図していると見るべきなのでしょうか?
実際に Specification Style Guidelines には (規格になかった) 適合するプログラム という言葉が出てきます (この wiki はちゃんとした人が編集しているかどうか知らないが)。 訂正: すみません適合するプログラム (conforming program) は空目でした。そんな言葉はありませんでした。
- 規格は適合する処理系に対する要求を記述する
- 規格をプログラムに対する直接の要求と見るかどうかは微妙
続いて、プログラムに対する規則が如何に処理系に転嫁されるかが箇条書きで述べられています。
- (2.1) If a program contains no violations of the rules in this International Standard, a conforming implementation shall, within its resource limits, accept and correctly execute [Footnote] that program. [Footnote: "Correct execution" can include undefined behavior, depending on the data being processed; see 1.3 and 1.9.]
(2.1) プログラムがこの国際規格の規則に何も違反していないとき、適合する処理系はそのリソースの限りにおいてそのプログラムを受領し正しく実行※しなければならない。[※ "正しく実行" とは (処理するデータに依存して起こる) 未定義の動作も含む。]- (2.2) If a program contains a violation of any diagnosable rule or an occurrence of a construct described in this Standard as "conditionally-supported" when the implementation does not support that construct, a conforming implementation shall issue at least one diagnostic message.
(2.2) プログラムが、診断対象規則の違反、またはこの国際規格で "条件付き対応" とされた構成要素で処理系が対応していないものを一つでも含む場合は、適合する処理系は少なくとも一つの診断メッセージを発行しなければならない。
まとめると
- 適合する処理系は、違反のないプログラムを正しく実行する。
- 適合する処理系は、診断対象規則を違反するプログラムについて何か診断情報を出す。
- 適合する処理系は、未対応の "条件付き対応" の機能を使うプログラムについて何か診断情報を出す。
ということです。
- (2.3) If a program contains a violation of a rule for which no diagnostic is required, this International Standard places no requirement on implementations with respect to that program.
(2.3) プログラムが診断不要の規則に違反している場合、この国際規格はそのプログラムに関して処理系に何も要求しない。
- 処理系は、診断不要の規則に違反するプログラムについて何をしても良い。
この項目は実質 診断不要 (no diagnostic required; NDR) の定義になっています (他には診断不要の規則の取り扱いについて述べている箇所はありません)。
[Note: During template argument deduction and substitution, certain constructs that in other contexts require a diagnostic are treated differently; see 14.8.2. -end note ]
[註釈: 或る特定のプログラム構成は、通常の文脈で診断を要求するとしても、テンプレート実引数推論および置換の際には違う取り扱いになる。14.8.2 を参照されたい。 - 註釈終わり]
これは SFINAE の変な推論で現れた規則違反については診断情報を出さなくても良いという註釈です。気にしなくて良いです。
まだ §1.4 の段落2つしか見ていませんが重要なところはこれで終わりです。
4. 議論: C++ の構文規則と意味規則の正体は何なのか?
さて前節では構文規則と意味規則が規格を読んでも不明瞭ということを書きました。ここでは構文規則と意味規則について現在最有力(?)の仮説と問題点を記述します。
4.1 現在の仮説
- 形式上、処理系に対する規則と、プログラムに対する規則がある。
-
プログラムに対する規則は構文規則と意味規則に分類される。
- 構文規則は構文記法とそれに付随する記述 (制約) からなる。
- 意味規則は構文規則以外のプログラムに対する規則である。
- 但し、どこまでを "構文記法に付随する記述 (制約)" とするかは謎。
※既に見た通り、処理系に対する規則とプログラムに対する規則は、実際は適合する処理系に対する要求を述べるための材料に過ぎません。処理系に対する規則は直接の要求で、プログラムに対する規則は処理系に対する間接的な要求です。
また、構文記法 (syntax notations) というのは、あの、規格によく挿入されている BNF3 みたいなののことです。構文記法の定義と意味 (読み方) は §1.6 [syntax]/¶1 で述べられていますがここでは省略します。例えば以下のような見た目をしています。
§6.5 [stmt.iter]/¶1 より
iteration-statement:
while ( condition ) statement
do statement while ( expression ) ;
for ( init-statement conditionopt ; expressionopt ) statement
for ( for-range-declaration : for-range-initializer ) statement
for-range-declaration:
attribute-specifier-seqopt decl-specifier-seq declarator
attribute-specifier-seqopt decl-specifier-seq ref-qualifieropt [ identifier-list ]
for-range-initializer:
expr-or-braced-init-list
このような形の構文記法は単体では何らの強制力もありませんが、§3.5 [basic.link]/¶1 を通してプログラムに対する規則としての意味を持ちます。
§3.5 [basic.link]/¶1 A program consists of one or more translation units (Clause 2) linked together. A translation unit consists of a sequence of declarations.
意訳: プログラムは、一つ以上の互いにリンクされることを前提とした翻訳単位 (§2) からなる。翻訳単位は宣言の列からなる。translation-unit:
declaration-seqopt
- プログラムは翻訳単位の集まりで、翻訳単位は syntax notations で指定される4。
4.2 仮説の根拠と問題点
C++ の規格には構文規則と意味規則が何であるか書かれていませんでしたが、C++ が参照している C言語の規格には何か書かれているかもしれません。結論から書くと、C言語の規格でも構文規則と意味規則自体に明確な定義は与えられていません。しかし、C言語の規格では一部の規則について構文規則または意味規則であることを指定している (と思われる) 部分があるのでそれらの分類方法を観察することで C++ 規格の構文規則と意味規則に解釈を作ることができそうです。
(1) C言語の規格には「構文規則の左辺」という記述がある
C11 §3 Terms, definitions, and symbols/¶1
Other terms are defined where they appear in italic type or on the left side of a syntax rule.
意訳: その他の項目はイタリック体で現れる時か、構文規則の左辺に現れる時に定義される。
いきなり構文規則の左辺という語が目に入ります。ここから直接読み取れるのは
- C言語の構文規則に左辺があるとき、その左辺はそこで定義される
という事です。これだけでは構文規則に常に左辺があるのかどうかまでは断定できませんが、書きぶりはそうであることを暗示しているように見えます。
- C言語の構文規則には左辺があると思われる
そしてこれは如何にも構文記法のことを指しているように思われます。
(2) C言語の規格では小見出し Syntax/Semantics で分けられた記述が繰り返し登場する
C言語の規格を読み進めていくと、C11 §6.4 以降、 Syntax / Semantics といった小見出しが繰り返し現れるようになります。例えば以下のようになっています。
C11 §6.4.1 Keywords より
Syntax
1 keyword: one of
auto ∗ if unsigned
break inline void
case int volatile
char long while
const register _Alignas
continue restrict _Alignof
default return _Atomic
do short _Bool
double signed _Complex
else sizeof _Generic
enum static _Imaginary
extern struct _Noreturn
float switch _Static_assert
for typedef _Thread_local
goto union
Semantics
2 The above tokens (case sensitive) are reserved (in translation phases 7 and 8) for use as keywords, and shall not be used otherwise. The keyword_Imaginary
is reserved for specifying imaginary types.70)
普通に考えれば Syntax の下にある記述が構文規則 (syntactic rules) を表し、Semantics の下にある記述が意味規則 (semantic rules) を表していると解釈するのが自然です。そして予想通り Syntax は必ず構文記法で記述されています。
- C言語の規格では構文規則と意味規則が明示されているようだ
- C言語の規格では構文規則は構文記法で与えられるようだ
(3) C言語の規格には Syntax/Sementics などの見出しの下にない規則がある
つまり、構文規則でも意味規則でもない規則があるということになりそうです。また、よく観察してみると、見出しの下にない記述は基本的に処理系に対する直接の要求であり、見出しの下にある記述はプログラムに対する規則になっているということに気づきます。
- C言語の処理系に対する直接の規則は構文規則でも意味規則でもない
- C言語のプログラムに対する規則は構文規則や意味規則(など)に分類されている
ところで C++ の場合には構文規則・意味規則のどちらでもない規則を認めると診断対象規則の定義が曖昧になるのではないかということを既に議論しました。しかし、上記のことを念頭に改めて C++ 規格を観察してみますと、診断対象規則は「診断対象規則に違反したプログラム」の形でしか登場しません。つまり、処理系に対する規則についてはそれが診断対象か否かは実質的に意味を持ちませんので曖昧でも問題は起こりません。先の議論の結論は以下のように修正されます。
- 構文規則でも意味規則でもないプログラムに対する規則は存在しない
(4) C言語の規格には Syntax/Semantics の他に Constraints などがある
よく観察すると Syntax/Semantics と同じレベルの見出しとして Constraints/Description/Implementation-limit などといったものもあることに気づきます。
- Constraints は、その内容から、構文記法で表現しきれていない追加の構文的制限のようです。
- Description はただ単に構文記法の内容を言葉で表現し直しただけのもののようです。
- Implementation limits は処理系定義の何らかの上限値です。
Constraints について改めてC言語規格を読んでみると C11 §1/1 で制約 (constraints) が登場し、syntax や semantic rules と同列に扱われていることが分かります。
C11 §1 Scope/1 より
This International Standard specifies the form and establishes the interpretation of programs written in the C programming language. 1) It specifies
- the representation of C programs;
- the syntax and constraints of the C language;
- the semantic rules for interpreting C programs;
- 以下略
一方で C++規格を改めても syntax に並ぶ constraints のような分類は見当たりません。しかし、C言語で constraints に分類されている規則と同様のプログラムに対する規則は確かに C++ の規格に存在しています。これらの規則は C++ では構文規則に属するのでしょうか、それとも意味規則に属するのでしょうか。
C規格では syntax/constraints はセットで取り扱われている雰囲気があるので、C++ ではこれらをまとめて構文規則と呼ぶのが良いでしょうか。しかし、C++ 規格に構文規則・意味規則が明記されていない以上は constraints と semantics の境界は曖昧です。明確さを優先させるのであれば syntax (構文記法) のみを構文規則とし、constraints/semantics (文章) を意味規則とするのが良いでしょうか。
- C言語には構文 (syntax) と制約 (constraints) と意味規則 (semantic rules) の三種類の規則がある。
- C++ では制約という分類はない。
- C で制約とされる類の規則が、C++ で構文規則・意味規則のどちらになるかは謎。
うーん…やはり感覚的には制約は構文規則にした方が良いように思います。
4.3 死んだ他の仮説
(1) 仮説1: §2, §16 に書かれている規則が構文規則でそれ以外が意味規則ではないか
章によって構文規則・意味規則が分けられるのではないかという説です。§2 は Lexical conventions (字句関連) で、§16 は Preprocessing directives (プリプロセッサ関連) です。この説は死にました。というのも、肝心の "構文" についての章が存在しないからです。構文と言えるものは各章に分散して記述されています。
(2) 仮説2: §2.2/¶1 で参照されている項目が構文規則なのではないか
構文規則 (syntax rules) について言及があるのは規格中で §2.2 [lex.phases]/¶1 だけです。この節では翻訳の構文規則の順序 (the precedence among the syntax rules of translation) が指定されます。順序が指定されるのだから全て列挙されているのではないかということです。
これに従うと、§2, §16, §14.2/¶3 に載っている規則が構文規則になります。§14.2/¶3 は、《std::vector<std::vector<hoge>>
の >>
は '>' ×2 に分解されてテンプレート引数の綴じ括弧になる》というやつです。しかしやはり構文に関連する規則が抜けています。よく見ると §2.2/1(7) において The resulting tokens are syntactically and semantically analyzed and translated as a translation unit の一言で不特定の構文規則が参照されています。§2.2/¶1 は構文規則の特定には使えません。
4.4 単一定義規則 (ODR) とその分類問題
未だ気になることがあります。C++ には単一定義規則 (ODR) というものがあります。この ODR が構文規則なのか意味規則なのかどちらでもないのかということです。
そもそも ODR とは、変数・クラス・関数にはただ一つの定義を与えなければならない (複数定義してはならない、かつ、使用する (odr-used) なら定義が存在しなければならない) という感じのものです。§3.2 [basic.def.odr] に記述されています。
雰囲気として意味規則の部分集合になるのではないかと思われますが、気になる記述が §1.3.29 [defns.well.formed] にあります。
well-formed program
C++ program constructed according to the syntax rules, diagnosable semantic rules, and the one-definition rule (3.2).
意訳: 構文規則、診断対象意味規則、ODR を満たす C++ プログラム
この書き方を見ると ODR は構文規則でも意味規則でもないのではないか という疑惑が発生します。しかし、この記事ではやはり ODR は意味規則と解釈します。
- ODR は意味規則の部分集合と解釈する。
以下の様な根拠に基づきます。
- ODR (§3.2) の一部の規則に診断不要などの指定がある。これから ODR は診断対象か診断不要かが意味を持つ規則と考えられる。そのような規則は §1.4/¶1 により意味規則と構文規則だけである。
- ODR が意味規則に属すると考えれば、規格文で "診断対象意味規則、ODR" となっているのは「意味規則の中でも診断対象または ODR のもの」と解釈できる。
- ODR が構文規則に属すると考えると、"構文規則、ODR" とわざわざ重ねて書く理由がなくおかしい。
5. 適格 (well-formed) と不適格 (ill-formed)
難所は過ぎました。ここからは C++er なら誰しも聞いたことがあるであろう用語について説明していきます。先ず、well-formed と ill-formed です。日本語の適格・不適格は聞きませんね。それに雰囲気が伝わってきません。日本語は忘れて well-formed と ill-formed で良いでしょう。§1.3.29 [defns.well.formed] と §1.3.11 [defns.ill.formed] で定義されています。
§1.3.29 [defns.well.formed] より
well-formed program
C++ program constructed according to the syntax rules, diagnosable semantic rules, and the one-definition rule (3.2).
§1.3.11 [defns.ill.formed] より
ill-formed program
program that is not well-formed (1.3.29)意訳:
適格プログラム: 構文規則、診断対象意味規則、ODR を満たす C++ プログラム構成
不適格プログラム: 適格 (1.3.29) でないプログラム
解釈などに特に問題はありません
- well-formed とは構文規則、診断対象意味規則、ODR を満たしていること
- ill-formed とは well-formed でないこと
6. 動作 (behavior)
動作 (behavior) というのは、プログラムを翻訳・実行するときに処理系が取ることのできる動作です。
(1) 外から見た動作 (observable behavior)
特に意味のある動作が 外から見た動作 (observable behavior) です。observable behavior は §1.9 [intro.execution]/¶8 で定義されます。
§1.9 [intro.execution]/¶8 より
The least requirements on a conforming implementation are:
- (8.1) Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
- (8.2) At program termination, all data written into files shall be identical to one of the possible results that execution of the program according to the abstract semantics would have produced.
- (8.3) The input and output dynamics of interactive devices shall take place in such a fashion that prompting output is actually delivered before a program waits for input. What constitutes an interactive device is implementation-defined.
These collectively are referred to as the observable behavior of the program. [Note: More stringent correspondences between abstract and actual semantics may be defined by each implementation. —end note ]
- observable behavior は入出力や volatile オブジェクトのアクセスなどを通して処理系外部から確認できる動作のことである。
Observable behavior は、規格通りの observable behavior である限り処理系の内部で何をしても良い (§1.9/¶1; いわゆる as-if 規則) ということを言う為に導入される概念です。最適化の根拠となります。この規則があるため、基本的に動作についての議論 (この節の続き) は observable behavior に対してしか意味を持たないと考えられます。
(2) undefined/unspecified/implementation-defined behavior
次は、この、違いがよく分からなくなる○○動作三兄弟です。日本語では未定義の動作 (undefined behavior)・未規定の動作 (unspecified behavior)・処理系定義の動作 (implementation-defined behavior) です。特に未定義動作は大人気ですよね。毎月未定義動作に関する記事が出ているのではないかと錯覚する程です (Qiita 検索結果)。巷では未定義動作は UB と省略されます。
これらはある状況に於いて適合する処理系が取ることのできる動作の選択肢の範囲を指定する用語です。未定義動作だとか未規定動作だとかいう名前の具体的な特殊動作が存在するわけではありません。これらの定義はやはり §1.3 で与えられています。
§1.3.27 [defns.undefined] より
undefined behavior
behavior for which this International Standard imposes no requirements
[Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed. Evaluation of a constant expression never exhibits behavior explicitly specified as undefined (5.20). —end note ]意訳: 未定義動作
この国際規格が何の要求もしないような動作 [註: この国際規格が動作について明示的な定義を指定しない場合や、プログラムが誤った構成かデータを使った時にも未定義動作を期待して良い。未定義動作として許容できる動作は多岐にわたる: (a) その状況を完全になかったことにして次に進みその結果何が起こっても知らない (b) 翻訳または実行過程で環境に応じて文書で事前に説明した通りに動作する (診断情報は出してもいいし出さなくてもいい) (c) 診断情報の発行と共に翻訳または実行を停止するなど。多くの誤ったプログラム構成は診断情報を要求されるので、未定義動作には至らない。定数式の評価は明示的な未定義動作を呈さない。]
- ある時の動作が未定義動作と記述されているときは処理系に対して何の要求もなされない (処理系は何をしても良い)
定数式の件から以下のことも言えそうです。
- 未定義動作には明示的に指定される場合とそうでない場合がある
この未定義動作に関してはまたあとで取り扱いたいと思います。次は未規定動作と処理系定義の動作です。
§1.3.28 [defns.unspecified] より
unspecified behavior
behavior, for a well-formed program construct and correct data, that depends on the implementation [Note: The implementation is not required to document which behavior occurs. The range of possible behaviors is usually delineated by this International Standard. —end note ]意訳: 未規定動作
適格プログラムと正しいデータに対する動作で、処理系に依存するもの [註: 処理系は実際にどのような動作をするかを文書に記述しなくても良い。可能な動作の範囲は通常この国際規格によって制限される。]§1.3.12 [defns.impl.defined] より
implementation-defined behavior
behavior, for a well-formed program construct and correct data, that depends on the implementation and that each implementation documents意訳: 処理系定義の動作
適格プログラムと正しいデータに対する動作で、処理系に依存してかつそれぞれの処理系で文書に具体的な動作を記述するもの
- 未規定の動作は、処理系依存の動作で処理系の文書に記述しなくて良いもの
- 処理系定義の動作は、処理系依存の動作で処理系の文書に記述しなければならないもの
訂正 但し、1.9 [intro.execution]/¶5 によると、適合する処理系は、適格な同じプログラム・同じデータを与える限りは同じ動作をしなければなりません。従って、未規定の動作であっても実行する度に異なる動作をするというのはできません。
-
適合する処理系は同じ適格プログラム・同じデータに対しては毎回同じ動作をする つまり、未規定な動作であっても処理系毎に一貫した動作が必要
@k-satoda さんのご指摘により修正いたします。1.9 [intro.execution]/¶5 を改めて読むと、以下のようになっていて「実行する度に同じ」という意味ではなく「(規格の定める) 抽象機械の動作と同じ」という意味でした。従って、未規定な動作の時に処理系が一貫した動作を取るかどうかは自由です。
1.9 [intro.execution]/¶5
A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input. However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even
with regard to operations preceding the first undefined operation).
意訳: 適格プログラムを実行する適合する処理系は、対応する抽象機械の同じプログラム・同じ入力に対する可能な実行の内の1つと同じ外から見た動作をしなければならない。但し、そのような実行の中に未定義動作が含まれる場合には、この国際規格はそのプログラム・入力について (未定義動作に至る初めの操作より前にある操作についても) 何の要求もしない。
(3) locale-specific behavior
序でに文化圏固有動作 (locale-specific behavior) も載せます。
§1.3.14 [defns.locale.specific] より
locale-specific behavior
behavior that depends on local conventions of nationality, culture, and language that each implementation documents意訳: 文化圏固有動作
現地の国家・文化・言語の風習に依存する動作で、処理系が文書に記述するもの
C++規格で直接規定されている locale-specific は1項目しかありません。拡張実行時文字集合 (extended execution (wide-)character set) にどの様な文字が用意されるかです。要するに ""
や L""
の文字コードが何なのかというのが locale-specific です。
他に C言語から継承している locale-specific が複数あります (一覧は C11 §J.4)。例えば isalpha
などの <ctype.h>
関数に拡張文字 (extended characters) を指定した時の結果や、std::locale()
に空文字列 ""
を指定した時の設定などです。
7. 議論: 未定義動作 (UB) の範囲
未定義動作は規格が処理系に何も要求をしない、つまり、処理系が何をしても良いという状況のことを指していました。しかし、何をしても良いというのはどういうことか、そして、規格が何も要求しないというのは具体的にどのような時かについて議論をします。
7.1 何をしても良いというのは本当?
本当らしいです。よく冗談で、鼻から悪魔を出しても良い (nasal demons) だとか、コンピュータを爆破しても良いだとか、宇宙を崩壊させても良いだとか言っているのを聞きます。C++規格としては何も制限を置きません (もちろん、普通はユーザから糾弾されたり、人間社会の法令に触れたり、悪魔を出すための超テクノロジーが必要になったり、いろいろするので実際的な制限があるでしょう。しかしこれらの制限はC++の制限ではありません)。
余談: 所で書いていて思ったのですけれど、別に未定義動作のないプログラムであっても処理系が鼻から悪魔を出すのはいつでも自由なのでは? 処理系に課されるのは observable behavior だけであって、そこには鼻に対する要件は含まれていません。ということは、処理系が例えば《鼻から悪魔を出して悪魔契約に依って (ユーザの命を代償に) 計算結果の答えを聞き出した方が早く結果を得られる》と判定して鼻から悪魔を出すように最適化するということがあっても良いのではないでしょうか…。
一方でコンピュータを爆破したり宇宙を崩壊させたりすると、入出力もままならなくなるのでこれは未定義動作のないプログラムに対して許容されません (いや、爆破・崩壊したあとに復元・時間遡行などすればよいのでしょうか)。人類滅亡などだと入出力には関係ないので処理系は自由に実行できますね。
7.2 NDR な規則違反は UB に至るのかそうでないのか
UB の定義を再度見てみます。
§1.3.27 [defns.undefined] より
undefined behavior
behavior for which this International Standard imposes no requirements
[Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. 以下略
国際規格が何の要求も課さない動作を UB と呼ぶと読めます。何の要求も課さないと言えば NDR です。
- (2.3) If a program contains a violation of a rule for which no diagnostic is required, this International Standard places no requirement on implementations with respect to that program.
"プログラムがNDRの規則違反を含むとき、そのプログラムについて国際規格は何の要求もしない" と書いてあります。従って、NDR ⊂ UB である…と思っていたのですが世の中的にはそうでもないようです。
- ISO C++ Standard - Discussion - "undefined behaviour" vs "ill-formed & no diagnostic required" ?
- c++ - Difference between Undefined Behavior and Ill-formed, no diagnostic message required - Stack Overflow
(元の問いが UB と ill-formed NDR の違いは何かというもので UB ≠ NDR を前提としているからなのかもしれませんが) NDR は翻訳時で UB は実行時の話についてなのであって両者は違うのだというコンセンサスがなんとなく形成されているように思われます。
しかし NDR 自体が UB でないとしても、翻訳時に規格が何の要求もしないということは、結果として実行時の動作についても規格は何も要求できないできないことになり、結局 NDR から UB が発生する気がします。
ただし、ここで少し気になることがあります。この主張では "規格が動作について何も要求しない" ならば UB ということを暗黙に前提としました。しかしこれは本当でしょうか。UB ならば "無要求" はいえますが、その逆は成立するのでしょうか。
★UB ⇔ 無要求 なのか UB ⇒ 無要求 なのか
先に引用した UB の記述は、規格の用語と定義 (Terms and definitions) という章に書かれている記述なので、"定義" であるはずです。つまり、何が UB であって (十分条件)、何が UB でないか (必要条件(の対偶)) をちゃんと示していなければなりません。だとすれば、
$$\mathrm{UB} \Leftrightarrow \text{(規格は処理系の動作に対し無要求)}$$
と解釈しなければなりません (一応無理矢理に UB := "(⇒無要求) を意味する用語" と解釈できないこともないですが、個人的にはかなり無理がある気がします)。
★may be expected の謎
ところが、UB の定義の Note に気になる文が存在しています。
Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data.
意訳: 国際規格が動作について何も規定していないときと、プログラムが誤った構成・誤ったデータを使ったとき、UB may be expected
この may be expected の解釈が曖昧です。may には "しても良い" と "かもしれない" の2種類の意味があります。従って、"処理系は UB を期待して良い" なのかそれとも "UB が期待されるかもしれない" なのかが分かりません。
- 前者ならばつまり処理系は UB として何を実行しても良いということになり、つまり UB です。"UB ⇔ 無要求" が支持されます。
- 後者ならば "UB が期待されるかもしれないしそうでないかもしれない" ということで単に UB の可能性がある状況を Note で示唆しているに過ぎないと解釈できます。"UB ⇒ 無要求" ということになります。
ここで UB の Note の続きを見ますとこのような文があります。
Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed.
注目点: 誤ったプログラムでも UB にならない例が示されています。
つまり、may be expected の解釈は後者でなければなりません。ということは "UB ⇒ 無要求" ということになります。
実は未だ続きがあります。ここで C++98 の UB の定義を見てみます。
C++98 §1.3.12 [defns.undefined] より
undefined behavior
behavior, such as might arise upon use of an erroneous program construct or erroneous data, for which this International Standard imposes no requirements. Undefined behavior may also be expected when this
International Standard omits the description of any explicit definition of behavior. [Note: (以下略)
注目点: 2種類の may/might が使われています
前半の might arise は "~しても良い" ではなくて "~するかもしれない・~する可能性がある" でないと意味が変です。後半の may also be expected はどちらとも取れますが、わざわざこの文を続く Note に入れずに normative な文にしたのであることを考えれば "~かもしれない" の解釈は不適切で "~しても良い" と解釈するのが適切です。つまり C++98 の段階では、なんだかんだいって "UB ⇔ 無要求" のような気がします。
これらの2つの文 might ~ (~かもしれない) と may ~ (~しても良い) が1つの文にまとめられたのは C++11 のことですが、これは事故ではないかという気がします。2つの異なる意味 (と思われる…たぶん) の may が一つにくっつけられてしまったのですから。
★explicit UB と implicit UB
実は "UB ⇔ 無要求" ではないかと考えられる間接的な証拠が規格の所々にあります。先ず、UB の定義の Note の最後の文です。
§1.3.27 [defns.undefined]/Note
[Note: (中略) Evaluation of a constant expression never exhibits behavior explicitly specified as undefined (5.20). —end note ]注目点: "明示的に未定義とされた動作" と書かれています。
訂正: 他にもあります。 下は改めてよく英語を見ると関係ないですね。which are がかかっているのは an explicit notation ではなくて rules の方です。
§1.4/¶1 The set of diagnosable rules consists of all syntactic and semantic rules in this International Standard except for those rules containing an explicit notation that "no diagnostic is required" or which are described as resulting in "undefined behavior."
注目点: "NDR という明示的記述を含む規則、または、UB に至ると説明して
ある明示的記述いる規則" と書かれています。 (訂正済)
これらの文章で undefined behavior に対してわざわざ明示的 (explicit) という修飾をしているのは UB には明示的な UB と暗黙的な UB の2種類があるということを暗示しています。更に暗黙的な UB の実例として次の節 7.3 で説明する無限ループがあります。暗黙的な UB を認めるということは "UB ⇔ 無要求" ということに他なりません。
以上のことから規格に忠実に従えば NDR はやはり結果として UB を引き起こすことになると言える気がします。ただ実際は規格よりも C++ コミュニティの共通認識のほうが優先されるべきなので NDR は UB には至らないとの解釈の方が無難…なのでしょうか。
7.3 無限ループと爆発原理
(1) 無限ループが UB というのはデマなんじゃないか?
(入出力・volatile なアクセス・同期操作・アトミック操作を含まない) 無限ループは UB を引き起こすと考えて良いのでしょうか。あるいは違うのでしょうか。C++ において無限ループが UB になるのではないかという議論は以下の2箇所に見られます (C 言語についてはまた独立に議論されていますが C++ とは状況や前提が異なるようです)。
でもこれらの議論の運び方について私は懐疑的です。
何れの記事も最終的にはたった一つの記事 "Compilers and Termination Revisited – Embedded in Academia" の以下の記述を根拠にしています5,6。
N3090
A loop that, outside of the for-init-statement in the case of a for statement,
- makes no calls to library I/O functions, and
- does not access or modify volatile objects, and
- performs no synchronization operations (1.10) or atomic operations (Clause 29)
may be assumed by the implementation to terminate. [ Note: This is intended to allow compiler transformations, such as removal of empty loops, even when termination cannot be proven. —end note ]
Unfortunately, the words “undefined behavior” are not used. However, anytime the standard says “the compiler may assume P,” it is implied that a program which has the property not-P has undefined semantics.
意訳:
N3090 (6.5) から抜粋すると、
ループが (もし for 文の場合には for 文の for-init-statement 以外の部分で)
- ライブラリ入出力関数を呼び出さず
- volatile オブジェクトにアクセス・変更を行わず、
- 同期操作 (1.10) またはアトミック操作 (章29) を実行しない
場合には、処理系はそのループが何れ終わることを仮定して良い。[註: これはコンパイラによる最適化を許容するためである。例えば、ループが終わることを証明できなくても空ループを除去して良い。]
残念ながら UB という単語は使われていないけれども、いつでも規格が "コンパイラは P を想定して良い" と言っている場合には、not-P なプログラムは未定義な意味論を持つんだ。
訳注:
N3090 の上記引用部分は実際には規格化されませんでした。C++11 にも C++14 にも最新 C++1z にも含まれません。但し、対応すると思われる記述が §1.10 [intro.multithread]/¶24 にあります。以下の通りです。The implementation may assume that any thread will eventually do one of the following:
- terminate,
- make a call to a library I/O function,
- access or modify a volatile object, or
- perform a synchronization operation or an atomic operation.
[Note: This is intended to allow compiler transformations such as removal of empty loops, even when termination cannot be proven. —end note ]
意訳: 処理系はどのスレッドも何れ以下をすると仮定して良い。
- 終了する
- ライブラリ入出力関数を呼び出す
- volatile オブジェクトにアクセス・変更を行う
- 同期操作またはアトミック操作を行う
[註: これはコンパイラによる最適化を許容するためである。例えば、ループが終わることを証明できなくても空ループを除去して良い。]
以上の記述はそもそも "フェルマーの最終定理の反例が見つかったら停止する" ループが停止してしまう のは何故かについての考察として UB があるからに違いないとして、各言語で UB は何処にあるのかという意図で書かれました。そして、C++ では矛盾する仮定をコンパイラがおけるから UB なのだと結論づけています。
しかし色々と無理矢理感があることを否めません。
- 先ず C++ では "ループが終わると仮定して良い" という規格の記述だけで UB を介さずに ループが停止してしまうことを説明できます。C++ の場合 UB であることはこの現象の説明に必要ありません。
- 記事では矛盾する仮定の下での意味論は定義されないから未定義動作なのだと主張しています。しかし、意味論が定義されないということと未定義動作の間には隔たりがあります。C++ における未定義動作というのは単に動作が定義されないということではなく、"規格が何も要求しない処理系の動作" という独立した定義が与えられた一つの用語です。
- そもそも anytime the standard says “the compiler may assume P,” it is implied that a program which has the property not-P has undefined semantics というのは広く認識されている主張なのでしょうか。意味論に矛盾があるということと定義されないということはまた少し違うことのように思われます。
(2) それでも注意深く考察すれば無限ループは UB かもしれないし違うかもしれない
しかし矛盾から何でも出せるというと(記号)論理学の爆発原理が思い出されます。前提に矛盾があると (通常の推論規則を使って) どんな命題でも導出できてしまうというものです (多くの体系では原理というより定理な気がします)。以下の記事がとてもわかり易いです。
特に難しいのは、ひと目見てそこに矛盾があると分からないぐらい状況が複雑な時があって、込み入った導出の過程で上記頁にあるような構造が巡り巡って再現され、結果として何でも導出できてしまう場合があることです。矛盾があると爆発原理は避けようと思って避けられるものではないのです。
矛盾というのはさながら論理学の UB のようです。しかし制限があります。爆発原理に依って示すことができるのはその体系の中で記述できる命題だけであって、論理学者の鼻から悪魔を出したり、論理学者の机を爆破したり、宇宙 $U$ に終焉(?)を齎したりすることはできません。
では C++ の場合はどうでしょう。矛盾する仮定を処理系がおいた場合に、鼻から悪魔を召喚することはできるでしょうか。爆発原理だけでは既に述べたように任意の命題しか導出できません。では UB な状況を誤導出させてそこから処理系に UB を使って鼻から悪魔を召喚させるというのはどうでしょう。
規格的には OK なんでしょうか。つまり、実際には UB の状況ではないけれど UB であると勘違いした時に処理系は何をしても良いのかどうかということです。
- 立場1: 実際には UB の状況ではないので処理系は何をしても良いという訳ではない
- 立場2: 処理系が勘違いしたのは規格が矛盾する仮定を提供したせいだから、処理系の側ではどうしようもない。従って、処理系は何をしても許されるべき
うーん。立場2にしておかないと厳密に適合する処理系を作ることが不可能になってしまう気がします (※規格の may assume を一切信用しなければ一応可能ですが)。従って
- 矛盾する仮定から処理系が UB を誤導出したらそれを UB とするしかなさそう
という訳で無限ループから UB を出しても良い…のでしょうか?
8. 条件付き対応の構成 (conditionally-supported constructs)
未だ一つ残っているものがあります。conditionally-supported です。これ自体は大したことありません。
§1.3.7 [defns.cond.supp] より
conditionally-supported
program construct that an implementation is not required to support [Note: Each implementation documents all conditionally-supported constructs that it does not support. - end note ]意訳: 条件付き対応の
処理系が対応しなくても良いプログラム構成 [註: 各処理系は対応していない全ての条件付き対応の構成を文書に記述しなければならない]
- conditionally-supported は処理系が対応してもしなくても良いもの
9. 図の解説
これで一通り説明は終わりました。これまでの知識を総合して地図を組み立てることにします。
Step 1: 先ず、プログラムの規則違反について考えましょう。プログラムに対する規則は構文規則 (syntactic rules) と意味規則 (semantic rules) に分けられました。また、ODR は意味規則の一部であるという風に 4.4 で結論づけました。また、規則違反のないプログラムは、処理系が正しく実行 (correctly execute) しなければなりませんでした。
Step 2: 更に、NDR (no diagnostic required) と明記された規則と UB (undefined behavior) と明記された規則以外が診断対象規則 (diagnosable rules) でした。 NDR と UB が両方明記された規則は規格を探しても見つからないので、NDR 規則違反と UB 規則違反に交わりはありません。実は規格を探しても構文規則違反かつ UB になりそうな箇所はないのでそれを反映した形にします。適格 (well-formed) は構文規則、診断対象意味規則、ODR に違反しないことでした。不適格 (ill-formed) はその逆でした。
Step 3: 未規定の動作 (unspecified behavior) と処理系定義の動作 (implementation-defined behavior) は適格プログラムの動作で、未定義の動作 (UB) と合わせて互いに交わりはありませんでした。文化圏固有動作 (locale-specific behavior) と条件付き対応 (conditionally-supported) も追加します。規格を調べると殆どの条件付き対応の機能は、実際に対応した場合その動作は処理系定義であるとされています。残った部分がいつでもどこでも動作の変わらない可搬なプログラム (portable program) です (まあ、locale-specific で出力が変わったりするぐらいは可搬として良い気もしますが…)。
Step 4: 後は規格が適合する処理系に要求する項目についてチェックします。処理系は文書に処理系定義の動作・文化圏固有動作・未対応の条件付き対応機能について明記しなければなりません。また規格をよく読むと診断情報が要求 (diagnostic required) される状況として、診断対象規則違反の他に、条件付き対応の未対応機能の使用があります。また、規格が処理系に何も要求しないのは NDR または UB のときでした。完成です。
ただし、規格をよく読むと "非ODR意味規則違反で ill-formed NDR" の実例があると分かるので左下の×の辺りを弄りました。これについては "謎" の項 11.1 で取り扱います。更に右側にも謎があります。11.2 で扱います。
Check: さて、この図は規格の記述から考えられる各概念の関係 (必要条件) を書きました。つまり、各事例は図のどこかのマス目に必ず来るはずです。しかし、どのマス目にも対応する事例が存在するかどうかについては確かめていません。今から確かめます。
- 診断対象構文規則違反: 殆どの構文規則がこれです
- NDR 構文規則違反: 例えば §2.7 [lex.comment]/¶1
- 診断対象 ODR 違反: 例えば §3.2 [basic.def.odr]/¶1
- NDR ODR 違反: 例えば §3.2 [basic.def.odr]/¶4
- UB ODR 違反: 例えば §3.2 [basic.def.odr]/¶6
- (非ODR) 診断対象意味規則違反: たくさん。殆どの意味規則がこれです
- (非ODR) NDR 意味規則違反: 例えば §3.3.7/(1.2)
- (非ODR) UB 意味規則違反: 例えば §3.8/¶9
- well-formed UB: 例えば §20.15.2/¶1
- well-formed NDR: 例えば §3.3.7/¶2
- ill-formed NDR: 例えば §7.1.5/¶5
- 処理系定義かつ条件付き対応: 殆ど全ての条件付き対応がこれです
- 処理系定義でない条件付き対応: §18.2.4 [support.types.layout]/¶1
OK ですね!
意外かもしれない事
- 規格は処理系に対して、条件を満たしたプログラムを正しく実行することを要求しますが、実行しないことを要求する条件はありません。つまり、コンパイルエラー (診断情報を出力して翻訳を停止) に絶対ならない処理系も可能です (例えばミスのあるソースコードを入れても無理やりミスを"修正"してコンパイルするなど)。但し、診断対象規則違反のソースコードは実行するとしても警告(診断情報)を出力する必要はあります。
- 条件付き対応の機能を対応しないときは文書に記述することが要求されますが、対応する時は文書に記述することは要求されていません。但し、殆ど全ての条件付き対応の機能は同時に処理系定義の動作なので、その場合には動作を文書に記述する必要があります。例外は offsetof (§18.2.4 [support.types.layout]/¶1) のみです。
過去の規格
作った図は原稿 C++1z に基づくものでした。過去の C++ 規格では多少の違いがあります。
- C++98, 03 では条件付き対応という概念はありませんでした。
- C++11, 14 では条件付き対応な機能に対応する場合、すべて処理系定義の動作でした。
10. 議論: 合法・違法とは結局何か?
合法 (legal)・違法 (illegal) といった言葉は規格には登場しません。C++er が勝手に使っている言葉です。海外の C++ ユーザも legal/illegal という単語を日常的に使っているようです。定義が先にあるようなものではないので、本当はこの2つの単語については使用実態を調べてそれに従うべきなのでしょう7。
しかし、ここでは使用実態は無視して、合法・違法をどのように定義するのが良さそうかについて考えましょう。先に結論から書くと
- プログラムについても処理系についても、合法 (legal) とは規格に適合することである
- プログラムについても処理系についても、違法 (illegal) とは規格に適合しないことである
と定めるのが良さそうです。
規格は本来処理系に対する要求として記述されています。従って、本来は合法 (規格に合致している)・違法 (規格を違えている) という語は処理系に対してのみ適用されるべきです。しかし、既に述べたように処理系に対する要求を規定する上でプログラムに対する規則が指定されます。これらのプログラムに対する規則を使って、プログラムの合法や違法について議論することができるようになります。
一方で、この記事では様々な程度の "正しいプログラム" について今まで議論してきました。
- 適格 (well-defined) なプログラム … 文法規則・診断対象意味規則・ODR を満たす
- "適合するプログラム" … 全ての規則を満たす (注意: 厳密には規格にこの概念はありません。ここで新しく導入する概念です)
- 安全なプログラム(?) … 適合かつ未定義動作にならない
合法、もしくは、規格に合致しているというのはどのレベルのプログラムを指すのでしょう。
実用上、信頼できるプログラムという意味であれば未定義動作のないプログラムを合法と呼ぶのが便利そうです。しかしながら規格には、未定義動作を含むプログラムについて明示的に禁止するよう記述は厳密にはありません。実際、特定の環境・処理系で特定の最適化の効果を期待する場合には規格上の未定義動作になるプログラムも考えられます。従って、ただ単に未定義動作があるというだけでそのプログラムを違法とするのには難があります。
すると適合か適格が合法の基準となりそうです。
- 適格を合法の基準とすると NDR 意味規則や UB 意味規則を破っても合法ということになり、気分が悪いです。
- また、プログラムの合法・違法ではなくて、処理系の合法・違法に目を向けてみます。処理系の場合には well-formed だとか undefined behavior だとかいったものはないので、単に適合する処理系を合法とすればよく、そこに迷いはありません。プログラムの場合も適合を合法の基準として、処理系の合法と統一感を出したいところです。
- 更に、C++警察的にはできるだけ沢山のコードを検挙したいので、より厳しい "適合" を合法の基準にしたいところです。
そんな訳で合法 = 適合と考えるのが良いと思うのです。
11. 未解決問題
地図上に記した2つの×印についてです。誰か助けて下さい!
11.1 ill-formed NDR の謎
- ill-formed NDR な意味規則違反は定義上存在しないはずなのに、そのような規則違反が規格にいくつも明記されている?
ill-formed は well-formed でないということでした。という事は、ill-formed は構文規則、診断対象意味規則、ODR のどれかに違反しているという意味です。非ODR NDR 意味規則の違反は ill-formed ではありません。言い換えると ill-formed かつ NDR な違反は、構文規則違反か ODR 違反でしかありえません。
つまり地図の左側の×印で示したマス目は本来存在しないはずなのです。しかし、実際にそこに位置する規則があります。規格をよく調べてみると ill-formed NDR は以下のように沢山あります: §6.8/¶3, §7.1.5/¶5, §7.1.5/¶6, §7.5/¶5, §7.6.3/¶2, §7.6.8/¶1, §12.6.2/¶6, §13.5.8/¶1, §14.3.3/¶2, §14.5.6.1/¶6, §14.6/¶8, §14.6.4.1/¶8, §14.7.2/¶11, §14.7.3/¶6。何れも ill-formed; no diagnostic required などと明記されています。先ず、これらは §3.2 の外にあるので ODR ではありません。上で列挙したものは (中には構文規則かもと思われるものもありますが) 内容を見ると全てが構文規則であるようには思われません。つまり、ill-formed NDR な意味規則違反が存在しています。規格の記述と矛盾しています。
どういうことなのでしょう。何かをミスっているのでしょうか。ill-formed の定義は明確です。迷うところではありません。だとすると構文規則・意味規則・ODR の関係について何かミスしているのでしょうか。どうも、構文規則と意味規則の狭間に何か答えがある気がしているのですが結局よく分からないというのが現状です。
それとも規格がテキトウなのでしょうか。改めて考えて見るに、そもそも個別の規則違反について ill-formed であると指定することが変です。ill-formed という語には既に定義があり、個別の規則違反が ill-formed かどうかはその定義に従って自動的に決まるものです。規則違反毎に指定できるものではないはずです。何か述べるとしても「この規則違反は ill-formed とする」ではなくて「この規則違反はこれこれの理由で ill-formed となる」という形でなければならないはずです。
うーん。何か変です。
11.2 unsupported conditionally-supported constructs の謎
- プログラムが或る conditionally-supported 機能を使っていて、処理系がその機能に対応していない場合でも、適合する処理系はそのプログラムを正しく実行しなければならない?
もう一回 §1.4/(2.1) と §1.4/(2.2) を見て下さい。
- (2.1) If a program contains no violations of the rules in this International Standard, a conforming implementation shall, within its resource limits, accept and correctly execute [Footnote] that program. [Footnote: "Correct execution" can include undefined behavior, depending on the data being processed; see 1.3 and 1.9.]
(2.1) プログラムがこの国際規格の規則に何も違反していないとき、適合する処理系はそのリソースの限りにおいてそのプログラムを受領し正しく実行※しなければならない。[※ "正しく実行" とは (処理するデータに依存して起こる) 未定義の動作も含む。]- (2.2) If a program contains a violation of any diagnosable rule or an occurrence of a construct described in this Standard as "conditionally-supported" when the implementation does not support that construct, a conforming implementation shall issue at least one diagnostic message.
(2.2) プログラムが、診断対象規則の違反、またはこの国際規格で "条件付き対応" とされた構成要素で処理系が対応していないものを一つでも含む場合は、適合する処理系は少なくとも一つの診断メッセージを発行しなければならない。
(2.2) では丁寧に条件付き対応 (conditionally-supported) の場合が言及されていますが、直前にある (2.1) では言及されていません。何か変です。表にすると以下のようになります。
状況 |
§1.4/(2.1) 規格は処理系に 正しい実行を要求 |
§1.4/(2.2) 規格は処理系に 診断情報の出力を要求 |
---|---|---|
診断対象規則違反 | - | |
診断対象でない規則違反 | - | - |
プログラムの使っている 条件付き対応機能に 処理系が対応していない |
表の一番下の行です。規格は、診断情報を出すことを要求しつつも、同時にプログラムを受け入れ正しく実行することも要求しています。対応していない機能を正しく実行することなんてできません。適合する処理系に残された手段として以下の 2 つがあります。
- Plan A: (2.1) にある例外条件「within its resource limits (そのリソースの範囲内で)」を利用する。つまり処理系はメモリを使い果たすなどして自爆すればOK。
- Plan B: そもそも未対応という状況にならないように、全ての条件付き対応の機能に予め対応しておく。
Plan A (自爆) などという酷い案を採用するわけには行きません。適合する処理系は plan B を採るしかない気がします。しかしそうするとそもそも規格で機能に条件付き対応という指定をする意味がありません。
おかしいですね。何を読み間違えているのでしょう。
- 条件付き対応の機能は実は resource の一部なのでしょうか?
だとすれば条件付き対応の機能に未対応 = resource 不足ということになって、処理系は実行をしなくてよくなります。でも、そんなの変です。
- 未対応機能を使ってはならないという規則があるのでしょうか?
だとするとその機能を使ったプログラムは規則違反なので、処理系は実行しなくてよくなります。しかし、そのような規則は探しても見つかりませんでした。もし暗黙にそういう規則があるとしても、NDR とも UB とも指定されていないのでそれは診断対象規則であり、(2.2) で診断対象規則違反と未対応機能使用を両方述べていることが理解できません。
- 未対応機能の使用は UB なのでしょうか?
だとすると (2.1)/Footnote により、処理系がそれを無視しても正しい実行と見なされます。でも、やはり UB とは何処にも書かれていない気がします。
うーん。謎です。
12 謝辞
冒頭に載せた図は C++ の会 Slack における @yumetodo さん、@kazatsuyu さんの御協力があって作成・修正されました。また、加えて @agate_pris さん、@ignis_fatuus さん、@yohhoy さんも含めた議論の内容を大いに参考にさせて頂きました。簡単ですがここで感謝させて下さい。
但し、この記事で行った様々な解釈に関しては私が勝手に考えて書いたものであり、間違いがあったとしても上記の方々の関知するところではありません。叩くのは私にして下さい。
余談1
それは或る夜[2016-10-27]のことでした。或る男がうとうととしていたところ夢のお告げがあって、未定義動作や ill-formed について、何かこのようなぼやっとしたイメージを与えられたのです。
男はこれは宝の地図に違いないと夢うつつに思いました。男は朝になって規格を改めて読み、地図作成について思いを馳せます。すると話はそんなに簡単ではないということに気づきます。それでも、あれこれと悩みつつ夢のイメージを具体化していきます。村人の手を借りながらも何とか形が出来あがったのですが、そこで今度は猫のお告げがあります。お告げにより幾つかの修正が入りまた地図は完成に近づきました。しかしお告げの代償として地図の左側に矛盾があることも分かりました。男は気づきました。何か致命的なことを間違えていると。しかし、いくら考えてもどこを間違えたのか分かりません。
そうこうする内に約10年[40日]の月日が経ち、男は地図のことはすっかり忘れて暮らしておりました。ところが、再び夢のお告げがあります。地図を完成させる機が熟したのです。世に広く問いかけて地図の矛盾を解き明かすのです。男は決心しました…(つづく)
余談2
タグをつけている時に 規格書リーディング という単語が思い浮かんだのでつけたのですけれど、この単語なんか響きが変な気がします。何か微妙に間違えているかなと思って検索してみるとちゃんとたくさん記事があります。あれ…でも…全て この記事 とそれに対する反応ではありませんか。
うーん。でも確かに代わる良い単語が思い浮かびません。敢えていうなら規格書講読 (購読じゃありません講読です) ですかね。まあいいや。
あとこのポエムってタグはなんですか。Qiita で個人的な感想っぽい記事を投稿するときのタグかと思っていたのですけれど、落ち着いて考えてみたらポエムって poem ですよね。詩ですよね。もしかして今まで見落としていただけで実はポエムタグのある Qiita 記事には詩が含まれているのですか? 再確認してしまいました。結果、詩が含まれているポエム記事は一つもありませんでした。よく考えたらエッセイ (随筆・随想) じゃありませんかね。いやまあ、"ポエム" は単に "恥ずかしい文章 (謙遜)" の隠喩ですかね。
註
-
合法だとか違法だとか言いたくて仕方がない人のこと? たぶん。 ↩
-
C++警察に対抗する準備でしょうか、それとも C++警察に成る準備でしょうか…どちらでしょうね ↩
-
BNF で検索するとお金持ちの人が出てきますが違います。こっちです → バッカス・ナウア記法 (Backus-Naur form)。 ↩
-
これによると構文規則を満たさないコードはそもそも "プログラム" ではないということになるのでしょうか? ↩
-
cppreference の記事では "副作用のない無限ループ" が挙げられています。しかし規格を見てもそのような記述はどこにも見当たりません。外部リンク Undefined Behavior and Fermat’s Last Theorem へ行くと、"外部状態を変更しない無限ループは UB と規格に定められている" と書いています (これは嘘の記述です。直接には定められていません)。この記事は更に C Compilers Disprove Fermat’s Last Theorem – Embedded in Academia を参照していて、この記事から更に上記引用の内容を含む頁にリンクが貼られています。更に "副作用のない" だとか "外部状態を変更しない" 無限ループという説明は、上記引用内容に照らしても誤っています。 ↩
-
Stack Overflow の記事では直接上記引用内容を含む頁にリンクが貼られています。 ↩
-
皆が皆、規格の規則の構成について熟知しているわけではない中、どのような態度で合法・違法という単語を使っているのか調べても曖昧な結果しか得られない気もします。 ↩