先日,「\label
によって直前の \refstepcounter
された値が取得できるはずなのに,自分の文書中では正しく動かない」という質問を受けて,状況を調べてみたら,次のような罠にはまっていました。
状況
- 自分でカウンタを用意しており,そのカウンタの
\refstepcounter
は自作コマンドの中で自動でなされる。 \label
付けをするのは,必要なときに限って自分で地の文中で行う。
このような場合,\refstepcounter
と \label
の距離があまり離れないように注意せねばなりません。\refstepcounter
と \label
の距離が離れると,次のような場合に正しくカウンタ値が取得できない恐れが高まります。
- 最後に
\refstepcounter
してから\label
を発行するまでの間に使っている命令が,内部的に\refstepcounter
を呼び出しており,その値に“汚染”されてしまう。 \refstepcounter
の実行がグループ内であって,\label
の発行がそのグループ終了後になってしまうと,「最後に\refstepcounter
された値」として保持されている値がグループ終了時に消えてしまう(グループに入る前の値に書き戻される)。
1点目については分かりやすいでしょう。2点目について,具体的なコード例を示しておきましょう。例えば,次のような構図です。
悪いコード例
\documentclass{jlreq} \newcounter{TheoremCount}% 自作カウンタ \newcommand\stateTheorem[1]{% 定理番号をインクリメントして定理番号とその内容を出力する命令 \begin{center}% \refstepcounter{TheoremCount}% 定理\arabic{TheoremCount}:#1% \end{center}% } \begin{document} \stateTheorem{ほげは存在する。} \stateTheorem{ふがは存在しない。}\label{ふがの定理} 定理\ref{ふがの定理}より,…… % => 正しく参照されない! \end{document}
こういうコードの場合,\refstepcounter{TheoremCount}
は center
環境内で実行されています。それに対し,\label{ふがの定理}
は \end{center}
の後で実行されています。\begin{xxx}
~\end{xxx}
の環境内はグルーピングされているので,保持されている値が消えてしまい,\label{ふがの定理}
でそれを捕捉できないわけです。
対策
このように,\label
を地の文で打つと,このように \refstepcounter
と \label
の距離が離れ,\refstepcounter
の値の確実な捕捉が難しくなります。
上記のコード例の場合は,単に \refstepcounter{TheoremCount}
を \begin{center}
の前に移せば済むわけですが,実際の凝った文書の場合,文書構造の制御やレイアウトのためのコードが入れ子になって用いられることが多く,「ここに移せば確実に最も外側に来る」ということが必ずしも保証できるわけではありません。
そこで,\label
を地の文に打つのを止めて,\label
をコマンド内で \refstepcounter
の直後に発行できるような構造にしておくのが安全でしょう。
安全なコード例
\documentclass{jlreq} \usepackage{ifthen} \newcounter{TheoremCount}% 自作カウンタ \newcommand\stateTheorem[2][]{% 定理番号をインクリメントして定理番号とその内容を出力する命令 \begin{center}% \refstepcounter{TheoremCount}% \ifthenelse{\equal{#1}{}}{}{\label{#1}}% 第1引数が空でなければ \label 発行 定理\arabic{TheoremCount}:#2% \end{center}% } \begin{document} \stateTheorem{ほげは存在する。}% ラベルなしの場合 \stateTheorem[ふがの定理]{ふがは存在しない。}% ラベルありの場合 定理\ref{ふがの定理}より,…… % => 正しく参照される \end{document}
このようにすることで,\refstepcounter
と \label
が連続することが保証できるようになります。
原因探究と別解
latex.ltx
で \refstepcounter
と \label
の定義を読んでみます。
\def\refstepcounter#1{\stepcounter{#1}% \edef\@currentcounter{#1}% \protected@edef\@currentlabel {\csname p@#1\expandafter\endcsname\csname the#1\endcsname}% } \def\label#1{\@bsphack \protected@write\@auxout{}% {\string\newlabel{#1}{{\@currentlabel}{\thepage}}}% \@esphack}
このように,\stepcounter
(このカウンタインクリメント操作はグローバルに効く)の後で,\protected@edef\@currentlabel
によってカウンタ値を保存し,それを \label
で捕捉していることが分かります。
このとき,\protected@edef
によって \@currentlabel
はローカルにしか定義されないので,グループ外に出たときに値が消えてしまうわけですね。
よって,\refstepcounter
の定義において,\protected@edef
を,そのグローバル版である \protected@xdef
に差し替えてしまうことで強引な解決を図るという手も考えられます。
etoolbox
パッケージが提供する \patchcmd
を用いると,次のようにして,\refstepcounter
の定義中の \protected@edef
を \protected@xdef
へと差し替えられます。
\patchcmd{\refstepcounter}{\protected@edef}{\protected@xdef}{}{}
このように定義を差し替えておくと,元の「悪いコード」の方でも意図通り動くようになります。
\documentclass{jlreq} \usepackage{etoolbox}% jlreq.cls からは自動で呼ばれるのでなくてもOK \makeatletter \patchcmd{\refstepcounter}{\protected@edef}{\protected@xdef}{}{} % 定義差し替え \makeatother \newcounter{TheoremCount}% 自作カウンタ \newcommand\stateTheorem[1]{% 定理番号をインクリメントして定理番号とその内容を出力する命令 \begin{center}% \refstepcounter{TheoremCount}% 定理\arabic{TheoremCount}:#1% \end{center}% } \begin{document} \stateTheorem{ほげは存在する。} \stateTheorem{ふがは存在しない。}\label{ふがの定理} 定理\ref{ふがの定理}より,…… % => 正しく参照される \end{document}
ただし,latex.ltx
が提供するコマンドの定義を変更するというのは,影響が大きくなりうる危険な技ですので,自分の使用する文書内で閉じた利用に留めておくのが無難でしょう。