C++ だと文字列をフォーマットするコードって結構大変です。
C++11(14) になって色々な選択肢は増えたんですが、いまいち決定打に欠けると言いますか・・。
というわけで boost です。boost::format を使った文字列フォーマットです。
% で繋げることなく、可変引数テンプレートを使って printf 風に使えるようにしてみました。
boost::format の機能のおかげで、型安全で例外が発生することもないようにしています。
例えばアプリで処理失敗が発生したときに、アプリログを残す際に sprintf などを使ってフォーマットしてるような場合、その書式をミスっていたりすると、アプリが落ちたりしちゃいます。
レアケース(処理失敗)を考慮したコードを書いたつもりが、再現率が低く修正しにくい致命的なバグを生んでしまったりするので、できれば、こういう箇所は特に安全に書いておきたいなと思う次第。
というわけで、
printf 系の関数は(windows の CString::Format とかも)使わないほうが安全です。
がしかし printf 系の使いやすさは捨て難いです。慣れちゃってもいるし。
そんなわけで printf 風の書き方も出来てしまう boost::format はすごく便利です。
開発者の方に感謝しつつ使わせていただこうと思います。
2021/3 追記
C++17 を前提にコードを刷新いたしました。
と同時に、色々な型を展開して出力する機能を追加してみました。
基本的な使い方は変わってませんが、引数に渡せる型を景気よく増やしてみました。
std::pair std::tuple
std::unique_ptr std::shared_ptr std::optional
std::array std::vector std::list std::deque std::initializer_list
std::set std::multiset std::map std::multimap
これらが渡された場合は、その中身を出力します。
std::tuple や std::unique_ptr などは、まぁ使い勝手が良くなったかなと感じますが、std::vector や std::map などのコンテナまで対応してしまったのはやりすぎかもしれません。
このあたりは実際に使う場合には要調整かなと思います。
よもやま
C++17 の if constexpr はとても便利だなと感じます。
その分、昨今のC++の発展っぷりに付いていくことは、自分程度のスペックだととても大変です。C++20 や 23 の話題もあるし・・。
そのため、わからないことはすぐにネットで答えを探しています。色々な情報を提供してくれている方々には感謝しかありません。
そしてここまで複雑な(と自分は思う) C++言語の、仕様決めをしている方々ももちろんですが、コンパイラを作っていらっしゃる方々には、畏敬の念を通り越して畏怖の念すら感じざるを得ません!
ありがとうございます。
コード
- StringFormat.h
#include <boost/format.hpp> namespace rlib { namespace string { namespace inner { template<class> struct IsArray : std::false_type {}; template<class T, std::size_t N> struct IsArray<std::array<T, N>> :std::true_type {}; template<class> struct IsVector : std::false_type {}; template<class T1, class T2> struct IsVector<std::vector<T1, T2>> :std::true_type {}; template<class> struct IsList : std::false_type {}; template<class T1, class T2> struct IsList<std::list<T1, T2>> :std::true_type {}; template<class> struct IsDeque : std::false_type {}; template<class T1, class T2> struct IsDeque<std::deque<T1, T2>> :std::true_type {}; template<class> struct IsInitializerList : std::false_type {}; template<class T> struct IsList<std::initializer_list<T>> :std::true_type {}; template<class> struct IsSet : std::false_type {}; template<class T1, class T2, class T3> struct IsSet<std::set<T1, T2, T3>> :std::true_type {}; template<class> struct IsMultiSet : std::false_type {}; template<class T1, class T2, class T3> struct IsMultiSet<std::multiset<T1, T2, T3>> :std::true_type {}; template<class> struct IsMap : std::false_type {}; template<class T1, class T2, class T3, class T4> struct IsMap<std::map<T1, T2, T3, T4>> :std::true_type {}; template<class> struct IsMultiMap : std::false_type {}; template<class T1, class T2, class T3, class T4> struct IsMultiMap<std::multimap<T1, T2, T3, T4>> :std::true_type {}; template<class> struct IsPair : std::false_type { }; template<class T1, class T2> struct IsPair<std::pair<T1,T2>> :std::true_type {}; template<class> struct IsUniquePtr : std::false_type {}; template<class T1, class T2> struct IsUniquePtr<std::unique_ptr<T1,T2>> :std::true_type {}; template<class> struct IsSharedPtr : std::false_type {}; template<class T> struct IsSharedPtr<std::shared_ptr<T>> :std::true_type {}; template<class> struct IsOptional : std::false_type {}; template<class T> struct IsOptional<std::optional<T>> :std::true_type {}; template<class> struct IsTuple : std::false_type {}; template<class... T> struct IsTuple<std::tuple<T...>> :std::true_type {}; template <class CharT, class Head, class... Tail> void f(boost::basic_format<CharT>&, Head&, Tail&&...); template <class CharT> void f(const boost::basic_format<CharT>&) {} template <std::size_t I, class CharT, class... T, class... Tail> void f(boost::basic_format<CharT>& format, const std::tuple<T...>& head, Tail&&... tail) { if constexpr (I < sizeof...(T)) { f<I + 1, CharT>(format % std::get<I>(head), head, tail...); } else { f<CharT>(format, tail...); } } template <class CharT, class Head, class... Tail> void f(boost::basic_format<CharT>& format, Head& head, Tail&&... tail) { using HeadT = std::remove_const_t<Head>; if constexpr (IsUniquePtr<HeadT>::value|| IsSharedPtr<HeadT>::value|| IsOptional<HeadT>::value) { if (head) { f<CharT>(format, *head, tail...); } else { f<CharT>(format % "(null)", tail...); } } else if constexpr (IsPair<HeadT>::value) { f<CharT>(format, head.first, head.second, tail...); } else if constexpr (IsTuple<HeadT>::value) { f<0, CharT>(format, head, tail...); } else if constexpr (IsArray<HeadT>::value || IsVector<HeadT>::value || IsList<HeadT>::value || IsDeque<HeadT>::value || IsInitializerList<HeadT>::value || IsSet<HeadT>::value || IsMultiSet<HeadT>::value || IsMap<HeadT>::value || IsMultiMap<HeadT>::value) { // 範囲ループ可の型か?っていう便利記述がもしあるなら知りたい for (const auto& i : head) { f<CharT>(format, i); } f<CharT>(format, tail...); #ifdef _MSC_VER } else if constexpr (std::is_same_v<HeadT, CStringA> || std::is_same_v<HeadT, CStringW>) { f<CharT>(format % std::basic_string<CharT>(head.GetString()), tail...); #endif } else { f<CharT>(format % head, tail...); } } } template <class CharT, class... Args> std::basic_string<CharT> format(const CharT* lpszFormat, Args&&... args) { boost::basic_format<CharT> format; format.exceptions(boost::io::no_error_bits); // 例外を発生させない format.parse(lpszFormat); try { inner::f<CharT>(format, args...); } catch (...) { } return format.str(); } template <class CharT, class... Args> std::basic_string<CharT> format(const std::basic_string<CharT>& s, Args&&... args) { return format<CharT>(s.c_str(), args...); } } }
使い方の例
- こちらのコードでは printf っぽい書式しか使っておりませんが、中身は boost::format です。書式の詳細はそちらのリファレンスもご参照下さい。
#include "StringFormat.h" // ヘッダをインクルードするのみで使えます void main() { using namespace rlib::string; {// 普通の使い方 std::string r = format(u8"%s %dSX SR%dDE%s", u8"日産", 180, 20, "T"); // "日産 180SX SR20DET" } {// std::wstring も可です std::wstring r = format(L"BNR%d RB%dDE%s", 32, 26, L"TT"); // L"BNR32 RB26DETT" } {// 適してない型でも例外発生しません std::string r = format("%dSX %s", 240, L"日産"); // "240SX 00007FF742CC123C" } {// std::string をそのまま書けます。c_str() が不要なので便利。 const std::string a = u8"トヨタ"; std::string b = "1JZ-GTE"; std::string r = format("%s jzx90 %s", a, b); // "トヨタ jzx90 1JZ-GTE" } #ifdef _MSC_VER {// VisualStudio の CString CString t0(_T("マツダ")); const CString t1(_T("13B-T")); auto r = format(_T("%s FC3S %s"), t0, t1); // L"マツダ FC3S 13B-T" } #endif {// std::tuple std::pair は展開します。 std::tuple<std::string, int, double> a{ "RPS13",1998,11.8 }; const std::pair<std::string, int> b{ "FR", 1848000 }; auto r = format( // "形式:RPS13 排気量:1998cc 燃費:11.8km/l 駆動方式:FR 価格:1848000円" u8"形式:%s 排気量:%dcc 燃費:%.1fkm/l 駆動方式:%s 価格:%d円", a, b); } {// std::unique_ptr std::shared_ptr std::optional は中身を出力にします。空(無効)の場合は"(null)"を出力します std::unique_ptr<int> u = std::make_unique<int>(1); std::shared_ptr<int> s = std::make_shared<int>(2); std::optional<int> o(3); std::optional<int> e; auto r = format("%d,%d,%d,%d", u, s, o, e); // "1,2,3,(null)" } {// その他コンテナも展開します。(やりすぎかもしれません。素直にコンパイルエラーに振ってもよいかも。) auto r0 = format("%d,%d,%d", std::array<int, 3>{ 1, 2, 3 }); // "1,2,3" auto r1 = format("%d,%d,%d", std::vector<int>{4, 5, 6}); // "4,5,6" auto r2 = format("%d,%d,%d", std::list<int>{7, 8, 9}); // "7,8,9" auto r3 = format("%d,%d,%d", std::deque<int>{10, 11, 12}); // "10,11,12" auto r4 = format("%d,%d,%d", std::set<int>{3, 2, 1}); // "1,2,3" 取れる順で出力します(以下同様) auto r5 = format("%d,%d,%d", std::map<int, int>{ {1, 2}, { 3,4 }}); // "1,2,3" auto r6 = format("%d,%d,%d", std::multiset<int>{1, 2, 3}); // "1,2,3" auto r7 = format("%d,%d,%d", std::multimap<int, int>{ {1, 2}, { 3,4 }});// "1,2,3" } {// コンテナの中も再帰で展開するので以下みたいな型も可です std::map<int, std::pair<std::string, std::vector<int>>> m{ {1, {"a", {5,6,7}}}, {8, {"b", {9,10}}} }; auto r = format("%s,%s,%s,%s,%s,%s", m); // "1,a,5,6,7,8" } {// boost::format なので未対応の型はコンパイルエラーになってくれます std::function<void()> f; auto r = format("%s", f); // コンパイルエラー }
Page Info | |
---|---|
Page Name : | 技術系備忘録/C++/Boost/boost.formatを使った文字列フォーマット。printf系関数を置き換え |
Page aliases : | None |
Page owner : | takatsuka |
Can Read | |
Groups : | All visitors |
Users : | All visitors |
Can Edit | |
Groups : | No one |
Users : | No one |