こんにちは。田原です。

無茶苦茶暑い日が続いています。更に逆走台風まで来ています。私も夏バテでついダラダラしてしまいます。
さて、C++を使っていてC言語を懐かしむことってありませんか? 私は書式付き出力したい時にprintfのお手軽さが懐かしいです。ただ、書式指定を間違っても取り敢えず出力してくれるstd::coutの魅力も大きいです。そこで今回は、boost::formatを使ってこの両者を満たすお手軽printf関数を作ってみましたのでご紹介します。

1.何故にC++版printf関数をわざわざ作る?

std::cout(std::ostreamすべて同じです)も、実は書式付き出力をサポートしています。<ios>で導入される hexboolalpha<iomanip>で導入される setwsetfillsetprecisionなどが良く使われると思います。
しかし、あまり頻繁に使う機能ではないので、いつもマニピュレータ名を忘れてしまって面倒な思いをします。その点、C言語のprintfはキーワードではなく記述場所で指定するので一度理解したら後はそうそう忘れることはありません。

上記の点でprintfは優れているのですが、残念なことに型チェックが凄く甘いです。
最近のコンパイラはprintf等の標準ライブラリについては書式と引数の型が一致していない時、警告してくれるため困ることはかなり減っているのですが、自作関数の書式指定の不一致警告はサポートされていないため、デバッグ・ログ出力で書式指定する独自関数を作った時に辛い思いをします。(やっと不具合を再現できてログをみていると、書式指定ミスでちゃんと読めないとか、お馬鹿なことを結構やりました。)

こんな時はboost::formatの出番です。これを使うと、printf()ライクな書式指定で変数の値をよしなに出力してくれます。更に書式指定を間違ってもそれなりに出力されるのでデバッグ・ログのような用途では本当に有り難いです。ただ、問題が1点ありboost:formatが開発された時はまだ、可変長引数テンプレートがありませんでした。そのため、複数の変数を出力する時は%演算子で区切ります。(これにどうしても慣れません。せめて << にしていればよいのにとか思ってしまいます。)

01
std::cout << boost::format("%10s:%5.2f\n") % "dummy" % 123.456;

更に、デバッグ・ログ用にマクロでラップしたくても容易にはできません。printf()なら簡単なのですが。

01
02
03
04
05
#ifdef DEBUG
    #define DEBUG_LOG(...)
#else
    #define DEBUG_LOG(...)  do { printf("%s(%d) : ", __FILE__, __LINE__); printf(__VA_ARGS__); } while(0)
#endif

Wandboxでみてみる。

この2つのジレンマを解決するため、(時々アナウンスしていましたが)既にTheolizer®では対応しています。しかし、もっとお手軽に導入できればと思うことが幾度かありましたので作ってみました。

2.まずはboostの準備

2-1.Windowsの場合

特に面倒なことはないです。boost::formatはヘッダ・オンリ(boostをビルドしなくても使える)ですので、ダウンロードして解凍すればOKです。
このページから、お使いのOS用の最新版をダウンロードし、適切なところへ解凍して下さい。

2-2.ubuntuの場合

もっと簡単です。以下のコマンドでインストールされます。

01
sudo apt-get install libboost-all-dev

3.そして可変長引数テンプレートでC++版printf関数を実装

意外に簡単ですので、いきなりソースを示します。C++版printfの関数名は xprintf としてみました。
展開に使ったテクニックは、第14回 結構便利な可変長引数テンプレート(Variadic templates)の「2-2.よくある使い方」にて解説したものそのままですので見比べて見て下さい。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#include <boost/format.hpp>
inline std::string xprintf_impl(boost::format& iFormat)
{
    return iFormat.str();
}
template<typename tFirst, typename... tParams>
inline std::string xprintf_impl(boost::format& iFormat, tFirst iFirst, tParams... iParams)
{
    return xprintf_impl(iFormat % iFirst, iParams...);
}
template<typename... tParams>
inline std::string xprintf(std::string const& iFormat, tParams... iParams)
{
    boost::format aFormat(iFormat);
    return xprintf_impl(aFormat, iParams...);
}

Wandboxで確認する。

ローカル環境でビルドする時は、インクルード・パスとして、boostを解凍したフォルダを指定して下さい。
WindowsでC:\boost_1_67_0以下へ解凍された場合は、cl.exe /IC:\boost_1_67_0 main.cppです。
なお、ubuntuでapt-getでインストールした場合は/usr/include/boost以下へインストールされますので、デフォルトで/usr/includeが指定されるため、別途インクルード・パスを指定する必要はないようです。ubuntuでは-std=c++11オプションをお忘れなく。

