カウンタと長さ

\setcounter\setlength は同じ形に見えて、正反対にふるまいます。両方をグループの中で実行すると、グループを抜けたあと カウンタは新しい値のまま、長さだけが黙って元へ戻ります——カーネルの定義を見ると、片方にだけ \global という一語が付いているからです。このページが扱うのは LaTeX の二つの数値レジスタ、つまり整数を入れて章・図・式の番号を動かす カウンタ\newcounter)と、寸法を入れる 長さ\newlength、その正体は dimen ではなく skip レジスタ)です。あわせて、\stepcounter の直後に置いた \label がなぜ他人の番号を指してしまうのか、\value が何に展開されるのか、\dimexprcalc\fpeval でどう計算するのか(1in = 72.27pt、1pt = 65536sp といった換算も)を見ていきます。

\newcounter でカウンタを作り、動かす

\newcounter{foo} で新しいカウンタを一つ作ります。名前はバックスラッシュなしの文字列で、作られた直後の値は 0。中身の正体は TeX の \count レジスタで、\meaning で覗くと \count196 のような答えが返ってきます。値を変える命令は三つ——\setcounter{foo}{3} が代入、\addtocounter{foo}{2} が加算(負の数で減算)、\stepcounter{foo} が 1 だけ増やす。そして省略可能な第二引数 \newcounter{foo}[⟨parent⟩] を付けると fooparent の従属カウンタになり、parent が一つ進むたびに foo が 0 に戻ります。節が変わると小節番号が振り出しに戻る、あの挙動の正体です。

latex
\newcounter{trial}            % created with value 0
\setcounter{trial}{5}         % trial = 5
\addtocounter{trial}{-2}      % trial = 3
\stepcounter{trial}           % trial = 4, and every child counter resets

\newcounter{item}[section]    % "item" restarts at 0 whenever section steps

% errors you will actually see
\setcounter{nosuch}{3}        % ! LaTeX Error: No counter 'nosuch' defined.
\newcounter{trial}            % ! LaTeX Error: Command \c@trial already defined.
\setcounter{trial}{}          % ! Missing number, treated as zero.

エラーの文面は覚えておく価値があります。存在しないカウンタを触ると ! LaTeX Error: No counter 'nosuch' defined.、同じ名前で二度 \newcounter すると ! LaTeX Error: Command \c@trial already defined. です。ここで名指しされる \c@trial こそ、trial の中身を保持している内部レジスタの名前——@ を含むので、プリアンブルから直接触るには \makeatletter が要ります。カウンタのリセットは \@stpelt という内部マクロが担当していて、その中身は「子を −1 にしてから \stepcounter する」というもの。0 を代入するのではなく もう一度 step させる ことで、孫カウンタまで芋づる式にリセットが伝わる仕掛けです。

\stepcounter\refstepcounter の違い — \label が別人の番号を指す

\refstepcounter{foo}\stepcounter と同じ増加とリセットを行い、そのうえで foo を「いま参照できる番号」に設定 します。この一手がないと \label は動きません。実際に測ってみると症状は思ったより悪質です。節を三つ書いた文書で \stepcounter{demo}\label{bad} としたとき、demo の値は 1 なのに、.aux に書き出されるのは \newlabel{bad}{{3}{1}{}{}{}}——\ref{bad} は 3、つまり直前の節番号を返します。「一つ前の値になる」のではなく、直近に \refstepcounter が呼ばれたまったく別のカウンタ を指してしまうのです。しかもエラーも警告もゼロ。文書の先頭で同じことをすると \newlabel{step}{{}{1}{}{}{}} となり、\ref は何も印字しません。

document.tex
\newcounter{demo}
\begin{document}
\section{First}\section{Second}\section{Third}

Here: \stepcounter{demo}\label{bad} (demo is now \thedemo)

Reference: \ref{bad}
\end{document}

% .aux    -> \newlabel{bad}{{3}{1}{}{}{}}
% output  -> "Here: (demo is now 1)"  /  "Reference: 3"
% log     -> 0 errors, 0 warnings
% the fix -> \refstepcounter{demo}\label{good}   gives \ref = 1

