0

Out of curiosity, and with hope of learning useful compile-time techniques, I'm trying to understand how std::format performs compile-time checking of its arguments.

I started from the implementation and cross-referenced it with this StackOverflow answer.

I'm thus trying a simplified use-case where I want to check if the number of {} placeholders in a format string, provided at compile-time, is less than or equal to the number of provided arguments. Here is my attempt:

#include <string_view>

// compile-time count of "{}" inside "str"
consteval int count_occurrences(std::string_view str) {
    int count = 0;
    size_t pos = 0;
    std::string_view pattern("{}");

    while ((pos = str.find(pattern, pos)) != std::string_view::npos) {
        ++count;
        pos += pattern.length();
    }

    return count;
}

template <typename... Args>
struct FormatStringImpl {
    template <typename Str>
    consteval FormatStringImpl(Str const& str) : frmt(str) {
        // frmt is not considered as a constant expression so the next line
        // obviously doesn't compile
        auto constexpr n = count_occurrences(frmt);
        static_assert(sizeof...(Args) >= n, "wrong number of arguments");
    }
    std::string_view frmt;
};

// don't try to deduce Args from FormatString<Args...> in foo call
template <typename... Args>
using FormatString = FormatStringImpl<std::type_identity_t<Args>...>;

template <typename... Args>
consteval void foo([[maybe_unused]] FormatString<Args...> const frmt,
                   [[maybe_unused]] Args&&... args) {};

DEMO

The general idea is to perform the check during compile-time construction of the argument frmt (and not inside foo() where no input argument could be used inside a constant evaluated expression).

Another trick is to make the format string type dependent on the types of the other arguments of foo() (and not their values).

Now I am stuck, because I don't see how to proceed with the check inside the constructor. My attempt obviously fails, because I am trying to initialize a compile-time expression from a formally non-constant expression. Looking at a Standard Library implementation quickly loses me in the details that prevent me from getting the main ideas (which I could reuse with more elaborated checks).

I found a possible solution from this other StackOverflow answer:

    consteval FormatStringImpl(Str const& str) : frmt(str) {
        // frmt is not considered as a constant expression so I'm not using a constexpr variable anymore
        auto n = count_occurrences(frmt);
        if (sizeof...(Args) < static_cast<std::size_t>(n))
        {
            // is it legal?!
            throw "wrong number of arguments";
        };

DEMO

Here, I am removing the need for a constant expression, but I thought that using a throw inside a constexpr/consteval function was ill-formed, and I am unsure that it is required for the compiler to emit an error if it encounters a throw during a compile-time evaluation (AFAIK, it is IFNDR: https://timsong-cpp.github.io/cppwp/n4861/dcl.constexpr#6).

What is the right technique to move on with the check? Is there a way to make use of a constant expression (and possibly static_assert)? Can the throw solution be used (update AFAIK, MSVC's implementation uses throw).

8
  • "(experimentally the behavior looks different with constexpr and consteval)" constexpr version will error out if called in constant expression. (providing constant parameter is not enough). consteval force to be in constant expression, so will error out when throw. Commented Jun 6 at 10:50
  • @Jarod42 sorry I just changed the text you are referring to Commented Jun 6 at 10:52
  • Working Demo Commented Jun 6 at 11:01
  • It is IFNDR only when you unconditionally throw in a constexpr function. Commented Jun 6 at 11:07
  • 1
    Notice in the example that f is constexpr, and f cannot produce constant expression. Commented Jun 6 at 12:03

1 Answer 1

1

I thought that using a throw inside a constexpr/consteval function was ill-formed and I am unsure that it is require for compiler to emit an error if it encounters a throw during a compile-time evaluation (AFAIU, it is IFNDR: https://timsong-cpp.github.io/cppwp/n4861/dcl.constexpr#6 ).

That paragraph, emphasis mine

if no argument values exist such that an invocation of the function or constructor could be an evaluated subexpression of a core constant expression, or, for a constructor, an evaluated subexpression of the initialization full-expression of some constant-initialized object ([basic.start.static]), the program is ill-formed, no diagnostic required.

is to specify that the function could be called correctly by at least one parameters set.

BTW, I think this paragraph has been removed to ease code to be valid with multiple different standard (as constexpr is added more and more, and allow more and more stuff)

Can the throw solution be used?

Yes, and it is used by several libraries in that context.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.