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) {};
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";
};
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).
constexprversion will error out if called in constant expression. (providing constant parameter is not enough).constevalforce to be in constant expression, so will error out whenthrow.throwin aconstexprfunction.fisconstexpr, andfcannot produce constant expression.