C ++中的“字符串插值”:构造一个带有嵌入值的std :: string(例如,用于错误消息)?

时间:2016-06-21 23:16:57

标签: c++ c++11

我想创建一个包含嵌入信息的字符串。实现我想要的一种方式(不是唯一的方法)称为string interpolation或变量替换,其中字符串中的占位符被替换为实际值。

在C中,我会做这样的事情:

printf("error! value was %d but I expected %d",actualValue,expectedValue)

如果我在python中编程,我会做这样的事情:

"error! value was {0} but I expected {1}".format(actualValue,expectedValue)

这两个都是字符串插值的例子。

如何在C ++中执行此操作?

重要提示

  1. 我知道如果我想将这样的消息打印到标准输出(不是字符串插值,但打印出我想要的字符串类型),我可以使用std::cout
  2. cout << "error! value was " << actualValue << " but I expected "
    << expectedValue;
    

    我不想打印字符串到stdout。我想将std::string作为参数传递给函数(例如异常对象的构造函数)。

    1. 我正在使用C ++ 11,但是可移植性可能是一个问题,所以知道哪些方法有效,哪些方法不适用于哪个版本的C ++。
    2. 修改

      1. 对于我的直接使用,我并不关心性能(我正在大声提出异常!)。 然而,了解各种方法的相对表现一般非常有用。

      2. 为什么不直接使用printf(毕竟C ++是C的超集......)? This answer讨论了为什么不这样做的一些原因。根据我的理解,类型安全是一个重要原因:如果你把%d放在那里,你放在那里的变量最好真的可以转换为整数,因为这就是函数如何计算它是什么类型。使用一种方法可以更安全地使用编译时知道要插入的变量的实际类型。

5 个答案:

答案 0 :(得分:16)

方法1:使用字符串流

看起来std::stringstream提供了一个快速解决方案:

std::stringstream ss;
ss << "error! value was " << actualValue << " but I expected " <<  expectedValue << endl;

//example usage
throw MyException(ss.str())

  • 没有外部依赖
  • 我相信这适用于C ++ 03以及c ++ 11.

否定

  • 据说很慢
  • 有点麻烦:你必须创建一个流,写入它,然后从中获取字符串。

方法2:提升格式

Boost Format库也是可能的。使用它,你会这样做:

throw MyException(boost::format("error! value was %1% but I expected %2%") % actualValue % expectedValue);

  • 与stringstream方法相比非常干净:一个紧凑的构造

否定

    据报道,
  • 速度很慢:内部使用流方法
  • 它是一种外部依赖

修改

方法3:可变参数模板参数

似乎可以通过使用可变参数模板参数(对于采用无限数量模板参数的模板的技术术语)来创建类型安全的printf版本。我已经看到了这方面的一些可能性:

  • This question给出了一个简洁的示例,并讨论了该示例的性能问题。
  • This answer该问题,其实施也非常紧凑,但据报道仍然存在性能问题。
  • 据报道,在The fmt library中讨论过的
  • this answer非常快,看起来像printf本身一样干净,但却是外部依赖

  • 用法很简洁:只需调用类似printf的函数
  • 据报道,fmt库非常快
  • 其他选项看起来非常紧凑(不需要外部依赖)

否定

  • fmt库虽然快,但是外部依赖
  • 其他选项显然存在一些性能问题

答案 1 :(得分:6)

在C ++ 11中,您可以使用std::to_string

"error! value was " + std::to_string(actualValue) + " but I expected " + std::to_string(expectedValue)

它并不漂亮,但它很简单,您可以使用宏来缩小它。性能不是很好,因为事先没有reserve()空间。 Variadic templates可能会更快,看起来更好。

这种字符串构造(而不是插值)对于本地化也是不好的,但如果需要,你可能会使用库。

答案 2 :(得分:4)

使用你喜欢的任何东西:

1)std :: stringstream

#include <sstream>
std::stringstream ss;
ss << "Hello world!" << std::endl;
throw std::runtime_error(ss.str());

2)libfmt:https://github.com/fmtlib/fmt

#include <stdexcept>
throw std::runtime_error(
    fmt::format("Error has been detected with code {} while {}",
        0x42, "copying"));

答案 3 :(得分:2)

在C ++ 20中,您将可以使用std::format

请参阅http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r9.html,以获取接受的论文。

这将支持python样式格式:

string s1 = std::format("{1} to {0}", "a", "b");

已经有一个实现:https://github.com/fmtlib/fmt

答案 4 :(得分:0)

免责声明: 后续代码基于我2年前阅读的一篇文章。我会找到源并将其尽快放在这里。

这就是我在C ++ 17项目中使用的东西。应该可以与任何支持可变参数模板的C ++编译器一起使用。

用法:

std::string const word    = "Beautiful";
std::string const message = CString::format("%0 is a %1 word with %2 characters.\n%0 %2 %0 %1 %2", word, "beautiful", word.size()); 
// Prints:
//   Beautiful is a beautiful word with 9 characters. 
//   Beautiful 9 Beautiful beautiful 9.

该类的实现:

