如何构造一个带嵌入值的std::string,即“字符串插值”?

69
我希望创建一个嵌入信息的字符串。实现这一目标的方法之一(并非唯一方法)被称为字符串插值或变量替换,在此方法中,字符串中的占位符将被实际值所替换。
在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
cout << "error! value was " << actualValue << " but I expected "
<< expectedValue;

1. 我不想将一个字符串打印到标准输出。我想将一个 std::string 作为参数传递给一个函数(例如,异常对象的构造函数)。 2. 我正在使用 C++11,但可移植性可能是一个问题,因此了解哪些方法在哪个版本的 C++ 中有效会是一个加分项。 3. 编辑
  1. 对于我目前的使用,性能并不是我的关注点(我正在抛出一个异常!)。然而,了解各种方法的相对性能在一般情况下非常有用。

  2. 为什么不直接使用printf本身(毕竟C++是C的超集...)?这个答案讨论了一些原因。据我所知,类型安全是一个很大的原因:如果你放置了%d,你放在那里的变量最好真的可以转换为整数,因为这是函数找出它是什么类型的方式。使用编译时知道实际要插入的变量类型的方法会更安全。


15
C++仍然没有一种标准的方法来做这件事吗?我很惊讶,它可以处理线程,但没有现代的printf。 - pm100
2
еңЁC++дёӯпјҢеҸӘдҪҝз”ЁprintfжҲ–fprintf(std::cout, ...)жҖҺд№Ҳж ·пјҹ - Fantastic Mr Fox
2
Stroustrup提供了一个很好的例子,展示了如何使用类型安全的printf来实现可变参数模板函数。我认为这是一个不错的实现。 - SergeyA
8
顺便提一下,这被称为字符串插值。 - user703016
1
%dint,不是 double - T.C.
显示剩余3条评论
8个回答

57

在C ++20中,您将能够使用std::format

这将支持Python风格的格式化:

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

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


7
请参考 cppreference.com 上的 std::format()。请注意,在格式占位符中指定索引是可选的:string s1 = std::format("{} to {}", "a", "b"); 这将输出字符串 "a to b"。 - Remy Lebeau
1
在 g++ v 11.2 中未找到 - Boppity Bop
3
这并不像 Python f-string 格式化那样好,它只需要 f"{a} to {b}" - Gulzar
这是这里最糟糕的答案。在C++20中根本不存在std::format()函数。 - Jeff Davenport
如何将STL更新到C++20版本?GCC-Ubuntu软件包似乎已经过时,手动更新看起来非常痛苦。使用Clang也不起作用,或者需要更改CMakeLists文件。请参考https://stackoverflow.com/a/61046697/5817580。 - undefined

38

方法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 中使用。

缺点

  • 据报道速度较慢
  • 有些混乱:你必须创建一个流,向其中写入内容,然后将其转换为字符串。

方法二:Boost Format

Boost Format 库也是一种可能的选择。使用该库时,您可以执行以下操作:

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

积极

  • 与stringstream方法相比,非常简洁:一个紧凑的结构

消极

  • 据报道,速度相当慢:内部使用流方法
  • 这是一个外部依赖项

编辑:

方法3:可变模板参数

似乎可以通过使用可变模板参数(接受不确定数量的模板参数的技术术语)创建类型安全版本的printf。我在这方面看到了许多可能性:

  • 这个问题给出了一个简洁的例子,并讨论了该例子的性能问题。
  • 这个答案对该问题进行了回答,其实现也相当简洁,但据说仍然存在性能问题。
  • fmt库,在这个答案中讨论,据说非常快,并且似乎与printf本身一样干净,但是它是一个外部依赖项

积极

  • 使用干净:只需调用类似于printf的函数
  • 据说fmt库非常快速
  • 其他选项看起来相当紧凑(不需要外部依赖项)

消极

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

