TeX Alchemist Online

TeX のこと,フォントのこと,Mac のこと

\refstepcounter & \label 使用上の注意点

先日,「\label によって直前の \refstepcounter された値が取得できるはずなのに,自分の文書中では正しく動かない」という質問を受けて,状況を調べてみたら,次のような罠にはまっていました。

状況

  • 自分でカウンタを用意しており,そのカウンタの \refstepcounter は自作コマンドの中で自動でなされる。
  • \label 付けをするのは,必要なときに限って自分で地の文中で行う。

このような場合,\refstepcounter\label の距離があまり離れないように注意せねばなりません。\refstepcounter\label の距離が離れると,次のような場合に正しくカウンタ値が取得できない恐れが高まります。

  1. 最後に \refstepcounter してから \label を発行するまでの間に使っている命令が,内部的に \refstepcounter を呼び出しており,その値に“汚染”されてしまう。
  2. \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 が提供するコマンドの定義を変更するというのは,影響が大きくなりうる危険な技ですので,自分の使用する文書内で閉じた利用に留めておくのが無難でしょう。