std::printとは
本記事はAdvent Calendar 2024の16日目である。
前記事: TauriにC++を組み込んでGUIアプリケーションを作ろう
std::print
とは、C++23で新たに追加された標準ライブラリの関数である。
今までのC++といえば、以下のようにostream
のエイリアスであるcout
にシフト演算子で文字列を流し込むものであった。
#include <iostream>
int main() {
std::cout << "Hello world!" << std::endl;
}
親の顔より良く見るC++のはろわであるが、C++のはろわがどういった機能を使用して記述されているのかを説明するだけでC++の初学者が去るのに十分すぎる。
誰が学び始めに、「cout
がostream
クラスのエイリアスとか、ostream
はストリームバッファの出力ストリームを制御するとか、シフト演算子をオーバーライドしてcout
は投げられた値を処理する」という難解さに触れようとするだろうか。まだ変数や演算子の理解もない状態からその応用をこれでもかと押し付けてくるのは不親切でしかない。
実際、多くのメジャーなプログラミング言語たちがそれほど苦しまずに言語機能を知る最序盤で触れる関数によってはろわを実現していることを鑑みても、この記述は明らかに異質なのである。
もっとも、C++に慣れるということはこの関門を突破できるか否かにあるため、むしろこれはふるいにかけている、という要素もあるのだろうか。
そんな中にあって、C++の出力には明確なフォーマッティング処理がなかったため、C++20でstd::format
が実装された。それに伴い、より標準I/Oに寄り添った入出力方法が模索されるようになる。その中で生まれたのがstd::print
である。
std::print
は他のメジャーなプログラミング言語群と比較して名付けられたAPI名であり、また本実装がC++に標準化されるまでの間format機能を拡張してきたfmtライブラリにて6年使用されてきたものである。
std::print
で記述するもっとも簡単なはろわは以下の通り。
#include <print>
int main() {
std::print("Hello world!");
}
std::printの機能
std::print
はprintf
ライクなフォーマッティングを行って出力することができる。
#include <print>
int main() {
std::string str = "World!";
std::print("Hello {}", str); // "Hello World!"
}
また、CライクなFILEポインタに対しても、ストリームバッファに対してもprintすることができる。
なおストリームバッファに対して操作する場合はヘッダを<print>
ではなく<ostream>
にする必要がある。
#include <print>
#include <cstdio>
int main(){
FILE* file = std::fopen("./hello.txt", "r+");
std::print(file,"{}", "Hello World");
fclose(file);
}
#include <ostream>
#include <fstream>
int main(){
std::fstream fs;
fs.open("./hello.txt");
std::print(fs,"{}", "Hello World");
fs.close();
}
std::print
は投げ込まれる文字列がunicodeの場合に自動的にunicodeとして出力してくれるが、C++20で実装されたu8string型の文字列を出力する場合はイテレータを利用してstring型に直す必要がある。
#include <print>
#include <string>
int main(){
std::u8string u8str = u8"はろーわーるど!";
std::print("{}", std::string(u8str.begin(), u8str.end()));
}
ぱふぉーまんすてすと
google-benchmarkを使用し、以下の環境でテストした。
CPU: ryzen 3900X
OS: Windows on WSL in opensuse-tumbleweed
Run on (24 X 3800.02 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x12)
L1 Instruction 32 KiB (x12)
L2 Unified 512 KiB (x12)
L3 Unified 16384 KiB (x1)
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
printf 5073 ns 5073 ns 113006
ostream 5256 ns 1173 ns 616476
print 5633 ns 5633 ns 125564
以下がテストしたサンプルコードである。
#include <cstdio>
#include <iostream>
#include <print>
#include <benchmark/benchmark.h>
void printf(benchmark::State& s) {
while (s.KeepRunning())
std::printf("The answer is %d.\n", 42);
}
BENCHMARK(printf);
void ostream(benchmark::State& s) {
while (s.KeepRunning())
std::ios::sync_with_stdio(false);
std::cout << "The answer is " << 42 << ".\n";
}
BENCHMARK(ostream);
void print(benchmark::State& s) {
while(s.KeepRunning())
std::print("The answer is {}.\n", 42);
}
BENCHMARK(print);
BENCHMARK_MAIN();
単純なパフォーマンス比較で考えるとどうしてもストリームバッファのオプションを使用できるcoutに軍配が上がってしまうが、printはまだ効率的な実装が行われている環境がないのでやむを得ないのかもしれない。
テストについてはオレオレ環境であったり、標準出力の/dev/null廃棄がうまく機能しないので--benchmark-filterオプションでごまかしたりなどしている部分があるためなんとも言えないのだが、より効果的なテストができたら更新・追記などしたい。
しめ
std::printはまだ実装予定の機能がC++の国際標準化委員会の中で議論されている発展途上の機能である。
実行時のフォーマッティングはC++26であるし、今後決定されればより効率的な実装も望めるものである。
また、std::printという表記方法やフォーマッティングの方法が他言語との兼ね合いで実装内容を調整しているところを見ても、C++は古くからある言語ながらもいまだに発展していることを如実に実感できる。おそらく一生付き合っていっても枯れず廃れず電子世界の中で生き生きと成長しつづけるのではないだろうか。
参考
P2093R14 Formatted output
cpprefjp std::print
おまけの追記
この記事を作るのにCopilotさんやGrokさんにはお世話になった。
AIに助けられるというなかなか良い時代になった。