How to construct a std::string with embedded values, i.e. "string interpolation"?
Asked Answered
I

8

76

I want to create a string with embedded information. One way (not the only way) of achieving what I want is called string interpolation or variable substitution, wherein placeholders in a string are replaced with actual values.

In C, I would do something like this:

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

whereas if I were programming in python, I would do something like this:

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

both of these are examples of string interpolation.

How can I do this in C++?

Important Caveats:

  1. I know that I can use std::cout if I want to print such a message to standard output (not string interpolation, but prints out the kind of string I want):
cout << "error! value was " << actualValue << " but I expected "
<< expectedValue;

I don't want to print a string to stdout. I want to pass a std::string as an argument to a function (e.g. the constructor of an exception object).

  1. I am using C++11, but portability is potentially an issue, so knowing which methods work and don't work in which versions of C++ would be a plus.

Edit

  1. For my immediate usage, I'm not concerned about performance (I'm raising an exception for cryin' out loud!). However, knowing the relative performance of the various methods would be very very useful in general.

  2. Why not just use printf itself (C++ is a superset of C after all...)? This answer discusses some reasons why not. As far as I can understand, type safety is a big reason: if you put %d, the variable you put in there had better really be convertible to an integer, as that's how the function figures out what type it is. It would be much safer to have a method which uses compile-time knowledge of the actual type of the variables to be inserted.

