top of page

Playing the strings: C++20 std::format: Compile-Time vs. Runtime

C++20 std::format is not all kitties and rainbows, at least until C++26 comes to the rescue. The issue relates to compile-time Vs runtime format strings — it seems like there’s no way around it — or is it?


C++20 introduced the long-awaited std::format function, a powerful tool for creating formatted strings, especially if we add some C++23 additions such as std::print and std::printl, as explained in my new C++ book Learning C++.

However, std::format is not all kitties and rainbows — and here’s why. Though std::format offers a great deal of flexibility over the old way of formatting strings, there’s a catch: a crucial distinction needs to be made when it comes to compile-time and runtime format strings, and it might turn your hair gray before you figure out the problem.


While working on our obfuscation tool, Tiny Obfuscate, and obfuscating some strings that are in fact formatting strings used by std::format I encountered this problem.


Compile-Time Format Strings

In the realm of compile-time format strings, the format is known and fixed at compile-time. This means that the format string is a constant expression, created during the build process. The benefit of using compile-time format strings is the ability to leverage the full potential of the C++ compiler to optimize and validate the format string.


Let’s start with the simplest example:


In this example we will use L”1-{}\n2-{}\n” as our formatting string for std::format. That’s the string we obfuscated using TinyObfuscate.


The format string L”1-{}\n2-{}\n” is a C++20-style format string that contains placeholders for values to be inserted: 1- and 2-: These are literal characters that will be included in the final formatted string as-is. They are hardcoded strings that will remain unchanged in the output.

{} (Curly Braces): These are placeholders indicating the locations where values should be inserted into the string. The {} serves as a placeholder for the arguments provided when using std::format. In this case, it expects two arguments, one for each set of curly braces.

Our expected output contains both and should be something like:

Here is the source code:

#include <iostream>
#include <format>
int main()
{ 
   std::wstring s1{ L"string1" }; 
   std::wstring s2{ L"string2" }; 
   std::wcout << std::format(L"1-{}\n2-{}\n", s1, s2);
}

The output will be:

Consider the following example that does the same, but uses a function called by std::format():

#include <iostream>
#include <format>
wchar_t const* myformat()
wchar_t const* result = L"1-{}\n2-{}\n"return result;
}
int main()
{ 
   std::wstring s1{ L"string1" }; 
   std::wstring s2{ L"string2" }; 
   std::wcout << std::format(myformat(), s1, s2);
}

This code won’t compile:

error C7595: ‘std::basic_format_string<wchar_t,std::wstring &,std::wstring &>::basic_format_string’: call to immediate function is not a constant expression

1>failure was caused by call of undefined function or one not declared ‘constexpr’

However we can fix the problem by changing our code to the following, declaring the function as constexp.

#include <iostream>
#include <format>
constexpr const wchar_t * myformat()
wchar_t const* result = L"1-{}\n2-{}\n"return result;
}
int main()
{ 
   std::wstring s1{ L"string1" }; 
   std::wstring s2{ L"string2" }; 
   std::wcout << std::format(myformat(), s1, s2);
}

In this example, myformat() returns a compile-time constant expression, allowing the compiler to perform optimizations and catch potential errors during compilation. This code will work seamlessly. However, our function won’t be able to compose the string during runtime, nor make any changes to the initial value of ‘result’.

Runtime Format Strings

Let’s dive deeper into the cause of the issue: The pitfall arises when attempting to use dynamically set or changed values as part of the format string. Unlike compile-time format strings, runtime format strings are created or modified during the execution of the program. Unfortunately, using such runtime format strings with std::format results in compilation errors.

#include <iostream>
#include <format>
constexpr const wchar_t * myformat()
wchar_t result[11]; 
   wcscpy_s(result, 11, L"1-{}\n2-{}\n"); 
   return result;
}
int main()
{ 
   std::wstring s1{ L"string1" }; 
   std::wstring s2{ L"string2" }; 
   std::wcout << std::format(myformat(), s1, s2);
}

This code will not compile, and we get the following compilation errors:

error C7595: ‘std::basic_format_string<wchar_t,std::wstring &,std::wstring &>::basic_format_string’: call to immediate function is not a constant expression

1>function violates ‘constexpr’ rules or has errors

These errors are related to the inability to treat the format string as a constant expression.

The Solution: std::vformat and make_wformat_args

To overcome this limitation without waiting for potential enhancements in future C++ standards (hello C++26), you can use std::vformat along with make_wformat_args.

std::vformat is a lower-level formatting function that is intended for use in situations where more fine-grained control is needed. For example, std::vformat can be used to format strings that are defined at runtime, and it can also be used to format strings using custom formatters.

std::format, on the other hand, is designed to be a safer and more flexible alternative to the printf family of functions. std::format can handle both compile-time and runtime format strings.

std::make_wformat_args function was introduced in C++20, and is a template function that constructs an object that stores an array of formatting arguments for formatting strings according to a wide character format string. The arguments can be of various types, including integral types, floating-point types, strings, and custom formatters.

The combination of std::vformatand make_wformat_args allows you to format strings at runtime by passing the arguments separately.

#include <iostream>
#include <format>
wchar_t formatString[11];
wchar_t* myformat()
wcscpy_s(formatString, 11, L"1-{}\n2-{}\n"); 
   return formatString;
}
int main()
{ 
   std::wstring s1{ L"string1" }; 
   std::wstring s2{ L"string2" }; 
   std::wcout << std::vformat(myformat(), std::make_wformat_args(s1, s2));
}

This code successfully compiles and runs, providing a workaround for using dynamic values in formatted strings.

C++ 26 is expected to introduce std::runtime_format() but until then, you should be aware of the limitations of std::format.

When facing scenarios that require dynamic formatting, the combination of std::vformat and make_wformat_args offers a viable solution until we can jump to C++ 26 and get the expected updates in future C++ standards.

Featured Posts

Recent Posts

Archive

Search By Tags

Follow Us

  • Facebook Basic Square
  • Twitter Basic Square
  • Google+ Basic Square
bottom of page