8
值得注意的是,第一种解决方案以后很难本地化。有意义的信息被分成了无意义的部分,并且一些语言可能需要不同的参数顺序才能形成一个合理的信息。 - user2512323
1
那是一个很好的观点,尽管有人可能会争辩说第二种方式并不比第一种更容易搜索。当查看这样的错误消息时,我倾向于搜索看起来像常量部分的内容(即我会搜索"error! value was"或者"but I expected")。理想情况下,错误消息本身应该有一些独特的前导内容可以进行搜索(例如"error #5:"),但这是关于错误结构的问题... - stochastic
5
“一些语言”是指真正的语言,而不是编程语言。 - anon
1
@stochastic,“未定义的sin引用”比仅“未定义的引用”提供更好的搜索结果,因此这取决于情况。在这里所说的语言是人类语言。有些语言可能需要不同的参数顺序才能不像Yoda一样听起来。有些语言可能需要在最后一个参数之后加上额外的标点符号。 - user2512323
2
最好将整个字符串作为一个局部变量。使用boost::format()的另一种替代方法是使用std :: string :: replace()来填充占位符,例如:std :: string s =" 错误!值为%1%,但我期望%2%"; std :: string :: size_type idx = s.find("%1%"); s.replace(idx,3,std :: to_string(acutalValue)); idx = s.find("%2%"); s.replace(idx,3,std :: to_string(expectedValue)); throw MyException(s);虽然不像boost::format()或甚至std::stringstream那样优雅,但在简单情况下仍然可以使用。 - Remy Lebeau
显示剩余2条评论

19

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

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

它看起来不太美观,但很直接,并且可以使用宏来稍微缩小一点。性能不是很好,因为你没有事先reserve()空间。 可变参数模板 可能会更快,并且看起来更漂亮。

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


5

使用您喜欢的任何方式:

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

C++17解决方案,适用于std::stringstd::wstring(在VS2019和VS2022上测试通过):

#include <string>
#include <stdexcept>
#include <cwchar>
#include <cstdio>
#include <type_traits>

template<typename T, typename ... Args>
std::basic_string<T> string_format(T const* const format, Args ... args)
{
    int size_signed{ 0 };

    // 1) Determine size with error handling:    
    if constexpr (std::is_same_v<T, char>) { // C++17
        size_signed = std::snprintf(nullptr, 0, format, args ...);
    }
    else {
        size_signed = std::swprintf(nullptr, 0, format, args ...);
    }  
    if (size_signed <= 0) {
        throw std::runtime_error("error during formatting.");
    }
    const auto size = static_cast<size_t>(size_signed);

    // 2) Prepare formatted string:
    std::basic_string<T> formatted(size, T{});
    if constexpr (std::is_same_v<T, char>) { // C++17
        std::snprintf(formatted.data(), size + 1, format, args ...); // +1 for the '\0' (it will not be part of formatted).
    }
    else {
        std::swprintf(formatted.data(), size + 1, format, args ...); // +1 for the '\0' (it will not be part of formatted).
    }

    return formatted; // Named Return Value Optimization (NRVO), avoids an unnecessary copy. 
}


// USE EXAMPLE: //

int main()
{
    int i{ 0 }; 
    const std::string example1 = string_format("string. number %d.", ++i); // => "string. number 1."  
    const std::wstring example2 = string_format(L"wstring. number %d.", ++i); // => L"wstring. number 2."
}

我看不出问题在哪里,但是无论是clang++还是g++都好像不喜欢swprintf演示 - Ted Lyngmo
找到了。std::swprintf:_"虽然窄字符串提供了std::snprintf,可以确定所需的输出缓冲区大小,但宽字符串没有相应的函数。为了确定缓冲区大小,程序可能需要调用std::swprintf,检查结果值,并重新分配一个更大的缓冲区,直到成功为止。"_ - Ted Lyngmo
可能的修复 - Ted Lyngmo

3
我对这个解决方案非常喜爱,尽管 std::format 令人不满。我对它有几点不喜欢(使用宏以及整个重载 `operator <<` 的概念)。但是它的易用性真的弥补了这些缺点。
#ifndef SS_HPP
#define SS_HPP

#include <sstream>
#include <iostream>

// usage:   SS("xyz" << 123 << 45.6) returning a std::string rvalue.
#define SS(x) ( ((std::stringstream&)(std::stringstream() << x )).str())

#endif

使用方法:

    std::string result = SS("ABC: " << 123, " DEF: " << 3.45 << std::endl);

1

如果您不介意使用预处理器脚本,这里有一个更简单但是非常方便的解决方案:https://github.com/crazybie/cpp_str_interpolation。然后您就可以像这样编写代码:

string s1 = "world", s2 = "!";
cout << _F("hello, {s1+s2}") << endl;

它还支持像模板引擎一样使用:

int a = 1;
float b = 2.3f;
cout << _F(R"(
`for (int i=0; i<2; i++) {`
    a is {a}, i is {i}.
    a+i is {a+i}.
`}`
b is {b}.
cout << "123" << endl;`
)") << endl;

1

免责声明:
下面的代码基于我两年前读过的一篇文章。我会尽快找到源代码并放在这里。

这是我在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;
}
//<-----------------------------------------------------------------------------

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接