仕組みは latex.ltx を見れば一目です。\refstepcounter\stepcounter を呼んだあと \@currentlabel\thefoo の値を書き込み、\label はその \@currentlabel.aux に写しているだけ。つまり \label は「直前のカウンタ」ではなく「グローバル変数 \@currentlabel の現在値」を保存しているのです。LaTeX が sectionequationfigure を進めるとき内部で使うのも \refstepcounter で、だから見出しや式に付けた \label は正しい番号を返します。実務の判断は簡単で、番号を内部的に動かすだけなら \stepcounter\label で参照できる「番号付きの何か」を作るなら \refstepcounter。自作の定理環境や問題番号がうまく参照できないときは、まずここを疑ってください。

\value:カウンタを「計算できる数」として取り出す

\value{foo} は、そのカウンタを保持している \count レジスタそのものに展開されます——カーネルの定義は文字どおり \csname c@#1\endcsname の一行です。だから \setcounter\addtocounter の値の位置、\ifnum の比較、\numexpr の式のなか、つまり LaTeX が整数を期待するあらゆる場所 で使えます。逆に言えば、\value は表示のための命令ではありません。本文に番号を刷るのは次節の \arabic\thefoo の仕事で、\value は計算のための生の数値を返すだけ。この二役を分けて覚えておくと、「番号が出ない」「数字が二重に出る」といった混乱が起きなくなります。

latex
\setcounter{bar}{\value{foo}}            % copy foo into bar
\addtocounter{bar}{\value{foo}}          % bar = bar + foo

\ifnum\value{page}>10 \dots\fi           % compare
\setcounter{bar}{\numexpr\value{foo}*2+1\relax}   % arithmetic, no package

% \value is NOT for printing:
\value{foo}          % wrong -- prints nothing useful
\arabic{foo}         % right
\thefoo              % right, and honours any format you defined

番号の見た目を変える — \arabic から \thefoo まで

カウンタの値を文字列に変える命令は六つあり、どれも引数にカウンタ名をとります。ただし 実際の組版で番号を刷っているのは、各カウンタに自動で用意される \thefoo という専用マクロ です。section には \thesectionfigure には \thefigure があり、これを \renewcommand で書き換えれば番号の書式をまるごと変えられます。\alph\Alph はアルファベット 26 文字に対応します。境目は少し細かくて、0 を渡すと何も出力せずエラーも出ません が、負の値と 27 以上は ! LaTeX Error: Counter too large. になります。\fnsymbol の有効範囲も 1〜9 で、10 を渡せば同じ ! LaTeX Error: Counter too large. になります。ここで一つ、古い解説に残る誤りを正しておきます——TeX Live 2024 同梱の LaTeX2e では \@fnsymbol の定義が全ケースで \TextOrMath を使っているため、\fnsymbol は地の文でもそのまま使えます$\fnsymbol{footnote}$ のように囲む必要はありません(脚注では \thefootnote 経由が通常です)。

命令出力範囲・注意
\arabic1, 2, 3 …既定。負の値も出せます
\romani, ii, iii …1 未満は何も出力しません
\RomanI, II, III …1 未満は何も出力しません
\alpha, b, c …1〜26。0 は無出力、負の値と 27 以上は Counter too large
\AlphA, B, C …1〜26。0 は無出力、負の値と 27 以上は Counter too large
\fnsymbol∗ † ‡ § ¶ ‖ ∗∗ †† ‡‡1〜9。10 以上は Counter too large。地の文でもそのまま使えます
\thefooそのカウンタの「正式な」書式実際に刷られるのはこれ。\renewcommand で変更
latex
\renewcommand{\thesection}{\Roman{section}}            % 1, 2, 3 -> I, II, III
\renewcommand{\thefigure}{\thesection.\arabic{figure}}  % figure 2.3
\renewcommand{\thefootnote}{\fnsymbol{footnote}}        % footnotes as * dagger ...

% appendix figures as A.1, A.2, ...
\appendix
\renewcommand{\thefigure}{\Alph{section}.\arabic{figure}}

