マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

もう脆弱性なんて怖くない(※ただしLaTeXの意味で)(3)

前回の続き)
保護しても頑強にならないという罠

それでは話を \NabeAzzLike に戻そう。問題で求められているのは「\NabeAzzLike を頑強にすること」である。すると、最も単純な解決方法は、そのマクロを「生来的に保護付」にする、つまり \DeclareRobustCommand で定義すればよいように思える。

\DeclareRobustCommand*\NabeAzzLike[1]{%
  \expandafter\nabeazz\expandafter{\number\csname c@#1\endcsname}}

ところが、実際に test1.tex\NabeAzzLike の定義を上記のものに変えたものをコンパイルすると、今度も誤った値が出力されてしまう。(全部「3」になるはずである。)何故だろうか。

例としてカウンタを enumi とする。そもそも \NabeAzzLike{enumi} の「正しい動作」は、「カウンタ enumi の現在の値を \nabeazz を通して出力すること」のはずである。「引数が動く」(保護付完全展開が起こる)場合に本当に正しく動くか確かめよう。\NabeAzzLike{enumi} の「保護付完全展開」の結果は、\NabeAzzLike が(生来的に)保護されているため、そのまま \NabeAzzLike{enumi} となる。そして、後で(展開限定でない文脈で)実行しようとすると、まずは \nabeazz{<enumi の値の10進表記>} に展開される。そして後は(展開限定でないので)脆弱であっても完全展開可能でなくても問題にならずに出力が行われる。万事うまくいっているように見えるが、実は大きな落とし穴がある。さっき \NabeAzzLike{enumi}\nabeazz{...} に展開すると述べたが、実はその引数である「enumi の値」というのは、「後で」実行したときの値なのである。「正しい動作」であるためには、「保護付完全展開」を行った時の値が使われなければならない。(「引数が動いた」からといって結果が変わってはいけない。)従って、実は上記の定義の \NabeAzzLike は(この記事の定義に従うと)頑強ではないということになる。だから、相互参照が絡んだ時に異常な結果になったわけである。

本当に頑強な \NabeAzzLike

それでは、本当に頑強な \NabeAzzLike を得るにはどうすればよいか。\NabeAzzLike 自体を保護するという方法は原理的に上手くいくはずがない。何故なら、その場合、保護付完全展開の結果は常に \NabeAzzLike{enumi} となり、そのトークン列には「展開時の enumi の値」の情報が残っていないからである。これを逆に考えると、保護付完全展開の際に enumi のカウンタ値への展開は必須であることが判る。もう一度、一番最初の \NabeAzzLike の展開のプロセスを見てみよう。(enumi の値を 42 とする。)

\NabeAzzLike {enumi}
\expandafter \nabeazz \expandafter {\number \csname c@enumi\endcsname }
\nabeazz {42}\number が展開された]
→ [\nabeazz が展開される]

enumi の値(42)が現れる 3 段目までの展開は必要である。一方で、脆弱である \nabeazz が展開されるともう結果の正しさが保証されない。だから、3 段目において、\nabeazz が保護されている形、すなわち「\protect\nabeazz{42}」となるようにすればよい。すると、\NabeAzzLike の定義を以下のようにすればよいことが判るだろう。

\def\NabeAzzLike#1{%
  \expandafter\protect\expandafter\nabeazz\expandafter{\number\csname c@#1\endcsname}}

これでようやく頑強な \NabeAzzLike が実現できた。まあ恐らくは多くの人は最初の(\NabeAzzLike 自体を保護する)失敗例でなくこっちを先に思いつくと思われるが、「無闇に保護を行っても頑強にならない場合がある」ことに対する注意を喚起するために敢えて失敗例を出してみた。

(まだ続く?)