間違ったコードはコンパイルできないようにする

ハンガリアン記法に関する間違った意見をよく目にする。Making Wrong Code Look Wrong間違ったコードは間違って見えるようにする)の記事では、ハンガリアン記法でもアプリケーションハンガリアンは有用だと主張している。もちろん、わかりやすい変数名を使用することには賛成だが、変数名を人間が目で見て正しいかどうかを判断しなければならないようなルールに価値はないと確信している。ワインバーグではないが、計算機が得意なことは計算機にやらせればよいのであって、人間がやることではないのだ。ソフトウェア工学的に正しいのは、間違ったコードはコンパイルエラーになるようにする、ということだ。

アプリケーションハンガリアンが善だと思い込んでる素人は、この続きを読まずにマンガ喫茶にでも行って時間を過ごして欲しい。


本当のソリューションは静的解析で正しさを証明すること

前述の記事では、アプリケーションハンガリアンの例として、安全な文字列の変数名にはプレフィックスsを、安全でない文字列の変数名にはプレフィックスusをつけるルールを紹介している。こうしたルールを用いれば、間違いが一目でわかるという主旨だ。しかし、この手のことは静的解析でできることがわかっている。C言語なら静的解析ツールでSplintというものがある。Splintではコメントによるアノテーションで型を修飾することで、伝統的なlintよりも強力にコードを検証できる。Splintのマニュアルのセクション10ではアノテーションの拡張例として、taintednessを扱うアノテーションを記述する方法を説明している。これはアノテーションにtainted属性、untainted属性を追加して、次のようにコードを検証できるようにする。

int printf (/*@untainted@*/ char *fmt, ...);
char *fgets (char *s, int n, FILE *stream) /*@ensures tainted s@*/ ;
char *strcat (/*@returned@*/ char *s1, char *s2)
   /*@ensures s1:taintedness = s1:taintedness | s2:taintedness @*/

日本語によるSplintの解説としてはオープンソース・ソフトウェアのセキュリティ確保に関する調査報告書 第2部オープンソース・ソフトウェアの効率的な検査技術の調査が参考になる。また他の言語でも、例えばPerlではtaintモードで同様なことを実現できる。

もう少しわかりやすい話をしよう。例えばC言語で、「指すメモリを書き換え可能なポインタの変数名にはプレフィックスcを付け、そうでなければプレフィックスucをつける」というルールがあったら、それはすばらしいだろうか。そんなことをするよりも変数の型にconst修飾子を指定したほうがマシだろう。次のコードのように、const属性をもつポインタをconst属性のないポインタに代入すれば、コンパイラが間違いを教えてくれる。

void
func(const char *name)
{
    const char *s;
    char *t;

    s = name;
    t = name; /* 警告 */
    ...
}

とはいえ、const修飾子のように標準のものならともかく、taintednessのようなアノテーションをコードに埋め込むことは非常にコストが高くつく。もうちょっと簡単にできないのか、と考えるのが普通のエンジニアだ。


同一シグニチャのクラスを異なる名前で複数定義する

分散型バージョン管理システムのmonotoneも興味深いソリューションを使用している。同じ文字列であっても、文字列の種類ごとにそれぞれ異なるクラスを用意して、ちょっとしたタイプセーフティを提供している。

具体的にはvocab_terms.hhで定義されている次のようなクラスで、同一シグニチャのクラスを異なる名前でいくつも定義している。

ATOMIC_NOVERIFY(external);    // "external" string in unknown system charset
ATOMIC_NOVERIFY(utf8);        // unknown string in UTF8 charset
...
ATOMIC_NOVERIFY(id);          // hash of data
ATOMIC_NOVERIFY(data);        // meaningless blob
ATOMIC_NOVERIFY(delta);       // xdelta between 2 datas
ATOMIC_NOVERIFY(inodeprint);  // fingerprint of an inode

externalutf8などはクラスである。ATOMIC_NOVERIFY()マクロはvocab.hhvocab_macros.hhでかなり複雑に展開され、最終的にヘッダファイルでは次のような形に展開される。