二つ目の例のように、\the… の定義に別のカウンタの表示を混ぜれば「2.3」のような複合番号が作れます。ここで見えてくるのは、番号の 見た目\thefigure の定義)と、いつ 0 に戻るかという 連動\newcounter の親指定)が、まったく別々に決められるという設計です。だから \renewcommand{\thefigure}{\thesection.\arabic{figure}} と書いただけでは、節が変わっても図番号は 1 に戻りません——リセットが欲しければ \counterwithin{figure}{section} のように親子関係のほうも設定する必要があります。

標準カウンタと、secnumdepthtocdepth という「設定値」

LaTeX はあらかじめ一群のカウンタを用意し、文書を読み進めながら自動で動かしています。これらも \setcounter で自由に触れるので、「章番号を途中から振り直す」「付録の図番号を A.1 にする」といった操作ができます。空の article クラスでカウンタを一つ作ると内部レジスタは \count196 あたりに割り当てられました——つまり数十個の標準カウンタがすでに動いているということです。ちなみに古典的な TeX ではレジスタは各種 256 個までしかありませんでしたが、e-TeX がこの上限を引き上げたため、いまは 400 個のカウンタを連続で確保しても文句を言われません(最後の一つは \count662 に着地しました)。

  • 見出し: partchaptersectionsubsectionsubsubsectionparagraphsubparagraph(使えるものはクラスによります)
  • フロートと式: figuretableequation
  • 脚注: footnotempfootnoteminipage 内の脚注)
  • 箇条書き: enumienumiienumiiienumivenumerate の入れ子 4 段ぶん)
  • ページ: page
  • 設定値: secnumdepth(どの深さの見出しまで番号を振るか)・tocdepth(どの深さまで目次に載せるか)

最後の二つは毛色が違います。番号や目次の「深さ」を支配する整数のつまみ で、数を数えるためのものではありません。見出し命令にはそれぞれ階層番号があり(section が 1、subsection が 2 …)、その番号が secnumdepth 以下 のときだけ番号が振られます。既定はおおむね 2 なので、\setcounter{secnumdepth}{1} と書けば小節以下の番号が消え、\setcounter{tocdepth}{1} と書けば目次が節までに絞られます。カウンタが単なる数取り器ではなく 組版を操縦する設定値 でもあることを、この二つがよく示しています。

\newlength が作るのは dimen ではなく skip レジスタ

\newlength{\mylen} の中身は \newskip です。カーネルの定義は \@ifdefinable#1{\newskip#1} の一行で、\meaning で確かめると \skip50 のような答えが返ってきます。ここが要点で、skip レジスタは伸縮成分(glue)を持てる ため、\setlength{\parskip}{1em plus 2pt minus 1pt} は完全に正当な代入です。実測すると \the\parskip10.00002pt plus 2.0pt minus 1.0pt を返し、三つの成分がすべて保存されていることが分かります。カウンタと違って引数は バックスラッシュ付きの命令名\mylen)で、初期値は 0pt。同じ名前で二度宣言すると ! LaTeX Error: Command \mylen already defined. になります。\a\b\c\d\t はアクセント命令として既に存在するので、\newlength{\a} が通らないのはこのためです。

document.tex
\newlength{\gap}
\setlength{\gap}{20pt plus 10pt minus 5pt}   % a rubber length in a register

% watch the stretch and shrink actually being used
\showboxbreadth=5 \showboxdepth=5
\setbox0=\hbox to 70pt{\rule{20pt}{2pt}\hspace{\gap}\rule{20pt}{2pt}}\showbox0
\setbox1=\hbox to 55pt{\rule{20pt}{2pt}\hspace{\gap}\rule{20pt}{2pt}}\showbox1

% log:  \hbox(2.0+0.0)x70.0, glue set 1.0      <- all 10pt of "plus" used
%       \hbox(2.0+0.0)x55.0, glue set - 1.0    <- all 5pt of "minus" used

