はじめに
早いもので、今年ももう大晦日です。
大晦日といえば、やることは1つです。
そう、コンパイル時処理ですね!!
コンパイル時出力
C++ のコンパイル時処理は非常に強力で、様々なことがコンパイル時にできます。
入力に依存しない計算なら、大抵コンパイル時にしてしまうことができます。
しかし、その結果の出力については実行時に行う必要があり、本当にコンパイル時に処理できているのか分かりにくくなってしまうこともあります。
そこで、なんかこういろいろ頑張ってゴリ押すことで、制限はありますがコンパイル時に出力することができます。
先日公開した記事では、そんなコンパイル時出力について書いています。
コンパイル時出力の改良
この記事の目的は、コンパイル時出力の改良です。
現在のコンパイル時出力は次のような問題点があり、使いやすくはありません。
- 出力の前後に余計な出力(はみ出たエラーメッセージ)がある
- ターミナルのサイズや出力の長さによって、手動で調整しなければならないパラメータがある
- エラーメッセージを利用するためコンパイルエラーとなる
この記事では、これらの問題を解決することを目的とします。
アセンブル時出力
結果からいうと、コンパイル時に解決するのは無理です!
「え、じゃあどうするんですか?」
コンパイル時出力を諦めます!
「え?」
アセンブル時に出力します!
GNU アセンブラの .print 命令
GNU アセンブラには、アセンブル時に文字列を出力する擬似命令、 .print
があります1。
( .print
命令については前回の記事のコメントで @fujitanozomu さんに教えていただきました。)
インラインアセンブラでこれを使うと、アセンブル時に文字列を出力できます。
ついでに、 GCC なら \
によるエスケープも処理してくれます。
int main() {
asm(R"(
.print "Hello,\nWorld!"
)");
}
$ g++ asm_print.cpp
Hello,
World!
上のコードをコンパイルすると、コンパイル時に出力が行われているように見えます。
これは広義のコンパイル出力といってもいいのではないでしょうか!
この出力が行われるのはアセンブル時、つまりコンパイルの後です。
つまり、コンパイル時処理の結果を反映した出力ができるということです!
さらに、 static_assert
を利用する場合と違い、指定したメッセージ以外の出力が一切ありません!
とてもスッキリした出力となっています!
さらにさらに、 static_assert
とは違い、コンパイルが通ります!
実行ファイルを生成しつつ文字列を出力することができ、用途がかなり広がりそうです!
(え、都合よすぎないですか?)
インラインアセンブラの問題点
まあ、そんなすべてが都合いいなんてことはなく、問題点もあります。
それは、 C++20 ではインラインアセンブラが constexpr 関数内で評価できないことです。
これでは少し不便です。
constexpr void f() {
asm(".print \"Hey!\"");
}
int main() {
f();
}
$ g++ -std=c++20 asm_print2.cpp
asm_print2.cpp: In function 'constexpr void f()':
asm_print2.cpp:2:5: error: inline assembly is not a constant expression [-Winvalid-constexpr]
2 | asm(".print \"Hey!\"");
| ^~~
asm_print2.cpp:2:5: note: only unevaluated inline assembly is allowed in a 'constexpr' function in C++20
……おや?
未評価のインラインアセンブリはOK
only unevaluated inline assembly is allowed in a 'constexpr' function in C++20
という note が出ていますね。
ということは、 unevaluated ならいいのでしょうか?
そう、実は C++20 から constexpr 関数内でも未評価であれば asm
宣言を使うことが許可されるようになりました。
(え、本当に都合よすぎないですか?)
.print
は実行時ではなくアセンブル時に出力するためのもので、アセンブリコードが生成されさえすれば出力されるので、評価される必要はありません。
ならば、後はアセンブリコードを生成しつつ評価しないようなコードが書ければよいです。
さて、そんなコードどう書けばいいのでしょうか。
不思議な std::is_constant_evaluated
アセンブリコードを生成しつつ評価しないコードを書くため、いろいろ試してみましょう。
評価しないようにするなら if (false)
に入れればいいのでは?
constexpr void f() {
if (false) asm(".print \"Hey!\"");
}
int main() {
f();
}
$ g++ -std=c++20 asm_print3.cpp
消滅してしまいました……。
変数を挟めばどうでしょう?
constexpr void f() {
bool b = false;
if (b) asm(".print \"Hey!\"");
}
int main() {
f();
}
$ g++ -std=c++20 asm_print4.cpp
Hey!
お!
$ g++ -std=c++20 -O2 asm_print4.cpp
あぁ、最適化で消滅してしまいました……。
ならば volatile
を付ければ……!
constexpr void f() {
volatile bool b = false;
if (b) asm(".print \"Hey!\"");
}
int main() {
f();
}
$ g++ -std=c++20 -O2 asm_print5.cpp
asm_print5.cpp: In function 'constexpr void f()':
asm_print5.cpp:3:9: error: lvalue-to-rvalue conversion of a volatile lvalue 'b' with type 'volatile bool' [-Winvalid-constexpr]
3 | if (b) asm(".print \"Hey!\"");
| ^
なんか怒られてしまいました……。
うーん、なにかこう、常に false に評価されるけど if 文の中身を消さないような式はないのでしょうか。
ありました。 not std::is_constant_evaluated()
です2。
これはコンパイル時は常に false に評価されますが、実行時は true になるため if 文の中身が消されることはありません。
#include <type_traits>
constexpr void f() {
if (not std::is_constant_evaluated()) asm(".print \"Hey!\"");
}
int main() {
f();
}
$ g++ -std=c++20 -O2 asm_print6.cpp
Hey!
やったね!
コンパイル時出力ライブラリ constexpr_print
これで準備は整いました!
前回作ったコンパイル時出力のプログラムを書き換えて、使いやすくします。
ざっくりいうと、 static_assert(condition, message);
としていたところを if constexpr (not condition) { if (not std::is_constant_evaluated()) asm(".print \"" escaped-message "\""); }
に置換します。
エスケープシーケンスによるカーソル操作は、エラーメッセージが出力された行を遡る分が不要になり、上下方向は1行上に移動するだけでよくなります。
そしてできたものがこちらです!
#include "constexpr_print.hpp"
#include <iostream>
#include <array>
#include <string_view>
#include <algorithm>
#include <concepts>
template <std::unsigned_integral T>
constexpr std::size_t digits(T x) {
std::size_t d = 1;
while (x /= 10) ++d;
return d;
}
template <std::size_t N>
struct fizzbuzz {
static constexpr std::size_t size =
fizzbuzz<N - 1>::size + (
N % 15 == 0 ? 9 :
N % 3 == 0 || N % 5 == 0 ? 5 :
digits(N) + 1
);
static constexpr std::array<char, size> data = [] {
using namespace std::literals::string_view_literals;
std::array<char, size> data;
auto it = std::ranges::copy(fizzbuzz<N - 1>::data, data.begin()).out;
if (N % 15 == 0) std::ranges::copy("FizzBuzz\n"sv, it);
else if (N % 3 == 0) std::ranges::copy("Fizz\n"sv, it);
else if (N % 5 == 0) std::ranges::copy("Buzz\n"sv, it);
else {
auto it = data.rbegin();
*it++ = '\n';
auto n = N;
do *it++ = '0' + n % 10;
while (n /= 10);
}
return data;
}();
static constexpr std::string_view value{ data.data(), size };
};
template <>
struct fizzbuzz<0> {
static constexpr std::size_t size = 0;
static constexpr std::array<char, size> data {};
static constexpr std::string_view value{ data.data(), size };
};
int main() {
constexpr_print("It's compile time!");
constexpr_print(fizzbuzz<100>::value);
std::cout << "It's runtime!\n";
std::cout << fizzbuzz<100>::value;
}
$ g++ -std=c++20 sample.cpp -o sample
It's compile time!
1
2
Fizz
4
Buzz
︙
$ ./sample
It's runtime!
1
2
Fizz
4
Buzz
︙
$
始めに目的とした問題が解決され、余計な出力がなくなり、手動でのパラメータ調整が不要になり、実行時処理との共存もできています。
見やすく、実用的なコンパイル時出力ができました!
おわりに
前回かなりのゴリ押しと妥協でコンパイル時出力を実現しましたが、今回の改良で実用的なものとなり、普段使いしやすくなったと思います。
なので、皆さん、普段使いしましょう。
皆でコンパイル時処理をしましょう。
謝辞
アセンブリ言語の .print
についてコメントして頂いた @fujitanozomu 氏に感謝します。