CおよびC++における整数パースの危険性

文字列を整数にパースすることは、プログラミングにおける最も基本的な操作の一つのように思えます。ほとんどの現代的な言語では、Pythonのint("123")やJavaScriptのNumber("123")のように、一行で済みます。しかし、CおよびC++では、この単純なタスクが未定義の動作、サイレントなオーバーフロー、そして直感に反するAPI設計という地雷原を露呈させます。

パーサーが「正しい」と見なされるためには、理想的には3つの基準を満たす必要があります。格納される数値が文字列内の視覚的な表現と一致していること、文字列全体が消費されていること(末尾にゴミが残っていないこと)、そしてこれらの基準を満たさない場合はエラーとして明示的に報告できることです。これから見ていくように、C標準ライブラリはこれらの要件を満たすのに苦労しています。

C標準関数の失敗

Cにおいて整数パースを試みる主な方法は3つあり、それぞれに重大な注意点があります。

1. atol(): 危険なレガシー

atol()はおそらく最も問題があります。エラーを検出する方法がありません。数字ではない文字列(例: "timmy")を渡すと、0を返します。数字で始まるがゴミが含まれる文字列(例: "123timmy")を渡すと、123を返します。

さらに恐ろしいことに、CおよびPOSIX標準では、値が表現できない場合、その動作は未定義であると規定されています。理論的な意味では、これは、信頼できない入力に直面した際、atol()がプログラムをクラッシュさせることを含む、あらゆることを起こし得ることを意味します。

2. strtol(): (符号付き整数のための)唯一の実行可能なCの選択肢

strtol()は、パースが停止した場所へのポインタ(endptr)を提供し、範囲エラーの際にerrnoを設定するため、大幅に堅牢です。しかし、これを正しく使用するには、かなりの量のボイラープレートが必要です。

bool parse_long(const char* in, long* out)
{
  if (!*in) return false; // 空文字列を検出

  char* endp = NULL;
  errno = 0;
  *out = strtol(in, &endp, 0);

  if (errno) return false; // 範囲エラー
  if (*endp) return false; // 末尾のゴミ
  return true;
}

3. strtoul()sscanf(): 符号なしの罠

strtol()は符号付き数値には機能しますが、strtoul() (unsigned long) と sscanf() は多くのユースケースにおいて根本的に壊れています。主な問題は、strtoul() が先頭のマイナス記号を許可してしまうことです。-1strtoul()に渡すと、エラーを返さず、ラップアラウンドしてULONG_MAX(例: 64ビットシステムでは $2^{64}-1$)を返します。

これにより曖昧さが生まれます。結果としてULONG_MAXを受け取った場合、入力が実際に最大値のunsigned longであったのか、文字列"-1"であったのか、あるいは範囲を超えてオーバーフローした値であったのかを知ることができません。このため、標準C関数を使用して64ビットのフラグフィールドや大きな符号なし識別子を確実にパースすることは不可能です。

C++の視点

C++はこれらのC関数をラップしようと試みますが、しばしばそれらの欠陥を継承してしまいます。std::stoul()std::istringstream は、Cの対応するものと同様に動作し、しばしば負の記号をラップアラウンドを通じて符号なし型にパースすることを許可してしまいます。

現代的な解決策: std::from_chars

C++17で導入されたstd::from_charsは、初めて直感的に動作する標準ライブラリ関数です。符号なし型に対してマイナス記号を許可せず、明確なエラーコード(std::errc)とパースされたシーケンスの末尾へのポインタを提供します。

template<typename T>
std::optional<T> parse_int(std::string_view sv)
{
  T i;
  const auto [ptr, ec] = std::from_chars(sv.begin(), sv.end(), i);
  if (ec != std::errc()) return {};
  if (ptr != sv.end()) return {};
  return i;
}
```\n
## コミュニティの洞察と回避策

このトピックに関する技術的な議論は、Cコミュニティにおける分断を浮き出しにしています。一部のエンジニアは、C標準ライブラリは「レガシーな塊」であり、経験豊富なCプログラマは、パフォーマンスと正確性を確保するために、単に独自のパースロジックを自ろうと主張しています。

符号なしlongを使用しなければならないCプログラマ向けの、提案されている回避策の一つは、まず`strtol()`を使用して文字列をパースし、負の記号をチェックしてから、実際の変換には`strtoul()`を使用することです。

> "負の数、スケール外の負の数を含めて除外する... [その]後、これからは平坦な道となる `strtoul()` を使う。"

他の貢献者たちは、さらに別の落とし穴、例えば、先頭のゼロが明示的に基数を10に設定しない限り、自動的に8進数インジケーターとして解釈される「8進数の罠」を指摘しています。これは、標準的な10進数パースを期待するユーザーにとって、しばしば予期せぬ動作となります。

## 結論

Cにおける整数パースは、設計の初期決定(安全性やエラー報告よりも、単純さと最小限のオーバーヘッドを優先すること)が、いかに長期的な技術的負債を生み出すかを示す典型的な例です。Cで作業する場合、最も安全な道は、多くの場合、標準ライブラリの`strto...`ファミリーを符号なし型に対して使用することを避け、カスタムパーサーを実装するか、あるいは負の記号や空文字列を明示的にチェックする堅牢なラッパーを自こそを使うことです。

Sources