C言語の標準関数を自作する:番外編 コーディング規約〜「ダメ文字」の罠〜
目次
はじめに
突然ですが、私の作っている「C言語の標準関数を自作する」シリーズには、ちょっと細かいコーディング規約があります。
その中の一つが、
//によるコメントの文末には、半角ピリオド.を付ける。
というルールです。
例えば、
// 文字列の長さを取得します.
lv_Length = my_strlen(ap_String);
という書き方をします。
「……いや、そこまで決める必要ある?」
と思った方もいるかもしれません。
確かに、ぱっと見では単なる見た目のこだわりにしか見えません。
// 文字列の長さを取得します
でも、
// 文字列の長さを取得します.
でも、人間が読めば意味は同じです。
では、なぜわざわざこんなルールを作っているのでしょうか。
実はこれ、過去に私自身がC言語を書いていて、かなり意味の分からないエラーに遭遇したことがきっかけになっています。
今回は、そのときに遭遇した「ダメ文字」の話をしてみたいと思います。
1. ある日、突然おかしくなった
以前、私は「モバイルC」というアプリを使って、趣味でC言語のコードを書いていました。
ある日、いつものようにコードを書いて、コンパイルしてみると……。
なぜかエラーが発生しました。
しかし、コードを見ても原因が分かりません。
エラーが出ている場所も、どうもおかしい。
そこで、いろいろとコードを確認していきました。
そして、あることに気付きました。
直前の // コメントを削除すると、正常に動作する。
……。
なぜ?
コメントですよ?
コメントなんて、プログラムの処理には関係ありません。
それなのに、あるコメントを書いていると、
コンパイルエラー
そのコメントを削除すると、
正常にコンパイルできる
という状態でした。
「そんなことある?」
という感じでした。
2. コメントなのに、なぜコードに影響するのか
C言語では、当然ですがコメントはプログラムとして実行されません。
例えば、
int lv_Value = 10;
// 値を表示します.
printf("%d\n", lv_Value);
と書いても、
// 値を表示します.
が何かの処理として実行されるわけではありません。
そのため、普通に考えれば、
コメントを追加しただけでコンパイルエラーになる
なんてことは、かなり不思議です。
ところが、C言語のソースコードは、単純に「人間が見ている文字列」として処理されているわけではありません。
コンパイラは、ソースコードを一定のルールに従って変換しながら処理していきます。
ここに今回の罠があります。
3. C言語は文字コードの影響を受ける
C言語のソースコードには、日本語などのマルチバイト文字を含めることができます。
例えば、
// 文字列の長さを取得します
というコメントです。
人間から見ると、
文字列の長さを取得します
という一つの文章です。
しかし、ソースコードは最終的にはコンピュータ上のバイト列として扱われます。
日本語などのマルチバイト文字は、1バイトだけで表現されるとは限りません。
例えば、ある文字コードでは、日本語の1文字が2バイトで表現されます。
ここで問題になるのが、
マルチバイト文字の2バイト目が、特定の制御文字と同じ値になることがある
ということです。
特に昔からC言語で問題になってきたのが、バックスラッシュ \ に相当するバイト 0x5C です。
4. 「ダメ文字」の正体
例えば、Shift_JISなどの文字コードでは、特定の日本語文字の2バイト目が 0x5C になる場合があります。
0x5C は、ASCIIではバックスラッシュ \ に対応します。
つまり、人間から見ると、
日本語の文字
なのに、バイト列として見ると、
[1バイト目][0x5C]
のようになっている場合があります。
これが、C言語のソースコードを扱う上で問題になることがあります。
なぜなら、C言語ではバックスラッシュには特別な意味があるからです。
5. バックスラッシュには特別な意味がある
C言語のソースコードでは、バックスラッシュ \ はエスケープシーケンスなどに利用されます。
例えば、
printf("Hello\n");
の、
\n
です。
また、C言語のソースコードを処理する初期段階では、バックスラッシュと改行の組み合わせにも特別な意味があります。
ここが今回のポイントです。
例えば、
// コメント\
次の行
のように、コメントの行末にバックスラッシュが存在すると、次の行までコメントとして扱われる原因になります。
すると、人間から見ると、
// コメント\
int lv_Value = 10;
なのに、コンパイラ側では、
// コメント\
int lv_Value = 10;
の2行目までコメントの一部として扱われてしまう可能性があります。
その結果、
int lv_Value = 10;
が存在しないことになってしまいます。
そして、その後のコードで、
printf("%d\n", lv_Value);
などと書いていれば、
lv_Value が見つからない
といった、一見すると全然関係のない場所でエラーが発生することになります。
これが、私が以前遭遇した「コメントを消すと直る謎のエラー」の正体でした。
6. これが「ダメ文字」
このように、特定の文字コードや処理系の組み合わせによって、ソースコード上では普通の日本語に見える文字が、コンパイラにとって特別な意味を持つバイトを内部に含んでいることがあります。
こうした文字は、一般に「ダメ文字」と呼ばれることがあります。
ただし、ここで少し厄介なのが、
ダメ文字は環境によって変わる
ということです。
使用する文字コードやコンパイラ、処理系などによって、問題になる文字が異なる場合があります。
そのため、
「この文字さえ使わなければ絶対に大丈夫」
と単純に決めることはできません。
また、現在ではUTF-8が広く使われているため、昔のShift_JIS環境とまったく同じ問題が常に発生するわけでもありません。
とはいえ、
「ソースコード中の文字が、見た目通りに単純な1文字として処理されるとは限らない」
ということを知っておくのは、C言語を書く上で無駄ではないと思います。
7. では、なぜ文末にピリオドを付けるのか
ここで、冒頭のコーディング規約に戻ります。
本プロジェクトでは、
//によるコメントの文末には、半角ピリオド.を付ける。
というルールを設定しています。
例えば、
// 文字列の長さを取得します.
です。
これは、単なる見た目の問題ではありません。
例えば、問題となるバイトがコメントの最後に存在して、
[問題となる文字]
改行
という状態になると、処理系によっては問題が発生する可能性があります。
そこで、コメントの最後に必ず半角ピリオドを置きます。
[問題となる文字].
改行
こうすると、問題となるバイトが行末に残りません。
もちろん、このルールだけですべての文字コード問題を解決できるわけではありません。
しかし、
コメントの文末に問題となるバイトが直接現れることを避ける
という意味では、非常に簡単な予防策になります。
私自身、過去にこの問題で実際に悩んだ経験があるため、プロジェクトのコーディング規約として明示的にルール化することにしました。
8. 「そんな細かいルールいる?」への答え
ここまで読んで、
でも、コメントの最後に
.を付けるだけって、本当にそこまで必要?
と思う方もいるかもしれません。
正直なところ、現在の一般的な開発環境で、必ずこの問題が発生するわけではありません。
UTF-8を前提とした環境で、適切なコンパイラやエディタを使っているのであれば、今回のような問題に遭遇する可能性はかなり低くなっています。
それでも、私はこのルールを残しています。
理由は単純です。
コストがほとんどないからです。
// 文字列の長さを取得します
を、
// 文字列の長さを取得します.
と書くだけです。
これによってコードの意味が変わるわけでもありません。
それなら、
「過去に実際に遭遇した問題を、簡単なルールで予防できるなら、最初からルールにしてしまおう」
と考えました。
9. コーディング規約は「見た目」だけではない
コーディング規約というと、
インデントは4スペース
変数名はPascalCase
関数名はsnake_case
など、コードの見た目を統一するためのルールを思い浮かべる方も多いと思います。
もちろん、それもコーディング規約の重要な役割です。
しかし、コーディング規約には、
人間が読みやすくする
だけではなく、
ミスを防ぐ
という役割もあります。
今回の、
//によるコメントの文末には半角ピリオド.を付ける。
というルールも、その一例です。
一見すると、
ただの見た目のこだわり
に見えます。
しかし実際には、
過去に遭遇した問題
↓
原因を調査
↓
文字コードによる問題を発見
↓
再発防止策を考える
↓
コーディング規約にする
という流れで生まれたルールです。
10. 「知っている」と「ルールにする」は別
個人的には、今回の経験からもう一つ学んだことがあります。
それは、
問題を知っているだけでは、再発防止にならない
ということです。
例えば、
「そういえば、コメントの最後の文字によっては
変な問題が起きることがあったな」
と知っていたとしても、毎回意識して確認するのは大変です。
人間なので、いつか忘れます。
そこで、
コメントの文末には半角ピリオドを付ける
というルールにしてしまいます。
すると、
毎回思い出す
↓
毎回確認する
必要がなくなります。
コードを書くときに自然と守れるようにしておけば、それだけで一定のリスクを減らせます。
11. 今回の話から分かること
今回の話は、
日本語をコメントに書くな
という話ではありません。
また、
コメントの最後には絶対にピリオドを付けなければならない
という話でもありません。
重要なのは、
ソースコードは、人間が見ている文字だけでできているわけではない
ということです。
特にC言語では、文字コードやプリプロセッサ、コンパイラの処理など、普段は意識しない部分がソースコードの解釈に影響することがあります。
そして、そうした問題を一度経験したからこそ、
// コメント.
という小さなルールが生まれました。
最初は、
「なんでこんな細かいルールがあるんだ?」
と思われるかもしれません。
でも、その裏側には、
過去の失敗
↓
原因の調査
↓
再発防止
という理由があります。
12. まとめ
今回は、本プロジェクトのコーディング規約の一つである、
//によるコメントの文末には半角ピリオド.を付ける。
というルールについて紹介しました。
一見すると、単なる見た目の統一に見えるルールです。
しかし、実際には私自身がC言語を書いているときに遭遇した、
コメントを削除すると直る謎のコンパイルエラー
がきっかけになっています。
原因を調べていくと、文字コードによってはマルチバイト文字の一部が、C言語のソースコード上で特別な意味を持つバイトと一致する場合があることが分かりました。
特に、コメントの末尾にバックスラッシュ \ に相当するバイトが現れると、処理系によっては次の行までコメントとして扱われる原因になることがあります。
そこで、本プロジェクトでは、
// コメント.
のように、コメントの文末に半角ピリオドを付けるルールを採用しています。
もちろん、これはすべての文字コード問題を解決する万能な方法ではありません。
それでも、
過去に遭遇した問題
↓
原因を理解する
↓
簡単なルールにする
↓
同じ問題を繰り返さない
という考え方は、コーディング規約を作る上で大切なのではないかと思います。
というわけで、
「コメントの最後に
.を付けるなんて、見た目へのこだわりでしょ?」
と思っていた方。
実は、ちゃんと理由があったんです。
🔗 関連記事・関連リンク
この記事の最新アップデートや、このシリーズの関連記事は以下のリンクからご覧いただけます。
内容は基本的に同じですので、お好みのサイトでお読みください。
🟢 Zenn
-
この記事の最新版はこちら。
-
https://zenn.dev/malloc/articles/08_extra02_codingrules_damemozi
-
「標準関数自作」Topicの関連記事はこちら。
🔵 Qiita
-
「標準関数自作」タグの関連記事はこちら。
🧡 note
-
このシリーズの記事はこちら。