syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

C関数のis*系ルーチンの注意点

(これは2012-06-03に書いた故OCNブログの記事を移植したものです)

C言語の <ctype.h> もしくはC++の <cctype> におけるisdigit(), isalpha(), isalnum(), isupper(), islower(), isspace(), ...とかの話です。

もともとこいつらは引数の型がcharではなくintになっているんですが、渡せる数値の範囲は0x00 ~ 0xFFとEOF(たいていの処理系でEOF = -1)となってます(intなのはCの関数プロトタイプとか型の昇格に関する仕様に由来するらしいです。Cでは文字リテラルの型もcharではなくintです)。負数は-1以外指定できません。charがsignedになる場合、MBCSリーディング バイト・トレーリング バイトは負数範囲を含むので注意が必要です。MSのVisual C++のCRT(C Runtime)実装では、デバッグバージョンで範囲外整数が渡されるとアサーションが失敗してくれるようになっています。ちなみにVCのCRTにはMBCS用の_ismbcdigit(), _ismbcalpha(), _ismbcalnum(), _ismbcupper(), _ismbclower(), _ismbcspace(), ...とかあるんですが、これらはANSI MBCSのリーディング バイト・トレーリング バイトの判定処理を含むため、ASCIIバージョンでは範囲外だった負の整数値が渡されてもアサート失敗にはなりません。

で、問題なのはこれらのis*系ルーチンを使っていたコードをUnicode対応(wchar_tワイド文字対応)させるときです。

ワイド文字バージョンは、それぞれiswdigit(), iswalpha(), iswalnum(), iswupper(), iswlower(), iswspace(), ...とかなんですが、こいつらの引数はwint_t(VCだとunsigned shortのtypedef)になっています。Unicode対応させる場合に文字型をcharからwchar_tに置き換えたら、ASCII用のis*系ルーチンをisw*系に置き換えてやればいいわけですが、ASCII用のis*系ルーチンの引数は前述のとおりintであり、str系ルーチンとは違って別にchar用からwchar_t用に置き換えなくてもコンパイル エラーとかにはならないことも意識しておく必要があります。EOFに関しても、ワイド文字用はWEOFであり、VCやgcc/g++ for Windowsではそれぞれ

#define EOF (-1) // VC++, gcc/g++

#define WEOF (wint_t)(0xFFFF) // VC++

#define WEOF (wchar_t)(0xFFFF) // gcc/g++

になっているので、(コンパイル エラーにはならないけれども)EOFとWEOFには双方互換性がありません。これを忘れていると痛い目にあいます(一応-1をwint_tでキャストすると0xFFFFにはなるのですが……)。なおLinuxとかMac OS Xとかはsizeof(wchar_t) == 4UTF-32なので、さらに話がややこしくなります。

まあそれはいったんおいといて、絶対に忘れてはならないのは「isw*系ルーチンはUnicode文字のプロパティも考慮する」という特性です。例えばいわゆる全角のアルファベットや数字「L'a'」「L'A'」「L'1'」とかはiswalnum()でtrue判定になり、また全角のスペース「L' '」とかはiswspace()でtrue判定になります。なので、ASCIIのアルファベット、数字、およびスペースである必要があるのであれば、isascii(), iswascii()を併用して確実に判定する必要があります*1。特にchar型変数を、(ASCII文字のみが入っていると仮定して)要素数256以下の固定長配列のインデクサに使っていた場合は、wchar_tに置き換えたときオーバーランしないよう注意が必要。まあぶっちゃけ処理系依存*2でもかまわないのであれば、いっそ

inline bool IsAsciiAlnum(int c)
{ return ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'); }

とかを自作して、charでもwchar_tでも両方渡せるようにしてしまったほうが分かりやすくていいかも。確かUTF-8もUTF-16もUTF-32も、ASCII文字の範囲内(0x00 ~ 0x7F)は同じ値であることが保証されています

文字・文字列を扱うプログラムを書くときは、ASCII、ANSI MBCS、UTF-8 MBCS、あとUTF-16くらいは最低限理解しておきましょう。

*1:ちなみにMSVCの場合、POSIX互換のisascii()は非推奨で、処理系固有の__isascii()が推奨されています。isalnum()などはISO Cで標準化されていますが、isascii()はISO Cとしては標準化されていません。ISO Cでは、処理系固有のシンボル名は2連のアンダースコアで始めることができますが、ISO規格で標準化されていない関数にisasciiという名前を付けるのはよくないだろう、ということで、MSVCはこのPOSIX互換の名前を非推奨としているようです。

*2:従来のC/C++における文字リテラルエンコーディング処理系依存であり、ASCII準拠とは限りません。具体的に言うと、'a' == 0x61とは限らないわけです。