C++ での C 文字列の比較
はじめに
C 文字列 (NTBS; null-terminated byte string) の等値比較について。
若手プログラマーが書いたコードのレビュー中に遭遇した問題がきっかけです。
前提
第三者が提供するライブラリの API にこのような関数があります:
#define MAX_VALUE_LENGTH xxx
void GetValue(const char *keyName, char *valueBuffer);
ある種の Key-Value ストアで、 Key を指定して Value を取得する関数です。
主な仕様は次のとおりです:
- Key も Value も C 文字列 (NTBS) です。
- Value の最大長は固定で
MAX_VALUE_LENGTH
マクロで提供されており、それゆえにGetValue
関数にはバッファサイズを受け取る引数はありません。 - 引数
keyName
で指定された Key が存在する場合は、それに対応する Value が引数valueBuffer
の先に書き込まれ、 null 終端されます。 - 引数
keyName
で指定された Key が存在しない場合は、引数valueBuffer
の先に null 終端文字のみが書き込まれます。
問題となったコード
さて、レビューに持ってきてくれたコードはこんな感じのものでした:
#include <string.h>
#include <keyvalue.h>
#define HOGE_KEY "xxx"
#define HOGE_VALUE_A "aaa"
#define HOGE_VALUE_B "bbb"
void hoge() {
char buffer[MAX_VALUE_LENGTH] = "";
// Key "xxx" に対応する Value を得る
GetValue(HOGE_KEY, buffer);
if (strncmp(buffer, HOGE_VALUE_A, strlen(HOGE_VALUE_A)) == 0) {
// Value が "aaa" の場合の処理
} else if (strncmp(buffer, HOGE_VALUE_B, strlen(HOGE_VALUE_B)) == 0) {
// Value が "bbb" の場合の処理
} else {
// それ以外の場合の処理
}
}
いくつか改善すべき点はありますね。 たとえば...
-
<string.h>
ではなく<cstring>
をインクルードし、strncmp
やstrlen
関数は名前空間名std::
で修飾しよう。 - 文字列リテラルはマクロではなく定数で扱おう。
- グローバル名前空間を汚さないようにしよう。
何が問題?
それよりももっと深刻な問題は strncmp
関数の第 3 引数です。
たとえば、 GetValue
関数に Key "xxx"
を渡したときに "aaa2"
が返ってきたらどうでしょうか?
コードを書いたプログラマーの期待値はもちろん「それ以外の場合の処理」です。
でも、最初の if 文の条件式 strncmp(buffer, HOGE_VALUE_A, strlen(HOGE_VALUE_A)) == 0
は buffer
の中身が "aaa2"
の場合にも真となります。
先頭から 3 文字分しか比較していないからです。
そのため、実際の Value は "aaa2"
にも関わらず、「Value が "aaa" の場合の処理」が実行されてしまいます。
どうしたらいい?
対策を考えてみましょう。
"aaa"
と "aaa2"
を等しいと誤判定してしまったのは、文字列を途中までしか比較していなかったからでした。
strcmp
関数を使う
代わりに 比較に strncmp
関数を使っているのは、比較対象のバッファが null 終端されていない場合にバッファオーバーリード (Wikipedia) を防ぐためです。
しかし、今回のケースでは、第 1 引数も第 2 引数も必ず null 終端されることが明らかです。
そのため、 strncmp
関数の代わりに strcmp
関数を使っても問題とはなりません。
if (std::strcmp(buffer, HOGE_VALUE_A) == 0) {
// Value が "aaa" の場合の処理
とはいえ、コードを見ただけでは、常に null 終端されているのか、されていない場合があるのかを区別できないこともあるでしょう。
null 終端される保証がない文字列の比較には strncmp
関数を使うべきですね。
そのため、 strcmp
関数を使うというのは実際には難しいかもしれません。
また、 strcmp
のように長さを指定しない <cstring>
の関数の使用が禁止されていることもあります。
たとえば、主に自動車業界で使われている (使われていた?) コーディングルール MISRA C++:2008 にはそのものずばりのルールがあります[1]。
コーディングルールの運用についてはこの記事では触れません。
先に長さを比較する
引き続き strncmp
関数を使うのであれば、先に文字列の長さを比較すればよいですね。
長さが等しくない文字列同士は必ず異なるので。
if ((std::strlen(buffer) == std::strlen(HOGE_VALUE_A)) &&
(std::strncmp(buffer, HOGE_VALUE_A, std::strlen(HOGE_VALUE_A)) == 0)) {
// Value が "aaa" の場合の処理
strncmp
関数の第 3 引数は strlen(buffer)
でも strlen(HOGE_VALUE_A)
でもどちらでもよいのですが、片方が文字列リテラルであればそちらを使う方がコンパイル時の最適化が効くので望ましいです。
std::string
クラスを使う
ここまでは C 文字列 (NTBS) をそのまま扱う方法を見てきました。
でも、 C++ であれば素直に std::string
クラスを使いましょう。
char buffer[MAX_VALUE_LENGTH] = "";
// Key "xxx" に対応する Value を得る
GetValue(HOGE_KEY, buffer);
if (std::string s{buffer}; s == HOGE_VALUE_A) {
// Value が "aaa" の場合の処理
使い慣れた ==
演算子で等値比較できます。
null 終端文字とかバッファオーバーリードとか、めんどうな上にセキュリティ上のリスクになり得るようなことは考えずにすみます。
std::string_view
クラスを使う
比較のためだけに一時的に std::string
オブジェクトを生成するのは (文字列の長さによっては動的なメモリ確保が行われるので) ちょっと... と気になる人もいるかもしれませんね。
std::string
クラスの代わりに std::string_view
クラスを使う方法もあります。
こちらは動的メモリ確保も文字列のコピーも行われないのでモア・ベターかなと思います。
char buffer[MAX_VALUE_LENGTH] = "";
// Key "xxx" に対応する Value を得る
GetValue(HOGE_KEY, buffer);
if (std::string_view sv{buffer}; sv == HOGE_VALUE_A) {
// Value が "aaa" の場合の処理
ただし、「std::string_view
って何?」というプログラマーが支配的なチーム/組織では控える方がよいかもしれません。 (std::string_view
に限りませんが)
おわりに
<cstring>
が提供する関数を一般のアプリケーションのコードで使うのはやめましょう。
-
今手元に同書がないので確認できていませんが、たぶん Rule 18-0-5 だと思います。 ↩︎
Discussion