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);
}
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
}
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;
};