最新のC ++での文字列のフォーマットについて

良い一日! この記事では、現代のC ++での文字列フォーマットの既存の機能について話し、実際のプロジェクトで数年間使用してきた成果を示し、文字列フォーマットに対するさまざまなアプローチのパフォーマンスを比較します。







文字列フォーマットは、テンプレート文字列と一連の引数から結果の文字列を取得できる操作です。 テンプレート文字列には、引数が置換される代わりに、プレースホルダーを含むテキストが含まれます。







わかりやすくするために、小さな例を示します。







int apples = 5; int oranges = 7; std::string str = format("I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges); std::cout << str << std::endl;
      
      





ここに:

テンプレート文字列:%d個のリンゴと%d個のオレンジがあるので、%d個の果物があります

プレースホルダー:%d、%d、%d

引数:リンゴ、オレンジ、リンゴ+オレンジ







例を実行すると、結果の文字列を取得します







 I have 5 apples and 7 oranges, so I have 12 fruits
      
      





次に、C ++が文字列の書式設定に提供するものを見てみましょう。







レガシーC



Cでの文字列の書式設定は、Xprintfファミリの関数を使用して行われます。 同じ成功を収めて、C ++で次の関数を使用できます。







 char buf[100]; int res = snprintf(buf, sizeof(buf), "I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges); std::string str = "error!"; if (res >= 0 && res < sizeof(buf)) str = buf; std::cout << str << std::endl;
      
      





明らかな不器用さにもかかわらず、これは非常に優れたフォーマット方法です。









しかし、もちろん、いくつかの欠点がありました。









関数std :: to_string()



C ++ 11以降、std :: to_string()関数が標準ライブラリに登場しました。これにより、送信された値を文字列に変換できます。 この関数は、すべての種類の引数では機能しませんが、次の場合にのみ機能します。









使用例:







 std::string str = "I have " + std::to_string(apples) + " apples and " + std::to_string(oranges) + " oranges, so I have " + std::to_string(apples + oranges) + " fruits"; std::cout << str << std::endl;
      
      





クラスstd :: stringstream



std :: stringstreamクラスは、C ++が提供する文字列フォーマットの主な方法です。







 std::stringstream ss; ss << "I have " << apples << " apples and " << oranges << " oranges, so I have " << apples + oranges << " fruits"; std::string str = ss.str(); std::cout << str << std::endl;
      
      





厳密に言えば、std :: stringstreamの使用は、プレースホルダーの代わりにテンプレート文字列に引数を挿入するため、完全な文字列フォーマットではありません。 これは最も単純な場合には受け入れられますが、より複雑な場合には、コードの可読性が著しく低下します。







 ss << "A[" << i1 << ", " << j1 << "] + A[" << i2 << ", " << j2 << "] = " << A[i1][j1] + A[i2][j2];
      
      





と比較してください:







 std::string str = format("A[%d, %d] + A[%d, %d] = %d", i1, j1, i2, j2, A[i1][j1] + A[i2][j2]);
      
      





std :: sringstreamオブジェクトを使用すると、将来必要になる可能性のあるいくつかの興味深いラッパーを実装できます。







「何でも」を文字列に変換する:







 template<typename T> std::string to_string(const T &t) { std::stringstream ss; ss << t; return ss.str(); }
      
      





 std::string str = to_string("5");
      
      





文字列を何かに変換する:







 template<typename T> T from_string(const std::string &str) { std::stringstream ss(str); T t; ss >> t; return t; } template<> std::string from_string(const std::string &str) { return str; }
      
      





 int x = from_string<int>("5");
      
      





検証を使用して文字列を任意のものに変換します。







 template<typename T> T from_string(const std::string &str, bool &ok) { std::stringstream ss(str); T t; ss >> t; ok = !ss.fail(); return t; } template<> std::string from_string(const std::string &str, bool &ok) { ok = true; return str; }
      
      





 bool ok = false; int x = from_string<int>("x5", ok); if (!ok) ...
      
      





また、1行でstd :: stringstreamを便利に使用するために、いくつかのラッパーを作成できます。







各引数にstd :: stringstreamオブジェクトを使用する:







 class fstr final : public std::string { public: fstr(const std::string &str = "") { *this += str; } template<typename T> fstr &operator<<(const T &t) { *this += to_string(t); return *this; } };
      
      





 std::string str = fstr() << "I have " << apples << " apples and " << oranges << " oranges, so I have " << apples + oranges << " fruits";
      
      





単一のstd :: stringstreamオブジェクトを文字列全体に使用する:







 class sstr final { public: sstr(const std::string &str = "") : ss_(str) { } template<typename T> sstr &operator<<(const T &t) { ss_ << t; return *this; } operator std::string() const { return ss_.str(); } private: std::stringstream ss_; };
      
      





 std::string str = sstr() << "I have " << apples << " apples and " << oranges << " oranges, so I have " << apples + oranges << " fruits";
      
      





先を見ると、std :: to_stringのパフォーマンスは、std :: stringstreamを使用して実装されたto_stringのパフォーマンスよりも3〜4倍高いことがわかります。 したがって、適切な型にはstd :: to_stringを使用し、他のすべての型にはテンプレートto_stringを使用するのが論理的です。







 std::string to_string(int x) { return std::to_string(x); } std::string to_string(unsigned int x) { return std::to_string(x); } std::string to_string(long x) { return std::to_string(x); } std::string to_string(unsigned long x) { return std::to_string(x); } std::string to_string(long long x) { return std::to_string(x); } std::string to_string(unsigned long long x) { return std::to_string(x); } std::string to_string(float x) { return std::to_string(x); } std::string to_string(double x) { return std::to_string(x); } std::string to_string(long double x) { return std::to_string(x); } std::string to_string(const char *x) { return std::string(x); } std::string to_string(const std::string &x) { return x; } template<typename T> std::string to_string(const T &t) { std::stringstream ss; ss << t; return ss.str(); }
      
      





Boost ::フォーマットライブラリ



boostライブラリセットは、C ++言語と標準ライブラリを完全に補完する強力なツールです。 文字列の書式設定は、boost :: formatライブラリで表されます。







両方の典型的なプレースホルダーの表示がサポートされています:







 std::string str = (boost::format("I have %d apples and %d oranges, so I have %d fruits") % apples % oranges % (apples + oranges)).str();
      
      





および順序:







 std::string str = (boost::format("I have %1% apples and %2% oranges, so I have %3% fruits") % apples % oranges % (apples + oranges)).str();
      
      





boost ::形式の唯一の欠点は、パフォーマンスが低いことです。これは、文字列形式への最も遅い方法です。 また、プロジェクトでサードパーティのライブラリを使用できない場合、この方法は適用できません。







そのため、C ++と標準ライブラリでは便利な文字列フォーマットツールが提供されないため、独自のコードを作成します。







vsnprintfのラップ



Xprintf関数のラッパーを作成して、十分なメモリを割り当て、任意の数のパラメーターを渡してみましょう。







メモリを割り当てるには、次の戦略を使用します。







  1. まず、ほとんどの場合に十分な量のメモリを割り当てます
  2. フォーマット関数を呼び出そう
  3. 呼び出しが失敗した場合、より多くのメモリを割り当て、前の手順を繰り返します


パラメーターを渡すには、stdargメカニズムとvsnprintf関数を使用します。







 std::string format(const char *fmt, ...) { va_list args; va_start(args, fmt); std::vector<char> v(1024); while (true) { va_list args2; va_copy(args2, args); int res = vsnprintf(v.data(), v.size(), fmt, args2); if ((res >= 0) && (res < static_cast<int>(v.size()))) { va_end(args); va_end(args2); return std::string(v.data()); } size_t size; if (res < 0) size = v.size() * 2; else size = static_cast<size_t>(res) + 1; v.clear(); v.resize(size); va_end(args2); } }
      
      





 std::string str = format("I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges);
      
      





ここでは、いくつかのニュアンスを明確にする価値があります。 Xprintf関数の戻り値は、プラットフォームによって異なります。一部のプラットフォームでは、障害が発生した場合は-1が返され、この場合はバッファーが2倍になります。 他のプラットフォームでは、結果の文字列の長さが返されます(null文字を除く)。この場合、必要なだけのメモリをすぐに割り当てることができます。 さまざまなプラットフォームでのXprintf関数の動作の詳細については、 こちらをご覧ください 。 また、一部のプラットフォームでは、vsnprintf()は引数リストを「台無しにします」ので、呼び出しの前にコピーします。







C ++ 11が登場する前にこの関数の使用を開始し、今日まで少し変更を加えて使用し続けています。 これを使用する場合の主な欠点は、引数としてstd :: stringがサポートされていないことです。したがって、すべての文字列引数に.c_str()を追加することを忘れないでください。







 std::string country = "Great Britain"; std::string capital = "London"; std::cout << format("%s is a capital of %s", capital.c_str(), country.c_str()) << std::endl;
      
      





可変引数テンプレート(可変長テンプレート)



C ++ 11以降、C ++では、可変数の引数を持つテンプレート(可変長テンプレート)を使用できるようになりました。







このようなパターンは、引数をフォーマット関数に渡すときに使用できます。 また、以前に実装されたテンプレートto_stringを使用できるため、引数の型について心配する必要がなくなりました。 したがって、通常のプレースホルダーを使用します。







すべての引数を取得するには、最初の引数を分離して文字列に変換し、この操作を覚えて再帰的に繰り返します。 引数がない場合、または引数の最後(再帰エンドポイント)で、テンプレート文字列を解析し、引数を置き換えて、結果の文字列を取得します。







したがって、テンプレート文字列の解析、すべてのパラメータの収集と文字列への変換、パラメータのテンプレート文字列への置換、結果の文字列の受信など、書式設定機能を完全に実装するすべてのものがあります。







 std::string vtformat_impl(const std::string &fmt, const std::vector<std::string> &strs) { static const char FORMAT_SYMBOL = '%'; std::string res; std::string buf; bool arg = false; for (int i = 0; i <= static_cast<int>(fmt.size()); ++i) { bool last = i == static_cast<int>(fmt.size()); char ch = fmt[i]; if (arg) { if (ch >= '0' && ch <= '9') { buf += ch; } else { int num = 0; if (!buf.empty() && buf.length() < 10) num = atoi(buf.c_str()); if (num >= 1 && num <= static_cast<int>(strs.size())) res += strs[num - 1]; else res += FORMAT_SYMBOL + buf; buf.clear(); if (ch != FORMAT_SYMBOL) { if (!last) res += ch; arg = false; } } } else { if (ch == FORMAT_SYMBOL) { arg = true; } else { if (!last) res += ch; } } } return res; } template<typename Arg, typename ... Args> inline std::string vtformat_impl(const std::string& fmt, std::vector<std::string>& strs, Arg&& arg, Args&& ... args) { strs.push_back(to_string(std::forward<Arg>(arg))); return vtformat_impl(fmt, strs, std::forward<Args>(args) ...); } inline std::string vtformat(const std::string& fmt) { return fmt; } template<typename Arg, typename ... Args> inline std::string vtformat(const std::string& fmt, Arg&& arg, Args&& ... args) { std::vector<std::string> strs; return vtformat_impl(fmt, strs, std::forward<Arg>(arg), std::forward<Args>(args) ...); }
      
      





アルゴリズムは非常に効果的であることが判明し、フォーマット文字列に沿って1つのパスで機​​能します。 プレースホルダーの代わりに引数を挿入できない場合、引数は変更されずに残り、例外はスローされません。







使用例:







 std::cout << vtformat("I have %1 apples and %2 oranges, so I have %3 fruits", apples, oranges, apples + oranges) << std::endl; I have 5 apples and 7 oranges, so I have 12 fruits std::cout << vtformat("%1 + %2 = %3", 2, 3, 2 + 3) << std::endl; 2 + 3 = 5 std::cout << vtformat("%3 = %2 + %1", 2, 3, 2 + 3) << std::endl; 5 = 3 + 2 std::cout << vtformat("%2 = %1 + %1 + %1", 2, 2 + 2 + 2) << std::endl; 6 = 2 + 2 + 2 std::cout << vtformat("%0 %1 %2 %3 %4 %5", 1, 2, 3, 4) << std::endl; %0 1 2 3 4 %5 std::cout << vtformat("%1 + 1% = %2", 54, 54 * 1.01) << std::endl; 54 + 1% = 54.540000 std::string country = "Russia"; const char *capital = "Moscow"; std::cout << vtformat("%1 is a capital of %2", capital, country) << std::endl; Moscow is a capital of Russia template<typename T> std::ostream &operator<<(std::ostream &os, const std::vector<T> &v) { os << "["; bool first = true; for (const auto &x : v) { if (first) first = false; else os << ", "; os << x; } os << "]"; return os; } std::vector<int> v = {1, 4, 5, 2, 7, 9}; std::cout << vtformat("v = %1", v) << std::endl; v = [1, 4, 5, 2, 7, 9]
      
      





性能比較



to_stringとstd :: to_stringのパフォーマンス比較、ミリ秒呼び出しあたりのミリ秒







int、ms ロングロング、ミリ秒 ダブル、ミリ秒
to_string 681 704 1109
std :: to_string 130 201 291


画像







フォーマット関数のパフォーマンス比較、100万回の呼び出しあたりのミリ秒







ミリ秒
fstr 1308
sstr 1243
書式 788
ブースト::形式 2554
vtformat 2022


画像







ご清聴ありがとうございました。 コメントや追加は大歓迎です。








All Articles