4.boostはコンパイルが遅いので、明示的実体化で対処

boostのヘッダ・オンリなライブラリは、#includeするだけで使えるのでお手軽なのですが、1つ欠点があります。
コンパイル時間が booooooooost!! することです。いやマジで結構伸びます。小さなプロジェクト(コンパイル単位が少ない)で使う場合にはそれほど気になりませんが、大きなプロジェクトで使う時は結構困ります。

そこで、boost::formatをインクルードするコンパイル単位を1つにしてしまいたいです。
そのためには、以下の3つの機能を実装側(cpp)で記述すればよいです。

No. 呼び出し側 boost::format機能
1. xprintf() boost::formatのコンストラクタ
2. xprintf_impl(iParams有り) iFormat % iFirst
3. xprintf_impl(iParams無し) iFormat.str()

boost:formatは実体化されたクラス・テンプレートですので1.と3.は 実装側だけでマシン語へ落とせますので問題ありません。
問題は 2. です。iFirstは呼び出し側で指定しますので、その型を実装側は知らないため、マシン語へ落とせません。
このような使い方の場合、出力したい変数の多くは数値型(arithmetic)ですので、基本型すべてを明示的実体化することが可能です。

さて、ユーザ定義側はどうしましょう? 実は、boost::formatはユーザ定義型にも対応しています。
そのユーザ定義型を出力できるoperator<<があれば使えます
Wandboxで確認する。

そこで、ユーザ定義型をクラス・テンプレートで受けて、プライマリー・テンプレートでユーザ定義型、数値型について部分特殊化して数値型のboost::format呼び出しすればboost::formatと同程度の機能に対応できます。

更に、デバッグ用として考えると Scoped Enum型もお手軽に出力したいものです。これはenum型について部分特殊化し、それをstatic_castしてboost::formatを呼び出せば良いです。

以上を纏めると次のようになります。

Wandboxで確認する。

xprintf.h

#include <type_traits>
#include <sstream>
namespace boost
{
template<class charT, class Traits, class Alloc> class basic_format;
typedef basic_format<char, std::char_traits<char>, std::allocator<char>> format;
}
namespace internal
{
std::string getString(boost::format& iFormat);
template<typename tType>
boost::format& output(boost::format& iFormat, tType iParam);
boost::format& makeFormat(std::string const& iFormat);
// ユーザ定義型対応
template<typename tType, typename tEnable=void>
struct Out
{
static boost::format& put(boost::format& iFormat, tType& iParam)
{
std::stringstream ss;
ss << iParam;
return output(iFormat, ss.str().c_str());
}
};
template<typename tType>
struct Out<tType, typename std::enable_if<std::is_arithmetic<tType>::value>::type>
{
static boost::format& put(boost::format& iFormat, tType& iParam)
{
return output(iFormat, iParam);
}
};
// scoped enum型にも対応
template<typename tType>
struct Out<tType, typename std::enable_if<std::is_enum<tType>::value>::type>
{
static boost::format& put(boost::format& iFormat, tType& iParam)
{
return output(iFormat, static_cast<typename std::underlying_type<tType>::type>(iParam));
}
};
inline std::string xprintf_impl(boost::format& iFormat)
{
return getString(iFormat);
}
template<typename tFirst, typename... tParams>
inline std::string xprintf_impl(boost::format& iFormat, tFirst iFirst, tParams... iParams)
{
return xprintf_impl(Out<tFirst>::put(iFormat, iFirst), iParams...);
}
}
template<typename... tParams>
inline std::string xprintf(std::string const& iFormat, tParams... iParams)
{
return internal::xprintf_impl(internal::makeFormat(iFormat), iParams...);
}
view raw xprintf.h hosted with ❤ by GitHub

xprintf.cpp