#define hh_ATOMIC(ty)                                  \
class ty {                                             \
  immutable_string s;                                  \
public:                                                \
  bool ok;                                             \
  ty() : ok(false) {}                                  \
  explicit ty(std::string const & str);                \
  ty(ty const & other);                                \
  std::string const & operator()() const               \
    { return s.get(); }                                \
  bool operator<(ty const & other) const               \
    { return s.get() < other(); }                      \
  ty const & operator=(ty const & other);              \
  bool operator==(ty const & other) const              \
    { return s.get() == other(); }                     \
  bool operator!=(ty const & other) const              \
    { return s.get() != other(); }                     \
  friend void verify(ty &);                            \
  friend void verify_full(ty &);                       \
  friend std::ostream & operator<<(std::ostream &,     \
                                   ty const &);        \
  struct symtab                                        \
  {                                                    \
    symtab();                                          \
    ~symtab();                                         \
  };                                                   \
};                                                     \
std::ostream & operator<<(std::ostream &, ty const &); \
template <>                                            \
void dump(ty const &, std::string &);                  \
inline void verify(ty &t)                              \
  { if(!t.ok) verify_full(t); }; 

また、vocab.cc内部では次のような定義に展開される。

#define cc_ATOMIC(ty)                        \
                                             \
static symtab_impl ty ## _tab;               \
static size_t ty ## _tab_active = 0;         \
                                             \
ty::ty(string const & str) :                 \
  s((ty ## _tab_active > 0)                  \
    ? (ty ## _tab.unique(str))               \
    : str),                                  \
  ok(false)                                  \
{ verify(*this); }                           \
                                             \
ty::ty(ty const & other) :                   \
            s(other.s), ok(other.ok)         \
{ verify(*this); }                           \
                                             \
ty const & ty::operator=(ty const & other)   \
{ s = other.s; ok = other.ok;                \
  verify(*this); return *this; }             \
                                             \
  std::ostream & operator<<(std::ostream & o,\
                            ty const & a)    \
  { return (o << a.s.get()); }               \
                                             \
template <>                                  \
void dump(ty const & obj, std::string & out) \
{ out = obj(); }                             \
                                             \
ty::symtab::symtab()                         \
{ ty ## _tab_active++; }                     \
                                             \
ty::symtab::~symtab()                        \
{                                            \
  I(ty ## _tab_active > 0);                  \
  ty ## _tab_active--;                       \
  if (ty ## _tab_active == 0)                \
    ty ## _tab.clear();                      \
}

つまり、utf8クラスもexternalクラスも定義は一緒だ。しかし、utf8クラスのインスタンスを生成するときはUTF-8でエンコードされた文字列をコンストラクタに渡すように運用する(ルール化する)。同様に、externalクラスのインスタンスを生成するときは現在のロケールでエンコードされた文字列をコンストラクタに渡すようにする。このようにすれば、UTF-8でエンコードされた文字列と、現在のロケールの文字列を間違って連結したり、比較したりするようなバグを防止できる。間違えばコンパイラが教えてくれるのだ。より詳しいことは monotone wikiのI18nL10nとmonotoneのコードを参照して欲しい。

例えば、アプリケーションハンガリアンなら次のように書くコードがあったとする。

string uMessage, eCommand;

これをvocab.hhの仕組みを利用して書くと次のようになるだろう。

utf8 message;
external command;

上の2つのどちらが嬉しいのか、素人でなければすぐわかるだろう。

ただし、こうしたことを実現するためにマクロを使わなければならない、ということは残念なことだ。言語でサポートして欲しい機能だろう(単純にstringを継承してutf8externalを定義するだけでは意味がないことはわかるよね?)。


重要なのは型

もう少し言わせてもらう。アプリケーションハンガリアン、システムハンガリアンに関わらず、ハンガリアン記法それ自体に価値がないことを示そう。例えば、次のコードはアプリケーションハンガリアンだろう。

int celsiusTemp, fahrenheitTemp;
int mileLength, meterLength;

そして、次のコードもアプリケーションハンガリアンなのかもしれない。

Celsius celsiusTemp;
Fahrenheit fahrenheitTemp;

Mile mileLength;
Meter meterLength;

それとも、後者はクラス(あるいは型)が組み込みのものならシステムハンガリアンと呼ぶのだろうか。問題は変数名の記法ではなく、変数の型だ。同じ数値でも型を与えることで異なる意味をもつことを思い出して欲しい。

摂氏と華氏、マイルとメートルなど、ただの整数にプレフィックスを付けて区別することに工学的な価値はないことが理解できたと思う。そんなことに意味があるとすれば変数のプレフィックスをひたすら比較する非人道的な労働に意味があることになる。

本当にバカばっかりで疲れる。ハンガリー人はハンガリーにいればよかったんだよ。


たなか ともひさ
Last modified: Sun May 25 21:30:09 JST 2008