These articles are written by Codalogic empowerees as a way of sharing knowledge with the programming community. They do not necessarily reflect the opinions of Codalogic.

Formatting Strings with C++20's std::format()

By: Pete, August 2022

A while back I blogged about using std::string and std::to_string as a cheap and cheerful way of doing string formatting as an alternative to using std::ostringstream and sprintf.

Now in C++20 we officially have std::format added. (Note however that as of writing it is only implemented in Visual Studio MSVC and not implemented in GCC or Clang.)

std::format ends up looking much like sprintf except that all the arguments are type-safe and it returns a std::string (or std::wstring).

In the original blog post I had code like:

fake_widget( std::string( "Can't open file: " ).append( file_name ) );
fake_widget( std::string( "Volume = " ).append( std::to_string( volume ) ) );

With std::format we can replace this with:

fake_widget( std::format( "Can't open file: {}", file_name ) );
fake_widget( std::format( "Volume = {}", volume ) );

Here the {}s represent where the parameter values are to be substituted.

For longer messages where previously I did this:

fake_widget( std::string( "You have " )
                .append( std::to_string( n_errors ) )
                .append( " errors in " )
                .append( std::to_string( n_files ) )
                .append( " files." ) );

You can now do:

fake_widget( std::format( "You have {} errors in {} files.", n_errors, n_files ) );

Much nicer.

Within the {}s you can include lots of formatting, including the index of the argument you want inserted at that position. For example, you can do:

fake_widget( std::format( "{1}, I said {1}, is bigger that {0}.", 2, 3 ) );

Which outputs:

3, I said 3, is bigger that 2.

This ability to change the parameter output order makes it appealing to consider using std::format for i18n internationalisation translation functionality where the translated text may require the parameters to be used in a different order. It would be nice to be able to do something like the following (where translate_fmt() returns a string in the nationalised language):

// Doesn't compile :(
fake_widget( std::format( translate_fmt( "You have {} errors in {} files." ), n_errors, n_files ) );

Alas, std::format requires that the format string be provided in a constexpr context so the above won't compile.

A workaround is to use the std::vformat sister function and std::make_format_args:

fake_widget( std::vformat( translate_fmt( "You have {} errors in {} files." ),
                           (n_errors, n_files) ) );

But this is pretty ugly! Fortunately adding a template helper function makes this much better:

template <typename... Args>
std::string translate( const std::string_view fmt, Args&&... args )
{
    return std::vformat( translate_fmt( fmt ), std::make_format_args( args... ) );
}

Then you can do:

fake_widget( translate( "You have {} errors in {} files.", 8, 4 ) );

Which in this case, due to my quirky translation logic, outputs:

In 4 files you have made 8 errors.

The whole test program is below and can be played with at: https://godbolt.org/z/n3ofTo8rT

#include <iostream>
#include <string>
#include <string_view>
#include <format>

void fake_widget( const std::string & s )
{
    std::cout << s << "\n";
}

// Pretend these are set earlier in the application
std::string file_name{ "notes.txt" };
int volume = 6;
int n_errors = 8;
int n_files = 4;

const char * translate_fmt( const std::string_view text )
{
    // Translations would normally be loaded from a translation file,
    // so this function can't be constexpr.  But for now...
    if( text == "You have {} errors in {} files." )
        return "In {1} files you have made {0} errors.";
    return text.data();
}

template <typename... Args>
std::string translate( const std::string_view fmt, Args&&... args )
{
    return std::vformat( translate_fmt( fmt ), std::make_format_args( args... ) );
}

int main()
{
    // Examples from the previous blog
    fake_widget( std::format( "Can't open file: {}", file_name ) );
    fake_widget( std::format( "Volume = {}", volume ) );

    // A longer message
    fake_widget( std::format( "You have {} errors in {} files.", n_errors, n_files ) );

    // The braces in the format can include foratting instructions,
    // including positional indicators.  e.g. Let's reverse the arguments
    fake_widget( std::format( "{1}, I said {1}, is bigger that {0}.", 2, 3 ) );

    // Thus potentially std::format could also help with message internationalisation
    // but the format supplied to std::format must be const so this doesn't work
    //fake_widget( std::format( translate_fmt( "You have {} errors in {} files." ), n_errors, n_files ) );

    // Instead we can do this, but it is a bit messy
    fake_widget( std::vformat( translate_fmt( "You have {} errors in {} files." ),
                                std::make_format_args(n_errors, n_files) ) );

    // To compensate we can write a helper template function
    fake_widget( translate( "You have {} errors in {} files.", 8, 4 ) );
}

This outputs the following:

Can't open file: notes.txt
Volume = 6
You have 8 errors in 4 files.
3, I said 3, is bigger that 2.
In 4 files you have made 8 errors.
In 4 files you have made 8 errors.

As std::format isn't in GCC and Clang yet, maybe it's a bit too early to get excited about it, but I think it will be very beneficial in the future.

Before finishing it's perhaps worth confirming that you can obviously use std::format() in other places, such as with std::cout:

std::cout << std::format( "You have {} errors in {} files.", n_errors, n_files );