やれやれ疲れた。この辺でひと休みして、デバッグについて考えよう。まずはコンパイルエラーについてだ。
プログラムにはさまざまなバグがあるが、コンパイルエラーは最も簡単なバグだ。というのも、プログラムのバグの存在が実行前に発覚したわけだから、手間が省ける。もしコンパイルエラーにならない場合、実行した結果から、バグがあるかどうかを判断しなければならない。
読者の中には、せっかく書いたソースコードをコンパイルしたらコンパイルエラーが出たので、運が悪かったとか、失敗したとか、怒られてつらい気持ちになったなどと感じることがあるかもしれない。しかしそれは大違いだ。コンパイラーによって読者はプログラムを実行することなくバグが発見できたのだから、読者は運が良かった、大成功した、褒められて最高の気持ちになったと感じるべきなのだ。
さあ皆さんご一緒に、
- コンパイルエラーは普通
- コンパイルエラーが出たらありがとう
- コンパイルエラーが出たら大喜び
熟練のプログラマーは自分の書いたコードがコンパイルエラーを出さずに一発でコンパイルが通った場合、逆に不安になるくらいだ。
もしバグがあるのにコンパイルエラーが出なければ、バグの存在に気が付かないまま、読者の書いたソフトウェアは広く世の中に使われ、10年後、20年後に最もバグが発見されてほしくない方法で発見されてしまうかもしれない。すなわち、セキュリティ上問題となる脆弱性という形での発覚だ。しかし安心してほしい。いま読者が出したコンパイルエラーによって、そのような悲しい未来の可能性は永久に排除されたのだ。コンパイルエラーはどんどん出すとよい。
コンパイルエラーの原因は2つ。
- 文法エラー
- 意味エラー
- コンパイラーのバグ
3つだった。コンパイルエラーの原因は3つ。
- 文法エラー
- 意味エラー
- コンパイラーのバグ
- コンピューターの故障
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つ使ってインデントしているので、auto
のa
の列番号は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_SUCCESS
とEXIT_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のバグなので、見つけた読者は適切な方法でバグ報告をしよう。