7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C++用printfライク文字列フォーマッタを作ってみた

Last updated at Posted at 2019-03-19

はじめに

C++で書式付き文字列を出力する際、std::coutを使うと"<<が大量に出現して可読性が落ちるし、using namespace std;を書くのは嫌なので、printf/sprintfを使いたい派の人のために、型安全文字列フォーマットライブラリを作ってみました。

boost::formatの劣化再発明のような気もしますが、ヘッダオンリーのシングルファイルとなっており、依存ライブラリは無く、既存のプラグラムに簡単に組み込めますので許してください。

標準の方法

std::coutを使った一般的なプログラムは次のようになります。

.cpp
#include <iostream>

using namespace std;

int main()
{
	char const *name = "太郎";
	cout << "私は" << name << "です。" << endl;
	return 0;
}

一方、printfを使うと次のようになります。

.cpp
#include <stdio.h>

int main()
{
	char const *name = "太郎";
	printf("私は%sです。\n", name);
	return 0;
}

さて、どちらが読みやすいでしょうか?

私は後者の方が一目見て何が出力されるかわかりやすいです。"<<がたくさん出てくるのは思考を妨げます。そこでprintfを使いたいのですが、この関数には安全上の問題があります。

危険な罠

.cpp
#include <stdio.h>

int main()
{
	std::string name = "太郎";
	printf("私は%sです。\n", name);
	return 0;
}

文字列ポインタが必要なところに、std::stringオブジェクトを渡しています。何が起こるか予想できません。少なくとも期待通りに動作しないことは明らかです。

.cpp
#include <stdio.h>

int main()
{
	printf("値は%fです。\n", 123.456);
	return 0;
}

これは問題ないプログラムで、実行すると次のように出力されます。

値は123.456000です。

次に、間違えた引数を渡してみます。

.cpp
#include <stdio.h>

int main()
{
	printf("値は%fです。\n", 123);
	return 0;
}

これを実行すると、

値は0.000000です。

もしかすると、これとは異なる結果が表示されることもありえます。%fは浮動小数点数を出力するためのものですが、引数は123という整数です。実数と整数ではメモリ上の内部表現のサイズが異なるため、期待した結果が得られません。場合によってはプログラムがクラッシュするかもしれません。

次はもっと危険なやつです。

.cpp
#include <stdio.h>

int main()
{
	char buf[10];
	sprintf(buf, "Hello, world\n");
	return 0;
}

10文字分のメモリに14文字書き込もうとします。これはバッファーオーバーフローです。もしたまたま動いてしまったとしたら運が良かっただけです。良くて異常終了。場合によっては悪意のあるコードを実行されて、情報漏洩を引き起こすなど、セキュリティ上の欠陥になります。

strformatライブラリ

そこで、読みやすさと書きやすさ、安全性を両立した文字列フォーマッターが欲しいと考えました。

使い方は次のようになります。

.cpp
#include "strformat.h"

int main()
{
	char const *name = "太郎";
	strformat("私は%sです。\n").s(name).out();
	return 0;
}

char const *の代わりにstd::stringを渡しても問題なく動作します。そればかりか、

.cpp
	strformat("値は%sです。\n").d(123.456).out();

のような、一見間違った引数を与えても動作するようになっています。%記号に続く書式文字と、d()などの引数関数は、フォーマットする上でのただのヒントに過ぎません。

.cpp
	strformat("値は%fです。\n").s("123.456").out();

書式文字と引数関数は一致することが推奨ですが、異なっていてもそれなりに表示されます。上記の例では、文字列をパースして浮動小数点数化してから、フォーマットされます。

operator ()演算子を実装しているため、大抵の引数は簡略化して書くことができます。

.cpp
	strformat("値は%fです。\n")(123.456).out();

最後の.out()で、stdoutに出力されます。
代わりに.err()とすれば、stderrに出力されます。
あと、.str()というのもあって、これはstd::stringを返すので、sprintfの代わりに使えます。
その他、write_to()というのを使えば、ファイルポインタやファイルデスクリプタに直接出力することもできます。

7
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?