Leo Violette
Leo Violette

Reputation: 83

Wrapping std::format in a template function fails to compile with the latest MSVC compiler update

I'm a bit stumped as to why this code has suddenly stopped compiling: https://godbolt.org/z/hhM5GG78x

But if I change the compiler back v19.31, it will compile: https://godbolt.org/z/11j8WbEzG

Here's the code in question:

#include <format>
#include <string>

template <typename... Args>
void FormatString(const std::string_view& fmt_str, Args&&... args)
{
    puts(std::format(fmt_str, args...).c_str());
}

int main()
{
    FormatString("This is a {}.\n", "test");
    return 0;
}

Here's the error message I'm receiving:

<source>(7): error C7595: 'std::_Basic_format_string<char,const char (&)[5]>::_Basic_format_string': call to immediate function is not a constant expression
<source>(7): note: failure was caused by a read of a variable outside its lifetime
<source>(7): note: see usage of 'fmt_str'
<source>(12): note: see reference to function template instantiation 'void FormatString<const char(&)[5]>(const std::string_view &,const char (&)[5])' being compiled

It's complaining about fmt_str being used outside of it's lifetime. I'm failing to see how this is the case?

Upvotes: 8

Views: 2158

Answers (1)

user17732522
user17732522

Reputation: 76688

The first argument to std::format must be known at compile-time, since the format string is specified to be only constructible as a constant expression. The purpose is to guarantee compile-time errors for invalid format strings.

fmt_str is a function parameter and so its value is never a compile-time constant.

You can use std::vformat instead, but it will perform no compile-time checks of the format string, instead delaying it to runtime (throwing std::format_error on error):

puts(std::vformat(fmt_str, std::make_format_args(args...)).c_str());

If you don't need fmt_str to be runtime-time-dependent, you can pass it as a template paramter instead. Unfortunately this isn't quite so straight-forward at the moment since std::string and std::string_view cannot be used for that and string literals can't be passed directly via const char* non-type template argument.

So you would probably want to create your own structural fixed-length string type that can be used as non-type template parameter, e.g. here a very minimal version for the use case, which you would probably want to extend as suits your needs:

template<std::size_t N>
struct fixed_string {
    char str[N];
    constexpr fixed_string(const char (&str_)[N]) noexcept {
        std::ranges::copy(str_, str);
    }
};

template <fixed_string fmt_str, typename... Args>
void FormatString(Args&&... args)
{
    puts(std::format(fmt_str.str, args...).c_str());
}

int main()
{
    FormatString<"This is a {}.\n">("test");
    return 0;
}

I would assume that MSVC simply hadn't yet implemented the requirement that the first parameter of std::format be constructible as a constant expresssion in the previous versions.

The requirement was added after C++20 for C++23, but if understand correctly applies retroactively to C++20 as defect report as well, going by the votes listed in the paper here which contains the relevant changes.

Upvotes: 9

Related Questions