C++で文字列フォーマッティング(書式付き文字列生成)
まえがき
C++でprintf
ライクな文字列フォーマッティングの実現方法です。
C++で文字列フォーマットをする方法はいくつかあります。
標準では、std::cout
などのiostream
を使った方法ですが、個人的にはちょっと使いづらいです。
C++20では、format
ライブラリが標準化されますが、まだ実装されているコンパイラはありません。
他にも、boostライブラリにフォーマットライブラリがありますが、インストールなどの手間があります。
色々と探した結果、std::snprintf
と可変引数テンプレートを使った方法が、分かりやすく簡単だったので紹介します。
前提条件
今回実装したソースは以下の環境でビルド、動作確認しています。
OS | Ver | Compiler | Remarks |
---|---|---|---|
Windows | 10 | Visual Studio 2019 | |
macOS | 10.15.6 | Clang 11.0 | |
Ubuntu | 18.04 | GCC 7.5.0 |
実装
まずは、メインの文字列フォーマットを行う関数です。
/* 文字列のフォーマッティング(内部処理) */ template<typename ... Args> std::string StringFormatInternal(const std::string& format, Args&& ... args) { /* フォーマット後の文字数を算出 */ int str_len = std::snprintf(nullptr, 0, format.c_str(), std::forward<Args>(args) ...); /* フォーマット失敗 */ if (str_len < 0) { throw std::runtime_error("String Formatting Error"); } else { /* Nothing to do */ } /* バッファサイズを算出(文字列長 + null文字サイズ) */ size_t buffer_size = str_len + sizeof(char); /* バッファサイズ分メモリ確保 */ std::unique_ptr<char[]> buffer(new char[buffer_size]); /* 文字列のフォーマット */ std::snprintf(buffer.get(), buffer_size, format.c_str(), args ...); /* 文字列をstd::string型に変換して出力 */ return std::string(buffer.get(), buffer.get() + str_len); }
最初に、std::snprintf
に空のバッファを渡すことで、フォーマット後の文字列サイズを算出しています。
文字列フォーマットの引数は、可変引数テンプレートを使って入力しています。
その後、取得した文字サイズから、std::unique_ptr
でバッファを確保し、そのバッファを指定してstd::snprintf
を呼び出すことで、フォーマットされた文字列(const char*
)を取得できます。
後は、C++の文字列型(std::string)に変換することで、C++で使いやすい文字列フォーマットができます。
しかし、このままだと少し問題があります。
std::snprintf
は、C言語の関数snprintf
のラッパーなので、C++の文字列型(std::string
)はそのままでは入れることが出来ません。
そのため、以下のように、C言語の文字列(const char*
)に変換して入力する必要があります。
/* C++の文字列 */ std::string text = "Test Message"; /* C++の文字列をCの文字列に変換して入力 */ std::string format_text = StringFormat("%s %d", text.c_str(), 100);
そこで、std::string
を直接引数として入力できるように、可変引数テンプレートArgs&& ... args
の各要素に変換関数(Convert
関数)をかまして、std::string
型の場合は、const char*
に変換して入力するようにします。
Convert
関数は以下のように実装します。
/* C++ 11/14版 */ /* std::string型をconst char*に変換 */ template<typename T, typename std::enable_if<std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, std::string>::value>::type* = nullptr> auto Convert(T&& value) { return std::forward<T>(value).c_str(); } /* std::string型以外は、そのまま出力 */ template<typename T, typename std::enable_if<!std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, std::string>::value>::type* = nullptr> auto Convert(T&& value) { return std::forward<T>(value); }
上記Convert
関数では、引数の型(T
)がstd::string
型だったら、c_str()
関数を呼び出し、const char*
に変換して返します。
そして、std::string
型以外の場合には、そのままの型で値を返します。
なお、Convert
関数は、C++17に対応しているコンパイラであれば、以下のようにif constexpr
式を使うことで、1つの関数で実現することも出来ます。
/* C++ 17版 */ /* std::string型をconst char*に変換し、それ以外はそのまま出力 */ template<typename T> auto Convert(T&& value) { /* std::string型をconst char*に変換 */ if constexpr (std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, std::string>::value) { return std::forward<T>(value).c_str(); } /* std::string型以外は、そのまま出力 */ else { return std::forward<T>(value); } }
後は、Args&& ... args
の各要素にConvert
関数をかまして、StringFormatInternal
関数を呼び出すことで、std::string
型もそのまま引数として入力できるようになります。
/* 文字列のフォーマッティング */ template<typename ... Args> std::string StringFormat(const std::string& format, Args&& ... args) { /* 各パラメータの型を変換して、文字列のフォーマッティング */ return detail::StringFormatInternal(format, detail::Convert(std::forward<Args>(args)) ...); }
以下に、コード全文を示します。
【StringFormat.h】
#pragma once #include <string> #include <cstdio> #include <stdexcept> #include <vector> #include <iostream> #include <memory> namespace detail { #if 0 /* C++ 17版 */ /* std::string型をconst char*に変換し、それ以外はそのまま出力 */ template<typename T> auto Convert(T&& value) { /* std::string型をconst char*に変換 */ if constexpr (std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, std::string>::value) { return std::forward<T>(value).c_str(); } /* std::string型以外は、そのまま出力 */ else { return std::forward<T>(value); } } #else /* C++ 11/14版 */ /* std::string型をconst char*に変換 */ template<typename T, typename std::enable_if<std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, std::string>::value>::type* = nullptr> auto Convert(T&& value) { return std::forward<T>(value).c_str(); } /* std::string型以外は、そのまま出力 */ template<typename T, typename std::enable_if<!std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, std::string>::value>::type* = nullptr> auto Convert(T&& value) { return std::forward<T>(value); } #endif /* 文字列のフォーマッティング(内部処理) */ template<typename ... Args> std::string StringFormatInternal(const std::string& format, Args&& ... args) { /* フォーマット後の文字数を算出 */ int str_len = std::snprintf(nullptr, 0, format.c_str(), std::forward<Args>(args) ...); /* フォーマット失敗 */ if (str_len < 0) { std::runtime_error("String Formatting Error"); } else { /* Nothing to do */ } /* バッファサイズを算出(文字列長 + null文字サイズ) */ size_t buffer_size = str_len + sizeof(char); /* バッファサイズ分メモリ確保 */ std::unique_ptr<char[]> buffer(new char[buffer_size]); /* 文字列のフォーマット */ std::snprintf(buffer.get(), buffer_size, format.c_str(), args ...); /* 文字列をstd::string型に変換して出力 */ return std::string(buffer.get(), buffer.get() + str_len); } } /* 文字列のフォーマッティング */ template<typename ... Args> std::string StringFormat(const std::string& format, Args&& ... args) { /* 各パラメータの型を変換して、文字列のフォーマッティング */ return detail::StringFormatInternal(format, detail::Convert(std::forward<Args>(args)) ...); }
また、StringFormat
関数を使ったサンプルコードは以下のようになります。
static void StringFormatTest() { /* C++形式文字列 */ std::string text1 = "Test Message1"; /* C形式文字列 */ const char* text2 = "Test Message 2"; /* 数値 */ int value = 256; /* 文字列をフォーマットしてコンソールに出力 */ std::cout << StringFormat("%s | %s | 0x%x = %d", text1, text2, value, value) << std::endl; }
実行結果は以下のようになります。
Test Message1 | Test Message2 | 0x100 = 256