A compile-time created string is being used as a character array in a structure and its max size is driven by that string. Also that structure is being used as a member variable in another struct, and there are multiple of such string buffer structs in it, which in turn drives its storage size.
Using strings as template arguments does work, but introduces multiple code repetitions, spreading like wild fire through the source code. Matters get even worse when the first structure has multiple strings as template arguments (2 or even 3).
Here's a working example:
#include <cstddef>
static constinit decltype(auto) c = "abc";
//static constinit decltype(auto) c2 = "12";
// size of A is driven by compile-time-created strings
template<typename T, size_t N /*, typename T2, size_t N2*/>
struct A{
char str[N] {};
// char str2[N2] {};
consteval A(const T(&s)[N] /*, const T2(&s2)[N2]*/) {
for(int i = 0; i < N; ++i){
str[i] = s[i];
if(s[i] == 0) // for strings shorter than storage
break;
}
// for(int i = 0; i < N2; ++i){
// str2[i] = s2[i];
// if(s2[i] == 0)
// break;
// }
}
};
// dummy function which's sole purpose is to help with the type deduction
template<typename T, size_t N /*, typename T2, size_t N2*/>
consteval auto f(const T(&s)[N] /*, const T2(&s2)[N2]*/){
return A<T, N /*, T2, N2*/>(s /*, s2*/);
}
// size of B's members and struct's total size are driven by compile-time
// created strings
struct B{
// explicit (manual) error-prone template params
A<char, 4 /*, char, 3*/> xxx{c /*, c2*/};
// use of a dummy function feels very wrong
decltype(f(c /*, c2*/)) yyy{c /*, c2*/};
// also uses dummy function
decltype(f("abc" /*, "12"*/)) zzz{"abc" /*, "12"*/};
// would like to be able to use shorter strings
//A<char, 4 /*, char, 3*/> fail{"a" /*, "1"*/};
};
The 3 working examples of usage are either prone to mistakes or introduce too much code repetition. And the last one, that is not working, is a nice-to-have sort of thing along the lines of "I wonder if this is doable?" to me.
Is there anything I'm missing in the C++ standard that would let me avoid repetition while retaining this automation to avoid potential manual mistakes?
Ideally I'd've loved to just code like this snippet below:
struct B{
// automatic type deduction without some dummy function's help
A xxx{c /*, c2*/};
// strings in multiple TUs produce independent templates in current standard :(
A<"abc" /*, c2*/> xxx;
};
// usage
// constructs with { {c /*, c2*/}, {"abc" /*, c2*/} }
constinit B b;
// constructs with { {c /*, c2*/}, {"bc" /*, "0"*/} }
constinit B b2{{}, {"bc" /*, "0"*/}};
Without using standard library headers (excepting
<cstddef>
), here's a definition for the class templateA
that gets most of the desired features with as little boilerplate as possible, given that C++ disallows automatic argument deduction in non-static struct member declarations. This uses a c++17 user-defined deduction guide:Usage and tests below, with obligatory Compiler Explorer link:
To completely eliminate duplication in the declaration, you can create a derived class that behaves like
A{c1, c2}
. To make the linkage well-behaved, you'll need to canonicalize the template arguments of the derived class. Here's one way to do that:For your purposes,
to_c_t<c1, c2> value;
should behave almost likedecltype(A{c1, c2}) value{c1, c2};
, except there's no duplication, and it is safe to use across translation units since it is an alias for the canonical typeC<char, 'a', 'b', 'c', '1', '2'>
. Just for completeness, here's the full example using this approach.