An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

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