/**
 * The CString class provides helpers to convert 8 and 16-bit
 * strings to each other or format a string with a variadic number
 * of arguments.
 */
class CString
{
    public:
    /**
     * Format a string based on 'aFormat' with a variadic number of arbitrarily typed arguments.
     *
     * @param aFormat
     * @param aArguments
     * @return
     */
    template <typename... TArgs>
    static std::string format(
            std::string const&aFormat,
            TArgs        &&...aArguments);

    /**
     * Accept an arbitrarily typed argument and convert it to it's proper
     * string representation.
     *
     * @tparam TArg
     * @tparam TEnable
     * @param aArg
     * @return
     */
    template <
            typename TArg,
            typename TEnable = void
            >
    static std::string toString(TArg const &aArg);

    /**
     * Accept a float argument and convert it to it's proper string representation.
     *
     * @tparam TArg
     * @param arg
     * @return
     */
    template <
            typename TArg,
            typename std::enable_if<std::is_floating_point<TArg>::value, TArg>::type
            >
    static std::string toString(const float& arg);


    /**
     * Convert a string into an arbitrarily typed representation.
     *
     * @param aString
     * @return
     */
    template <
            typename TData,
            typename TEnable = void
            >
    static TData const fromString(std::string const &aString);


    template <
            typename TData,
            typename std::enable_if
                     <
                        std::is_integral<TData>::value || std::is_floating_point<TData>::value,
                        TData
                     >::type
            >
    static TData fromString(std::string const &aString);

private:
    /**
     * Format a list of arguments. In this case zero arguments as the abort-condition
     * of the recursive expansion of the parameter pack.
     *
     * @param aArguments
     */
    template <std::size_t NArgs>
    static void formatArguments(std::array<std::string, NArgs> const &aArguments);

    /**
     * Format a list of arguments of arbitrary type and expand recursively.
     *
     * @param outFormatted
     * @param inArg
     * @param inArgs
     */
    template <
            std::size_t NArgs,
            typename    TArg,
            typename... TArgs
            >
    static void formatArguments(
            std::array<std::string, NArgs>     &aOutFormatted,
            TArg                              &&aInArg,
            TArgs                          &&...aInArgs);
};
//<-----------------------------------------------------------------------------

//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <typename... TArgs>
std::string CString::format(
        const std::string     &aFormat,
        TArgs             &&...aArgs)
{
    std::array<std::string, sizeof...(aArgs)> formattedArguments{};

    formatArguments(formattedArguments, std::forward<TArgs>(aArgs)...);

    if constexpr (sizeof...(aArgs) == 0)
    {
        return aFormat;
    }
    else {
        uint32_t number     = 0;
        bool     readNumber = false;

        std::ostringstream stream;

        for(std::size_t k = 0; k < aFormat.size(); ++k)
        {
            switch(aFormat[k])
            {
            case '%':
                readNumber = true;
                break;
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                // Desired behaviour to enable reading numbers in text w/o preceding %
                #pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
                if(readNumber)
                {
                    number *= 10;
                    number += static_cast<uint32_t>(aFormat[k] - '0');
                    break;
                }
            default:
                if(readNumber)
                {
                    stream << formattedArguments[std::size_t(number)];
                    readNumber = false;
                    number     = 0;
                }

                stream << aFormat[k];
                break;
                #pragma GCC diagnostic warning "-Wimplicit-fallthrough"
            }
        }

        if(readNumber)
        {
            stream << formattedArguments[std::size_t(number)];
            readNumber = false;
            number     = 0;
        }

        return stream.str();
    }
}
//<-----------------------------------------------------------------------------

//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <typename TArg, typename enable>
std::string CString::toString(TArg const &aArg)
{
    std::ostringstream stream;
    stream << aArg;
    return stream.str();
}
//<-----------------------------------------------------------------------------

//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <
        typename TArg,
        typename std::enable_if<std::is_floating_point<TArg>::value, TArg>::type
        >
std::string CString::toString(const float& arg) {
    std::ostringstream stream;
    stream << std::setprecision(12) << arg;
    return stream.str();
}
//<-----------------------------------------------------------------------------

//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <std::size_t argCount>
void CString::formatArguments(std::array<std::string, argCount> const&aArgs)
{
    // Unused: aArgs
}
//<-----------------------------------------------------------------------------

//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <std::size_t argCount, typename TArg, typename... TArgs>
void CString::formatArguments(
        std::array<std::string, argCount>     &outFormatted,
        TArg                                 &&inArg,
        TArgs                             &&...inArgs)
{
    // Executed for each, recursively until there's no param left.
    uint32_t const index = (argCount - 1 - sizeof...(TArgs));
    outFormatted[index] = toString(inArg);

    formatArguments(outFormatted, std::forward<TArgs>(inArgs)...);
}
//<-----------------------------------------------------------------------------

//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <
        typename TData,
        typename std::enable_if
                 <
                    std::is_integral<TData>::value || std::is_floating_point<TData>::value,
                    TData
                 >::type
        >
TData CString::fromString(std::string const &aString)
{
    TData const result{};

    std::stringstream ss(aString);
    ss >> result;

    return result;
}
//<-----------------------------------------------------------------------------

}