Ivonneivor answered 21/6, 2016 at 23:16 Comment(9)
c++ still doesn't have a standard way of doing this? I am surprised, it managed to get threads but not a modern printfSumba
What about just using printf or fprintf(std::cout, ... in c++?Hailee
Stroustrupp puts together a nice example of using variadic template functions with type-safe printf. I actually think it's a good implementation.Sundried
@Ben: that's a great question. I've added a note about that topic.Ivonneivor
This is called string interpolation, by the way.Aphasia
@KretabChabawenizc thanks, knowing that term is helpful. In the same vein as pm100 's comment, I note that the wikipedia page on string interpolation (linked) gives examples for many many languages, but not C++Ivonneivor
...%d is int, not double.Polyamide
There's a long-standing concern that printf & co. are not type-safe, but these days (2022+) both the compilers I use, MSVC and gcc, do a great job of, well, not so much type checking as safety-checking the arguments to the standard *printf functions and issuing warnings. If you set "treat warnings as errors" (a good idea) then printf seems not more dangerous than using <<, although you don't get all the nice implicit conversions (e.g. std::string).Langsdon
since it came up in comments and was upvoted.. OPs Q is not "string interpolation" it is "string formatting". interpolation mixes inputs into the format string directly, such as myVar="World"; x = "Hello, {myVar}!"; x=="Hello, World!" string formatting uses input arguments separate from a format string containing placeholders/specifiers of how those inputs should be formatted. anyone referring to "string formatting" as "string interpolation" has a gross misunderstanding of what string interpolation is.Scab
W
61

In C++20 you will be able to use std::format.

This will support python style formatting:

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

There is already an implementation available: https://github.com/fmtlib/fmt.

Willtrude answered 20/7, 2019 at 22:17 Comment(5)
Also see std::format() on cppreference.com. Note that specifying indexes in the format placeholders is optional: string s1 = std::format("{} to {}", "a", "b");Gathard
not found in g++ v 11.2Heall
This is not nearly as good as the Python f-string formatting which would be just f"{a} to {b}"Unpen
This is the worst answer here. std::format() doesn't exist anywhere in c++20Thallus
How to I update the STL to C++20. The GCC-Ubuntu package seems to be outdated and doing it by hand looks like pure pain. With Clang it does not work either and/or you need to change the CMakeLists-File. https://mcmap.net/q/270168/-what-is-the-correct-way-to-include-llvm-library-in-clionBacciferous
I
38

Method 1: Using a string stream

It looks like std::stringstream gives a quick solution:

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

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

Positive

  • no external dependencies
  • I believe this works in C++ 03 as well as c++ 11.

Negative

  • reportedly quite slow
  • a bit more messy: you must create a stream, write to it, and then get the string out of it.

Method 2: Boost Format

The Boost Format library is also a possibility. Using this, you would do:

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

Positive

  • pretty clean compared to stringstream method: one compact construct

Negative

  • reportedly quite slow: uses the stream method internally
  • it's an external dependency

Edit:

Method 3: variadic template parameters

It seems that a type-safe version of printf can be created by using variadic template parameters (the techincal term for a template that takes an indefinite number of template parameters). I have seen a number of possibilities in this vein:

  • This question gives a compact example and discusses performance problems with that example.
  • This answer to that question, whose implementation is also quite compact, but reportedly still suffers from performance issues.
  • The fmt library, discussed in this answer, is reportedly quite fast and seems to be as clean as printf itself, but is an external dependency

Positive

  • usage is clean: just call a printf-like function
  • The fmt library is reportedly quite fast
  • The other options seem quite compact (no external dependency required)

Negative

  • the fmt library, while fast, is an external dependency
  • the other options apparently have some performance issues
Ivonneivor answered 21/6, 2016 at 23:16 Comment(7)
it's worth noting that the first solution will be very hard to localize later. Meaningful message is spilt into not meaningful parts, and some languages may require different order of parameters to form a sound messageGranese
That is a good point, though one could contend that the second way is really no easier to search for. When looking at an error message like that, I would tend to search for what look like the constant parts anyhow (i.e. I'd search for "error! value was", or for "but I expected"). Ideally, the error message itself would have some unique leading thing that could be searched for (e.g. "error #5:"), but that's a question of how the errors are structured...Ivonneivor
@deniss I'm not following what you mean by "some languages may require different order of parameters": aren't we only dealing with C++?Ivonneivor
@Ivonneivor "Some languages" as in real languages, not programming languages.Pye
@stochastic, "Undefined reference to sin" gives better search results than just "Undefined reference", so it depends. By languages, I mean human languages. Some languages may require different order of parameters to not sound like Yoda. Some may require extra punctuation after the last parameter.Granese
Yes, its best to localize the entire string as a whole. An alternative to using boost::format() would be simply std::string::replace() to fill in the placeholders, eg: std::string s = "error! value was %1% but I expected %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); Not as elegant as boost::format(), or even std::stringstream, but still usable in simple cases.Gathard
For reference: stringstream lives in <sstream>Gradeigh
P
20

In C++11 you can use std::to_string:

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

It's not pretty, but it's straightforward, and you can use a macro to shrink it a bit. Performance is not great, since you do not reserve() space beforehand. Variadic templates would probably be faster and look nicer.

This kind of string construction (instead of interpolation) is also bad for localization, but you'd probably use a library if you needed that.

Predatory answered 15/11, 2016 at 7:10 Comment(0)
A
5

Use whatever you like:

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"));
Aeneas answered 15/11, 2016 at 7:24 Comment(0)
K
3

C++17 solution that works both for std::string & for std::wstring (Tested on 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."
}
Kumasi answered 7/8, 2022 at 6:56 Comment(3)
I can't see what the problem is but neither clang++ nor g++ seems to like the swprintf. DemoDictator
Found it. std::swprintf: "While narrow strings provide std::snprintf, which makes it possible to determine the required output buffer size, there is no equivalent for wide strings, and in order to determine the buffer size, the program may need to call std::swprintf, check the result value, and reallocate a larger buffer, trying again until successful."Dictator
Possible fixDictator
U
3

I've grown very fond of this solution, std::format notwithstanding. I dislike it on several counts (use of macros, and the whole concept of operator << overloading). But the ease of use truly makes up for it.

#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

usage:

    std::string result = SS("ABC: " << 123, " DEF: " << 3.45 << std::endl);
Unaunabated answered 23/2, 2023 at 17:48 Comment(0)
D
1

DISCLAIMER:
The subsequent code is based on an article I read 2 years ago. I will find the source and put it here ASAP.

This is what I use in my C++17 project. Should work with any C++ compiler supporting variadic templates though.

Usage:

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 class implementation:

/**
 * 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;
}
//<-----------------------------------------------------------------------------
Delafuente answered 21/2, 2019 at 6:49 Comment(0)
F
1

If you don't mind using a preprocessor script, here is a more easy but handy solution: https://github.com/crazybie/cpp_str_interpolation. Then you can write the code like this:

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

it also support using like a template engine:

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;
Foreordination answered 26/11, 2022 at 2:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.