\showbox を使うと、伸縮が実際に使われている様子をログで直接読めます。自然長 60pt の箱を 70pt に伸ばすと glue set 1.0——plus 10pt をちょうど使い切ったという意味です。55pt に縮めると glue set - 1.0 で、minus 5pt を使い切っています。この glue set の値はレイアウト調査の強い味方で、値が 2 や 3 になっていたら「その箱は無理に引き伸ばされている」と読めます。長さの plusminus が組版全体でどう働くか(\fill\flushbottom の話)は「長さと空白」のページが詳しく扱っています。

なぜ \setcounter は大域で \setlength は局所なのか

理由は latex.ltx の定義に書かれた一語です。\setcounter\addtocounter には \global が付いていて、\setlength\addtolength には付いていません。 それだけの差が、まったく違うふるまいを生みます。カウンタと長さを同じグループの中で書き換えると、グループを抜けたあと カウンタは新しい値のまま、長さは元の値に戻っています——実測では 1cm から 9cm に変えた長さが、グループを出た瞬間に 28.45274pt(=1cm)へ戻りました。この非対称は理にかなっています。章番号は環境を出たら忘れられては困る「文書の状態」ですが、余白の一時的な調整は「その範囲だけ」効いてほしいからです。

document.tex
\newcounter{cnt}\newlength{\len}
\setcounter{cnt}{1}\setlength{\len}{1cm}

{\setcounter{cnt}{99}\setlength{\len}{9cm}%
  inside the group: cnt=\thecnt\ len=\the\len}      % 99 / 256.0748pt

after the group:  cnt=\thecnt\ len=\the\len         % 99 / 28.45274pt

% latex.ltx:
%   \def\setcounter#1#2{... {\global\csname c@#1\endcsname#2\relax}}
%   \def\setlength#1#2{#1 #2\relax}          <- no \global
% to make a length assignment survive a group:
\global\setlength{\len}{9cm}

この違いは、\newcommand\newenvironment で自作の道具を書くときに効いてきます。環境の中で \setlength{\parindent}{0pt} と書けば \end で自動的に元に戻り、後片付けが不要です。逆に、環境の中で数えた回数を外へ持ち出したければ \setcounter(あるいは \global)が要ります。「環境を抜けたら設定が消えた」「環境の外まで余白が変わってしまった」という二種類のバグは、どちらもこの一語の有無で説明が付きます。なお、環境が自動的にグループを作るという性質そのものは「環境を作る」のページが扱っています。

長さの計算 — \dimexprcalc\fpeval と単位の換算

足し算・掛け算・割り算のうち、追加パッケージなしで書けるのは e-TeX のプリミティブ \numexpr\dimexpr です。\the\dimexpr\textwidth/3\relax は既定の article115.0pt を返しますし、\the\dimexpr 2cm+4pt\relax60.9055pt になります。注意点が一つ——\numexpr の割り算は切り捨てではなく 四捨五入 です。\the\numexpr 7/2\relax は 4、-7/2 は −4 を返します。LaTeX らしい書き味を求めるなら calc パッケージ で、\setlength{\x}{(\textwidth - 2em)/2} のような式が \setlength の中にそのまま書けるようになります(実測 162.49998pt)。小数倍だけは書き方が別で、\widthof{Total:} * \real{0.5} のように \real{} を挟みます。

書き方必要なものTeX Live 2024 での実測結果
\dimexprなし(e-TeX プリミティブ)\the\dimexpr\textwidth/3\relax115.0pt
\numexprなし(e-TeX プリミティブ)7/24-7/2-4(切り捨てではなく四捨五入)
calc\usepackage{calc}(\textwidth - 2em)/2162.49998pt
\real\usepackage{calc}\widthof{Total:} * \real{0.5}12.91669pt
\fpevalLaTeX カーネル(xfp は不要)\fpeval{345/7}pt49.28572pt\fpeval{sqrt(2)}1.414213562373095

