みんな代替トークン使とる。使てへんのお前だけ。
この記事は 2020/1/9(水) の C++ MIX #7 で発表した初心者向けのネタを C++ アドベントカレンダー 2021 用に使い回しただけのモノである。
何の新規性も無い記事だが、代替トークンの地位が不当に低い(と思っている)ので、普及促進に貢献しようと思い改めて記事起こしした次第である。
「そんなんみんな知っとるが」など、その筋の方からのツッコミは無しの方向で何卒…
代替トークンとは?
「代替」の「トークン」(まんま)。
要は、本来のトークンの替わりに使用可能な代替のトークン表現である。
一覧は以下の通り。
代替 | 本来 | 代替 | 本来 | 代替 | 本来 |
---|---|---|---|---|---|
<% |
{ |
and |
&& |
xor |
^ |
%> |
} |
bitand |
& |
xor_eq |
^= |
<: |
[ |
and_eq |
&= |
not |
! |
:> |
] |
or |
|| |
not_eq |
!= |
%: |
# |
bitor |
| |
compl |
~ |
%:%: |
## |
or_eq |
|= |
なお、よく「ダイグラフ」と呼ばれたりもするが、見ての通りキーワード型の物も結構あるし、キーワード型を除いたとしても必ずしも「ダイ」(2文字)ではない(%:%:
)。
ちゃんと「代替トークン」(あるいは「オルタナティブトークン」)と呼んであげよう。
ちなみにこれらの名前も予約語なので、例えば and
や or
と言った関数名や変数名は使えない。
and
って言う関数書こうとしたら何故かエラーが出た的な話を稀に聞くが、まさにコイツらの仕業である。
なお、「トークン」と名前がついている通り、コンパイラがソースをトークン化する際に処理されるので、文字列中や他の識別子中では代替トークンとはみなされない。
まぁ当たり前っちゃあ当たり前なんだが…
使用例(その1)
さっそく使用例を見ていこうと思う。
使用前
#include <iostream>
int main()
{
int a[] = { 114, 514, };
for (int i = 0; i < 2; ++i) {
std::cout << a[i] << ", ";
}
std::cout << '\n';
}
使用後
%:include <iostream>
int main()
<%
int a<::> = <% 114, 514, %>;
for (int i = 0; i < 2; ++i) <%
std::cout << a<:i:> << ", ";
%>
std::cout << '\n';
%>
ちょっと分かりにくいが、include
の頭の部分の %:
、main
関数本体と for
文のブロック、および、配列 a
の初期化子の部分の <%
と %>
、配列 a
の宣言と添え字アクセスの部分の <:
と :>
で使用している。
どうだろうか、とても読み易くなったのではないだろうか?(異論は認める)
使用例(その2)
…これ、使う気がしない…
と思っただろうか?
まぁそう言わずに、次の例を見て欲しい。
使用前
template <class InputIterator, class Predicate>
bool
all_of(InputIterator first, InputIterator last, Predicate pred)
{
for (; first != last; ++first)
if (!pred(*first))
return false;
return true;
}
これは C++ 標準ライブラリの all_of
の一般的な実装である。
そんなに難しいコードではないが、このコードには 1 点だけ致命的な問題がある。
そう、否定演算子が全く目立たないのだ。
おそらく、言われるまで否定演算子がある事に気付かなかったハズだ。
代替トークンを使えばこれを完璧な形で解決できる。
使用後
template <class InputIterator, class Predicate>
bool
all_of(InputIterator first, InputIterator last, Predicate pred)
{
for (; first != last; ++first)
if (not pred(*first))
return false;
return true;
}
どうだろうか、今度こそとても読み易くなったのではないだろうか?(異論は認めない)
ほら、あなたはだんだん代替トークンを使いたくなってくる…
使用例(その3)
もう一つ使用例を出そう。
使用前
const size_t j = (i + 1) % n;
const result_type mask = r == Dt ? result_type(~0) :
(result_type(1) << r) - result_type(1);
const result_type Yp = (x[i] & ~mask) | (x[j] & mask);
const size_t k = (i + m) % n;
x[i] = x[k] ^ rshift<1>(Yp) ^ (a * (Yp & 1));
result_type z = x[i] ^ (rshift<u>(x[i]) & d);
i = j;
z ^= lshift<s>(z) & b;
z ^= lshift<t>(z) & c;
return z ^ rshift<l>(z);
これは C++ 標準ライブラリの mersenne_twister_engine::operator()
を某 libc++ 実装から抜き出してちょいと読み易いように手直ししたものである。(オリジナルはアンダースコアだらけで目に優しくない)
まぁそこまで悪くはないが(上から目線)、ちょっとビット反転演算子が目立たな過ぎな気がする。
これも、代替トークンを使えばこうなる。
使用後
const size_t j = (i + 1) % n;
const result_type mask = r == Dt ? result_type(compl 0) :
(result_type(1) << r) - result_type(1);
const result_type Yp = (x[i] & compl mask) | (x[j] & mask);
const size_t k = (i + m) % n;
x[i] = x[k] ^ rshift<1>(Yp) ^ (a * (Yp & 1));
result_type z = x[i] ^ (rshift<u>(x[i]) & d);
i = j;
z ^= lshift<s>(z) & b;
z ^= lshift<t>(z) & c;
return z ^ rshift<l>(z);
ほら、ビット反転演算子が見やすい!(異論は認める)
だが、まだ改善(?)の余地はある。
使用後(過激派)
const size_t j = (i + 1) % n;
const result_type mask = r == Dt ? result_type(compl 0) :
(result_type(1) << r) - result_type(1);
const result_type Yp = (x<:i:> bitand compl mask) bitor (x<:j:> bitand mask);
const size_t k = (i + m) % n;
X<:i:> = x<:k:> xor rshift<1>(Yp) xor (a * (Yp bitand 1));
result_type z = x<:i:> xor (rshift<u>(x<:i:>) bitand d);
i = j;
z xor_eq lshift<s>(z) bitand b;
z xor_eq lshift<t>(z) bitand c;
return z xor rshift<l>(z);
ほら、何もかもが見やすい!(異論は認める)
使用例(その4)
もうあなたも代替トークンを使いたくなってきている頃だとは思うが、ダメ押しでもう一つ実用例を挙げておこう。
使用前
template <class InputIterator1, class InputIterator2,
class BinaryPredicate>
pair<InputIterator1, InputIterator2>
mismatch(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2,
BinaryPredicate pred)
{
for (; first1 != last1 && first2 != last2; ++first1, ++first2)
if (!pred(*first1, *first2))
break;
return pair<InputIterator1, InputIterator2>(first1, first2);
}
これは C++ 標準ライブラリの mismatch
の一般的な実装である。
使用後
template <class InputIterator1, class InputIterator2,
class BinaryPredicate>
pair<InputIterator1, InputIterator2>
mismatch(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2,
BinaryPredicate pred)
{
for (; first1 != last1 and first2 != last2; ++first1, ++first2)
if (not pred(*first1, *first2))
break;
return pair<InputIterator1, InputIterator2>(first1, first2);
}
ここまで読んだあなたは、最早代替トークン無しでは生きていけない体になっているに違いない。
使用例(究極)
ところで皆さん、代替トークンの字面に惑わされてはいないだろうか?
使用前
struct S {
S() = default;
S(const S&) = default;
S(S&&) = default;
~S() = default;
};
見ての通り、何の変哲もない空のクラス定義だ。
だが、このクラス定義には代替トークン使用の余地が沢山ある。
使用後
struct S <%
S() = default;
S(const S bitand) = default;
S(S and) = default;
compl S() = default;
%>;
参照もデストラクタも見やすい!
結論
みんな積極的に代替トークンを使おう!
蛇足(トライグラフ)
C++17 で廃止されてしまったトライグラフ(こちらは正真正銘 3 文字)は、代替トークンとは異なりトークン化どころかプリプロセス前に処理されていたので、文字列内とかでも問答無用で変換対象になっていた。
代替 | 本来 | 代替 | 本来 | 代替 | 本来 |
---|---|---|---|---|---|
??= |
# |
??( |
[ |
??< |
{ |
??/ |
\ |
??) |
] |
??> |
} |
??' |
^ |
??! |
` | ` | ??- |
使用前(再掲)
#include <iostream>
int main()
{
int a[] = { 114, 514, };
for (int i = 0; i < 2; ++i) {
std::cout << a[i] << ", ";
}
std::cout << '\n';
}
使用後
??=include <iostream>
int main()
??<
int a??(??) = ??< 114, 514, ??>;
for (int i = 0; i < 2; ++i) ??<
std::cout << a??(i??) << ", ";
??>
std::cout << '??/n';
??>
読みづらい(異論は認める)のはいいとして(良くはないが)、文字定数中のバックスラッシュにもトライグラフが使われているのがお分かり頂けるだろう。
蛇足(トークン最長切り出しの例外)
C++ のトークン化は、通常最長のトークンを切り出すことになっている。
(C++ に限らず、大抵のプログラミング言語はそうなっている)
例
-
external
⇒extern
とal
と言う2つのトークンではなく、external
と言う単一のトークン -
a+++++b
⇒a
++
+
++
b
ではなく、a
++
++
+
b
と言うトークン列
しかし、C++ には代替トークンに関連した以下のような例外がある。
例
-
<:
⇒<:
(つまり{
) -
<::
⇒<
と::
-
<::>
⇒<:
と:>
(つまり{
と}
) -
<:::
⇒<:
と::
(つまり{
と::
)
まぁ通常この規則に引っかかることはおそらく一生のうち1度も無いとは思うが…
蛇足(C 言語)
ココで説明した代替トークンは C++ の仕様である。
では C 言語はどうなっているのかと言うと、残念ながらキーワード型の代替トークンは言語仕様では提供されていない。(記号類は提供されている)
だか、やむを得ず C 言語を使用しなければならないという憂き目にあっている(?)あなたに朗報がある。
C 言語では、キーワード型の代替トークンは言語仕様ではなく、ヘッダ <iso646.h>
で提供されている。
C 言語でキーワード型の代替トークンを使用する際には忘れずに <iso646.h>
をインクルードしよう。
蛇足(通常のトークンとの違い)
本来のトークンの替わりに使用可能な代替のトークン表現と言ったな。あれはウソだ。
代替トークンは通常本来のトークンと完全に互換であるが、実は 1 つだけ例外がある。
それは、プリプロセッサマクロで文字列化する時だ。
例
#include <iostream>
#define STR(a) #a
int main()
{
std::cout << STR(#) << '\n';
std::cout << STR(%:) << '\n';
}
出力
#
%:
プリプロセッサマクロに渡す場合には気を付けよう。
まぁ通常この差に引っかかることはおそらく一生のうち1度も無いとは思うが…
Discussion