#include <boost/format.hpp>
#include "xprintf.h"
namespace internal
{
std::string getString(boost::format& iFormat)
{
return iFormat.str();
}
template<typename tType>
boost::format& output(boost::format& iFormat, tType iParam)
{
return iFormat % iParam;
}
boost::format& makeFormat(std::string const& iFormat)
{
static boost::format aFormat("");
aFormat.parse(iFormat);
return aFormat;
}
// 明示的実体化
template boost::format& output(boost::format& iFormat, bool iParam);
template boost::format& output(boost::format& iFormat, char iParam);
template boost::format& output(boost::format& iFormat, signed char iParam);
template boost::format& output(boost::format& iFormat, short iParam);
template boost::format& output(boost::format& iFormat, int iParam);
template boost::format& output(boost::format& iFormat, long iParam);
template boost::format& output(boost::format& iFormat, long long iParam);
template boost::format& output(boost::format& iFormat, unsigned char iParam);
template boost::format& output(boost::format& iFormat, unsigned short iParam);
template boost::format& output(boost::format& iFormat, unsigned int iParam);
template boost::format& output(boost::format& iFormat, unsigned long long iParam);
template boost::format& output(boost::format& iFormat, float iParam);
template boost::format& output(boost::format& iFormat, double iParam);
template boost::format& output(boost::format& iFormat, long double iParam);
template boost::format& output(boost::format& iFormat, char const* iParam);
}

main.cpp

#include <iostream>
#include "xprintf.h"
#ifdef DEBUG
#define DEBUG_LOG(...)
#else
#define DEBUG_LOG(...) std::cout << __FILE__ << "(" << __LINE__ << ") : " << xprintf(__VA_ARGS__)
#endif
enum class Enum { Idle, Test };
class Ratio
{
int mFraction;
int mDenominator;
public:
Ratio(int iFraction, int iDenominator) :
mFraction(iFraction), mDenominator(iDenominator)
{ }
friend std::ostream& operator<<(std::ostream& iOStream, Ratio const& iRatio)
{
iOStream << iRatio.mFraction << "/" << iRatio.mDenominator;
return iOStream;
}
};
int main()
{
std::cout << xprintf("%10s = %6.2f\n", "result", 2345.678);
DEBUG_LOG("%10s = %10.2f\n", "result", 3456.789);
Enum aEnum = Enum::Test;
DEBUG_LOG("%10s = %10d\n", "aEnum", aEnum);
Ratio aRatio(16, 9);
DEBUG_LOG("%10s = %10s\n", "aRatio", aRatio);
std::string aString="test";
DEBUG_LOG("%10s = %10s\n", "aString", aString);
}
view raw main.cpp hosted with ❤ by GitHub

CMakeLists.txt

cmake_minimum_required(VERSION 3.5.1)
project(sample)
find_package(Boost)
include_directories("${Boost_INCLUDE_DIR}")
if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()
add_executable(sample main.cpp xprintf.cpp xprintf.h)

CMakeList.txtを使ってプロジェクトを生成する時は、-DBOOST_ROOT=<boostを解凍したフォルダのパス>を指定して下さい。
例えば、WindowsでC:\boost_1_67_0以下へ解凍された場合は、-DBOOST_ROOT=C:\boost_1_67_0です。
ubuntuでapt-getを使ってインストールした場合は、これを指定しなくてもfind_packageがうまいこと見つけてくれるようです。

Visual C++ 2017(Windows)
01
02
03
04
> mkdir msvc
> cd msvc
> cmake -G "Visual Studio 15 2017" .. "-DBOOST_ROOT=C:\boost_1_67_0"
> cmake --build .
gcc 5.4.0(ubuntu 16.04 LTS)
01
02
03
04
> mkdir gcc
> cd gcc
> cmake ..
> make

当ソフトウェアはご自由に使って下さい。(ただし、 私からの保証はありません ので、ご自身の責任でお願いします。)

5.最後に速度について

残念ながら、boost::formatは速度がちょっと遅いです。書式指定の順序と出力する値の順序を一致させないでも良いという国際化対応時に非常に有り難い機能をサポートしているからです。(日本語と英語では単語の並び順が異なりますが、例えば、書式文字列を日本語→英語に変えた時、出力する値の順序を変更しないで済めば文字列だけ変えれば国際化できるのです。gettextという非常にメジャーな国際化対応方式で積極的に使われている仕組みです。)

速度については、A Note about performance に記載があるので、速度が気になる場合には確認されて下さい。

6.まとめ

夏バテでなかなか集中できず、今回はちょっと駆け足になってしまいました。内容的には既に解説したことの比較的簡単な応用ですので、ご容赦下さい。
それでは、台風の被害が大きくならないことを祈りつつ、今日はこのへんで終わりにいたします。お疲れ様でした。