計算するときは単位の実体を知っておくと事故が減ります。TeX の基準は pt(TeX ポイント)で、1in = 72.27pt\the\dimexpr 1in\relax72.26999pt を返します——ぴったり 72.27 でないのは、TeX が寸法を sp(スケールドポイント)という整数で持っているからで、1pt = 65536sp\the\dimexpr 65536sp\relax はきっちり 1.0pt です。PostScript 系の bp は 1/72 in なので 1bp1.00374pt72bp72.26999pt=1in にぴったり戻ります。もう一つ、em は代入した瞬間に評価されて凍結します。プリアンブルで \setlength{\x}{1em} と書くと 10.00002pt が入り、あとで \Large の中に入っても \x10.00002pt のまま——同じ場所で新たに 1em を測れば 14.09984pt です。単位の一覧そのものや \hspace\vspace は「長さと空白」のページが持ち主です。

組んだ結果を測る — \settowidth\settoheight\settodepth

もっとも強力な長さの操作は、実際に組んでみた結果を測って長さに入れる ことです。\settowidth{\mylen}{text} は与えたテキストを内部で組版し、その \mylen に代入します。同様に \settoheight はベースラインより の高さを、\settodepth の深さを測ります。実測してみると違いがはっきりします——Total: は幅 25.83339pt・高さ 6.94444pt・深さ 0.0ptxyz は高さ 4.30554pt(x ハイト)で深さ 1.94444ptgjpqy は高さ 6.67859pt・深さ 1.94444pt。ディセンダを持つ文字が入ると深さが生まれることが、数値でそのまま見えます。

document.tex
\usepackage{calc}
\newlength{\labelw}

\settowidth{\labelw}{Total:}      % 25.83339pt -- the real typeset width
\noindent\makebox[\labelw][l]{Total:}42\par
\rule{\labelw}{0.4pt}             % a rule exactly as wide as the label

% the same idea as a value inside an expression (needs calc):
\setlength{\labelw}{\widthof{Total:} + 1em}
\the\labelw                       % print the value to check it

calc を読み込むと同じ発想が式のなかで使えて、\widthof{text}\heightof\depthof を値としてそのまま書けます。違いは役割で、\settowidth は結果を長さに「代入する命令」、\widthof は式のなかに置ける「値」 です。そして calc を読み込み忘れたときの症状を覚えておいてください——\setlength{\mylen}{\textwidth/3}エラーを一つも出しません\mylen には \textwidth の値(既定の article345.0pt)がそのまま入り、余った /3本文に文字として組まれます。PDF に「/3」が出てきて初めて気づくたぐいの事故なので、割り算を書くなら calc を読むか、パッケージ不要の \dimexpr を使ってください。

通しで使う — 参照できる問題番号を自作する

最後に、ここまでの部品を一つにまとめます。カウンタ question を作り、表示形式を \thequestion で「Q1.」の形に定め、ラベル幅を \settowidth で測って番号をきれいに揃え、\refstepcounter で番号を進めることで \label\ref を効かせる——この四点が揃うと、番号付き環境として実用に耐えるものになります。書式が一箇所(\thequestion の定義)に集まっているので、後から「Q」を「問」に変えるのも一行です。

document.tex
\documentclass{article}
\newcounter{question}
\newlength{\qlabel}
\renewcommand{\thequestion}{Q\arabic{question}}

\newcommand{\question}{%
  \refstepcounter{question}%        <- \refstepcounter, so \label works
  \settowidth{\qlabel}{\textbf{\thequestion.}}%
  \par\noindent\makebox[\qlabel][l]{\textbf{\thequestion.}}\hspace{0.5em}}

\begin{document}
\question The first question.\label{q:first}
\question The second question.

See question~\ref{q:first}.        % -> "See question Q1."
\end{document}

ここに、カウンタと長さをプログラミングの道具として使うコツが凝縮されています。値を直接書かず、名前を付けて一箇所で管理する。番号の書式は \the… に、寸法は \newlength した名前に集めておけば、後から仕様が変わっても直すのは一箇所です。寸法は可能なかぎり既存の長さから組み立ててください——0.8\textwidth2\baselineskip のように書いておけば、用紙サイズや余白が変わっても比率が保たれます。逆に 8cm と直書きしたその瞬間から、その値は次のレイアウト変更で壊れる予約席になります。