英文:
string literals as template arguments forces code duplication and verbosity
问题
以下是翻译好的内容:
编译时创建的字符串被用作结构体中的字符数组,并且其最大大小由该字符串驱动。此外,该结构体被用作另一个结构体的成员变量,并且其中有多个类似的字符串缓冲结构体,从而驱动其存储大小。
在模板参数中使用字符串是可行的,但会引入多个代码重复,像野火一样在源代码中蔓延。当第一个结构体有多个字符串作为模板参数(2个甚至3个)时,情况会变得更糟。
以下是一个可工作的示例:
#include <cstddef>
static constinit decltype(auto) c = "abc";
//static constinit decltype(auto) c2 = "12";
// A的大小由编译时创建的字符串驱动
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) // 对于比存储更短的字符串
break;
}
// for(int i = 0; i < N2; ++i){
// str2[i] = s2[i];
// if(s2[i] == 0)
// break;
// }
}
};
// 唯一目的是帮助类型推断的虚拟函数
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*/);
}
// B的成员和结构体的总大小由编译时创建的字符串驱动
struct B{
// 显式(手动)容易出错的模板参数
A<char, 4 /*, char, 3*/> xxx{c /*, c2*/};
// 使用虚拟函数感觉很不对
decltype(f(c /*, c2*/)) yyy{c /*, c2*/};
// 也使用虚拟函数
decltype(f("abc" /*, "12"*/)) zzz{"abc" /*, "12"*/};
// 希望能够使用较短的字符串
//A<char, 4 /*, char, 3*/> fail{"a" /*, "1"*/};
};
这三个可工作的用法示例要么容易出错,要么引入了太多的代码重复。而最后一个不起作用的示例,对我来说更像是“我想知道这是否可行?”的一种愿望。
在C++标准中是否有我可以避免重复而仍然保留此自动化以避免潜在手动错误的内容?
理想情况下,我希望能够像下面的代码片段一样编写代码:
struct B{
// 自动类型推断,无需虚拟函数的帮助
A xxx{c /*, c2*/};
// 在多个翻译单元中使用字符串会产生独立的模板:(
A<"abc" /*, c2*/> xxx;
};
// 用法
// 使用{ {c /*, c2*/}, {"abc" /*, c2*/} }构造
constinit B b;
// 使用{ {c /*, c2*/}, {"bc" /*, "0"*/} }构造
constinit B b2{{}, {"bc" /*, "0"*/}};
英文:
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"*/}};
答案1
得分: 1
以下是翻译好的部分:
#include <cstddef>;
template <typename T, std::size_t N>
struct A {
// + 1 to account for null termination in storage
T str[N + 1];
template <std::size_t... Ns>
// to prevent aggregate construction from compiling
// if sum of string literal sizes exceed storage capacity
requires(N + 1 > (0 + ... + (Ns - 1)))
consteval A(const T (&...s)[Ns]) {
auto it = str;
(..., [&](const auto &r) {
for (const auto c : r) {
if (!c) break;
*it++ = c;
}
}(s));
*it = T{};
}
};
// user-defined deduction guide
template <typename T, std::size_t... Ns>
// - 1 to exclude null termination from each string literal in the pack
A(const T (&s)[Ns]) -> A<T, (0 + ... + (Ns - 1))>;
// 使用和测试如下,带有强制要求的编译器链接:
static constinit decltype(auto) c1 = "abc";
static constinit decltype(auto) c2 = "12";
struct B {
// 无需虚拟函数
// 但需要在类型表达式中重复编译时常量
decltype(A{c1, c2}) value{c1, c2};
};
#include <concepts>;
// 从编译时常量推断出正确的宽度
static_assert(std::same_as<decltype(B{}.value), A<char, 5>>);
#include <string_view>;
using namespace std::string_view_literals;
// 默认构造
static_assert(B{}.value.str == "abc12"sv);
// 聚合构造
static_assert(B{{}}.value.str == ""sv);
static_assert(B{{c1}}.value.str == "abc"sv);
static_assert(B{{c2}}.value.str == "12"sv);
static_assert(B{{c2, c1}}.value.str == "12abc"sv);
static_assert(B{{"a", "1"}}.value.str == "a1"sv);
// 候选模板被忽略:不满足约束条件 [with Ns = <3, 5>]
// 因为 '5UL + 1 > 0 + (3UL - 1) + (5UL - 1)' (6 > 6) 评估为 false
// static_assert(B{{"ab", "1234"}}.value.str == "ab1234"sv);
// 完全消除声明中的重复,可以创建一个行为类似于 `A{c1, c2}` 的派生类。
// 为了使链接行为良好,您需要规范化派生类的模板参数。
// 这里是一种方法:
template <typename T, T... Cs>
struct C : decltype(A{{Cs..., T{}}}) {
using base = decltype(A{{Cs..., T{}}});
using base::base;
constexpr C() : base{{Cs..., T{}}} {}
};
#include <utility>;
template <A, typename...>
struct to_c;
template <typename T, std::size_t N, A<T, N> a>
struct to_c<a> : to_c<a, std::make_index_sequence<N>> {};
template <typename T, std::size_t N, A<T, N> a, std::size_t... Is>
struct to_c<a, std::index_sequence<Is...>> {
using type = C<T, a.str[Is]...>;
};
template <A... a>
using to_c_t = typename to_c<A{a.str...}>::type;
static constinit decltype(auto) c1 = "abc";
static constinit decltype(auto) c2 = "12";
#include <concepts>;
static_assert(std::same_as<to_c_t<c1, c2>, C<char, 'a', 'b', 'c', '1', '2'>>);
希望这对您有所帮助。如有其他问题,请随时提出。
英文:
Without using standard library headers (excepting <cstddef>
), here's a definition for the class template A
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 [tag:C++17] user-defined deduction guide:
#include <cstddef>
template <typename T, std::size_t N>
struct A {
// + 1 to account for null termination in storage
T str[N + 1];
template <std::size_t... Ns>
// to prevent aggregate construction from compiling
// if sum of string literal sizes exceed storage capacity
requires(N + 1 > (0 + ... + (Ns - 1)))
consteval A(const T (&...s)[Ns]) {
auto it = str;
(..., [&](const auto &r) {
for (const auto c : r) {
if (!c) break;
*it++ = c;
}
}(s));
*it = T{};
}
};
// user-defined deduction guide
template <typename T, std::size_t... Ns>
// - 1 to exclude null termination from each string literal in the pack
A(const T (&...s)[Ns]) -> A<T, (0 + ... + (Ns - 1))>;
Usage and tests below, with obligatory Compiler Explorer link:
static constinit decltype(auto) c1 = "abc";
static constinit decltype(auto) c2 = "12";
struct B {
// no dummy function
// but requires repetition of the compile-time constants
// in the type expression
decltype(A{c1, c2}) value{c1, c2};
};
#include <concepts>
// deduces correct width from compile-time constants
static_assert(std::same_as<decltype(B{}.value), A<char, 5>>);
#include <string_view>
using namespace std::string_view_literals;
// default construction
static_assert(B{}.value.str == "abc12"sv);
// aggregate construction
static_assert(B{{}}.value.str == ""sv);
static_assert(B{{c1}}.value.str == "abc"sv);
static_assert(B{{c2}}.value.str == "12"sv);
static_assert(B{{c2, c1}}.value.str == "12abc"sv);
static_assert(B{{"a", "1"}}.value.str == "a1"sv);
// candidate template ignored: constraints not satisfied [with Ns = <3, 5>]
// because '5UL + 1 > 0 + (3UL - 1) + (5UL - 1)' (6 > 6) evaluated to false
// static_assert(B{{"ab", "1234"}}.value.str == "ab1234"sv);
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:
template <typename T, T... Cs>
struct C : decltype(A{{Cs..., T{}}}) {
using base = decltype(A{{Cs..., T{}}});
using base::base;
constexpr C() : base{{Cs..., T{}}} {}
};
#include <utility>
template <A, typename...>
struct to_c;
template <typename T, std::size_t N, A<T, N> a>
struct to_c<a> : to_c<a, std::make_index_sequence<N>> {};
template <typename T, std::size_t N, A<T, N> a, std::size_t... Is>
struct to_c<a, std::index_sequence<Is...>> {
using type = C<T, a.str[Is]...>;
};
template <A... a>
using to_c_t = typename to_c<A{a.str...}>::type;
static constinit decltype(auto) c1 = "abc";
static constinit decltype(auto) c2 = "12";
#include <concepts>
static_assert(std::same_as<to_c_t<c1, c2>, C<char, 'a', 'b', 'c', '1', '2'>>);
For your purposes, to_c_t<c1, c2> value;
should behave almost like decltype(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 type C<char, 'a', 'b', 'c', '1', '2'>
. Just for completeness, here's the full example using this approach.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论