Search code examples
c++templatesconstexprcompile-time

How to detect const char* format errors at C++ compile time?


I'm trying to write a function "my_func()" that counts the number of characters "a" in a string at compile time, which makes the code fail to compile when the count of "a" is wrong.

I was inspired by the function std::format() in the C++ standard library, which checks the number of {} in the format string.

The compiler I use is msvc, C++20.

My code below won't compile because I don't know how to implement functionality like this. So how can I fix function my_func()?

template <size_t Size>
auto my_func(const char(&str)[Size]) -> void {
    // dosomething...

    constexpr size_t count = 0;
    const char* c = str;
    for (size_t i = 0; i < Size; ++i) {
        if (*c == 'a') {
            count++; // const values cannot be modified
        }
        c++;
    }

    // If the variable `count ` is not set to constexpr, an error will be reported here.
    static_assert(count == 2);

    // dosomething...
}

auto main() -> int {
    my_func("abc abc"); // is error
}

Next

Thanks to @ecatmur for the answer, but I don't know how to get the value of str when str is converted to counting_string.

I tried passing the constructor's template parameters to the class, but this prevented me from finding the matching overloaded function when calling the prepare function.

template <char Char, size_t Count, size_t Size>
struct counting_string {
    std::array<char, Size> m_chars;

    // NOLINTNEXTLINE(google-explicit-constructor)
    consteval explicit(false) counting_string(char const (&str)[Size]) {
        size_t count = 0;
        const char* c = str;
        for (size_t i = 0; i < Size; ++i) {
            if (*c == Char) {
                count++;
            }
            c++;
        }
        if (count != Count) {
            throw "invalid str";
        }
    }
};

template <size_t Size, class... Args>
auto prepare(const counting_string<'?', sizeof...(Args), Size> sql, Args... args) -> void {
// use sql as: sql.m_chars.data();
}

auto main() -> int {
    // call function error
    // prepare("insert into test (name, age) values (?, ?)", 1, 2);
}

The code below is the end result I hope to achieve. Without extra overhead (compile-time checking), check the number of placeholders in SQL statements to avoid mistakes.

template <char Char, size_t Count>
struct constexpr_counting_string {
    template <size_t Size>
    // NOLINTNEXTLINE(google-explicit-constructor)
    consteval explicit(false) constexpr_counting_string(char const (&str)[Size]) {
        size_t count = 0;
        const char* c = str;
        for (size_t i = 0; i < Size; ++i) {
            if (*c == Char) {
                count++;
            }
            c++;
        }
        if (count != Count) {
            throw "invalid str";
        }
    }
};

// prepare and bind
template <typename... Args>
auto prepare(statement_t& stmt, constexpr_counting_string<'?', sizeof...(Args)> sql, Args... args) noexcept -> code_t {
    // prepare
    code_t error = helper_prepare(stmt, sql);

    // bind
    auto do_bind = [](statement_t& stmt, size_t n, auto arg, code_t& error) noexcept {
        if (error != code_t::ok) {
            return;
        }

        using arg_type = decltype(arg);

        if constexpr (std::is_same_v<arg_type, uint64_t>) {
            error = stmt.bind(n, arg);
        }
        else if constexpr (std::is_same_v<arg_type, int64_t>) {
            error = stmt.bind(n, arg);
        }
        else if constexpr (std::is_same_v<arg_type, int>) {
            error = stmt.bind(n, arg);
        }
        else if constexpr (std::is_same_v<arg_type, const char*>) {
            error = stmt.bind(n, arg);
        }
        else {
            static_assert(std::is_same_v<arg_type, uint64_t>, "args error");
        }
    };

    size_t index = 0;
    (do_bind(stmt, index++, args, error), ...);
    return error;
}

auto main() -> int {
    prepare("insert into test (name, age) values (?, ?)", 1, 2);
}

Solution

  • The key is that std::format checks in the consteval constructor of std::format_string that the format-string argument is well-formed and appropriate for the std::format arguments that follow.

    This means that you need to invert your logic; the simplest way would be to write a counting_string type whose consteval constructor accepts only string literals containing 2 'a's:

    struct counting_string {
        template<unsigned Size>
        consteval explicit(false) counting_string(char const (&str)[Size]) {
            unsigned count = 0;
            const char* c = str;
            for (unsigned i = 0; i < Size; ++i) {
                if (*c == 'a') {
                    count++; // const values cannot be modified
                }
                c++;
            }
            if (count != 2) throw "invalid str";
        }
    };
    auto my_func(counting_string str) -> void {
        ; // if we get here we know `str` contains 2 'a's
    }
    

    Example.

    For style points, you might want to make counting_string a class template.

    For the follow up question, the problem is that we can't use std::string in the consteval constructor since it would need to allocate memory, and (currently) memory allocations can't be passed from compile time to run time. Also, we can't use a static string (or, e.g., std::array<char, Size>) since class template argument deduction (CTAD) isn't smart enough to infer a Size class template parameter from a function template function argument. (Annoyingly, CTAD is smart enough to do so from a non-type function template template argument - but that doesn't help here.)

    So, the only thing to do is to store a pointer (or, e.g., a std::string_view) to the string literal in the counting_string, and hope that the argument that the user passed in has a long enough lifetime (if it is indeed a string literal, it'll be fine - the only problem would be if it was the char array data member of a static string or std::array<char>.) Indeed, this is how std::format_string works, so it has the same potential problem. Example:

    #include <string_view>
    struct counting_string {
        template<unsigned Size>
        consteval explicit(false) counting_string(char const (&str)[Size]) : sv(str, Size) {
            unsigned count = 0;
            for (char const c : sv)
                if (c == 'a')
                    count++; // const values cannot be modified
            if (count != 2) throw "invalid str";
        }
        std::string_view sv;
    };