我试图在编译时连接类似字符串的对象。在这篇文章的帮助下,我想出了这样的东西:
#include <cstddef>
#include <algorithm>
#include <array>
#include <cstring>
#include <string>
#include <string_view>
template <std::size_t N>
class ConstexprChars
{
private:
std::array<char, N> _string;
template <std::size_t S>
friend class ConstexprChars;
template <std::size_t S1, std::size_t S2>
constexpr ConstexprChars(const std::array<char, S1> s1,
const std::array<char, S2> s2)
: _string() {
std::copy(s1.begin(), s1.end() - 1, _string.begin());
std::copy(s2.begin(), s2.end() - 1, _string.begin() + S1 - 1);
}
public:
constexpr ConstexprChars(const char (&str)[N]) : _string() {
std::copy(&str[0], &str[0] + N, _string.begin());
}
constexpr ConstexprChars(const std::array<char, N> str) : _string() {
std::copy(&str[0], &str[0] + N, _string.begin());
}
constexpr ConstexprChars(std::string_view str) : _string() {
std::copy(str.data(), str.data() + N, _string.begin());
}
template <std::size_t S>
constexpr auto operator+(const ConstexprChars<S> other) const {
return ConstexprChars<N + other._string.size() - 1>(_string, other._string);
}
[[nodiscard]]
constexpr auto c_str() const {
return _string.data();
}
};
template <std::size_t N, std::size_t... Ns>
constexpr auto cconcat_impl(const char (&first)[N], const char (&... rest)[Ns]) {
if constexpr (!sizeof...(Ns)) { return ConstexprChars<N>{first}; }
else { return ConstexprChars<N>{first} + cconcat_impl(rest...); }
}
template <std::size_t... Ns>
constexpr auto cconcat(const char (&... strings)[Ns]) {
return cconcat_impl(strings...);
}
#include <iostream>
int main() {
// this works;
constexpr const char name[] = "Edward";
constexpr auto joined1 = cconcat("name=", name);
std::cout << joined1.c_str() << std::endl;
// this does not work:
// constexpr std::string_view value = "42"; // essentially same as constexpr const char*
// constexpr auto joined2 = cconcat("value=", value);
// std::cout << joined2.c_str() << std::endl;
return 0;
}
但是,这仅适用于字符串文字和字符数组。有没有办法扩展其他编译时类似字符串对象的功能?
编辑: 正如@Oersted所建议的,实现这一目标的一种方法是添加这些重载和宏方案(为了简单起见,忽略了混合搭配的可能性):
constexpr std::size_t cptr_size(const char* cptr) {
return std::string_view{cptr}.length() + 1;
}
template <std::size_t N>
constexpr auto make_array(const char* cptr) {
std::array<char, N> arr;
std::copy_n(cptr, N, arr.data());
return arr;
}
#define MAKEARRAY(str) make_array<cptr_size(str)>(str)
template <std::size_t N, std::size_t... Ns>
constexpr auto
cconcat_impl(const std::array<char, N> first, const std::array<char, Ns>... rest) {
if constexpr (!sizeof...(Ns)) { return ConstexprChars<N>{first}; }
else { return ConstexprChars<N>{first} + cconcat_impl(rest...); }
}
template <std::size_t... Ns>
constexpr auto cconcat(const std::array<char, Ns>... strings) {
return cconcat_impl(strings...);
}
那么,我们可以这样做:
int main() {
constexpr const char* value = "42";
constexpr auto joined2 = cconcat(MAKEARRAY("value="), MAKEARRAY(value));
std::cout << joined2.c_str() << std::endl;
return 0;
}
但是,这改变了 API。是否可以在保留相同 API 的同时实现这一目标?也就是说,是否可以有这个:
int main() {
constexpr const char* value = "42";
constexpr auto joined2 = cconcat("value=", value);
std::cout << joined2.c_str() << std::endl;
return 0;
}
您需要将串联结果存储在编译时大小的数组中(例如
std::array
)。但是 std::string_view::size
是其值的一部分(而不是其类型),并且不能用于确定串联函数的结果类型。最直接的方法是定义一个 fixed_string
类模板并将大小放入其类型元数据中。类模板可以将转换运算符声明为 std::array
、std::string_view
和 std::string
。对于运行时串联,您需要在串联序列中提供至少一个 std::string
类的实例。如果使用 operator+
来执行串联,语法将保持一致:
template<std::array val>
struct fixed_string{
using this_arr_t = std::remove_cv_t<decltype(val)>;
using value_type = this_arr_t::value_type;
using string_view = std::basic_string_view<value_type>;
consteval static auto size()
{return size(val)-!back(val);};
consteval operator std::basic_string_view<value_type>()
const{return string_view{data(val), this->size()};};
constexpr operator std::basic_string<value_type>()
const{return std::basic_string{
string_view{*this}};};
consteval static auto begin()
{return std::begin(string_view{fixed_string{}});};
consteval static auto end()
{return std::end(string_view{fixed_string{}});};
template<std::size_t N>
consteval static auto arrcat(array<value_type, N> rhs)
->std::array<value_type, size() + N> {
std::array<value_type, size() + N> res;
std::ranges::copy(rhs,
std::ranges::copy(fixed_string{}, begin(res)).second);
return res;
};
template<std::array rhs>
consteval fixed_string<arrcat(rhs)>
operator+(fixed_string<rhs>)
{return {};};
private:
// I would like the class empty
};
template<std::array val>
consteval fixed_string<val> operator""_fxstr()
{return {};};
auto str = "this"_fxstr + " is a "_fxstr + "std::string"s;
为了清楚起见,我留下了很多实现细节。例如:我会从末尾删除空终止符,但人们可能想保留它。 但是 ` 提供了一种更简单的方法来零努力地实现这一目标:
auto str = std::array{ "this"sv
, "is"sv
, "a"sv
, "std::string"s }
| std::views::join("_")
| std::ranges::to<std::string>();
如果输入是,则
join
的结果可以是constexpr
。但在被收集为 string
或 vector
与 ranges::to
之前,它不会是连续的范围。
最后一点 - 正如评论中已经提到的 - 是避免定义与 Qt 等著名库中的名称相同的标识符:
QObject
是与您的预期用途无关的名称,并且是抽象基类名称Qt。
总结我的评论,我实现了这个片段。是否满足您的需求:
#include <algorithm>
#include <array>
#include <cstddef>
#include <iostream>
#include <string_view>
namespace Details {
template <size_t N>
struct StringWrapper {
std::array<char, N> value;
std::size_t size;
constexpr explicit(false) StringWrapper(const char (&str)[N]) : size(N) {
std::copy_n(static_cast<const char *>(str), N, value.data());
}
};
} // namespace Details
template <Details::StringWrapper head, Details::StringWrapper tail>
class SConcat {
constexpr static std::array<char, head.size + tail.size - 1> init() {
std::array<char, head.size + tail.size - 1> conc;
std::copy(std::cbegin(head.value), std::cend(head.value) - 1,
std::begin(conc));
std::copy(std::cbegin(tail.value), std::cend(tail.value),
std::begin(conc) + head.size - 1);
return conc;
}
static constexpr std::array<char, head.size + tail.size - 1> data = init();
public:
static constexpr std::string_view as_sv() { return {data.data()}; }
};
int main() {
constexpr char value[] = "42";
const std::string_view conc{SConcat<"value=", value>::as_sv()};
std::cout << conc << '\n';
return static_cast<int>(conc[1] == 'a');
}
我认为它可以得到显着的完善,但它给出了主要思想。
First
StringWrapper
只是一种将原始字符串隐式转换为可用作 SConcat
的 CNTTP 对象的方法(因此需要 C++20)。
因此,
SConcat
为其声明中使用的每对字符串提供了唯一的类型,并且串联结果保存在提供存储的静态成员变量data
中。
通过
string_view
方法最终可以得到as_sv()
。
(NB1,通过删除
std::cout
部分,您可以看到编译器如何优化所有内容,然后是程序集:
main:
mov eax, 1
ret
NB2
static_cast
只是为了消除一些误报警告。
)
编辑:支持各种字符串文字声明的替代方案,按照 op 的要求,有点详细。
#include <algorithm>
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
// #define PROVEOPTIM
#ifndef PROVEOPTIM
#include <iostream>
#endif
namespace Details {
constexpr std::size_t MySize(const char *str) {
std::string tmp{str};
return tmp.length();
}
template <std::size_t N>
constexpr auto MakeArray(const char *str) {
std::array<char, N + 1> arr;
std::copy_n(str, N + 1, arr.data());
return arr;
}
} // namespace Details
#define MAKEARRAY(str) Details::MakeArray<Details::MySize(str)>(str)
template <std::array head, std::array tail>
class SConcat {
constexpr static std::array<char, head.size() + tail.size() - 1> init() {
std::array<char, head.size() + tail.size() - 1> conc;
std::copy(std::cbegin(head), std::cend(head) - 1, std::begin(conc));
std::copy(std::cbegin(tail), std::cend(tail),
std::begin(conc) + head.size() - 1);
return conc;
}
static constexpr std::array<char, head.size() + tail.size() - 1> data =
init();
public:
static constexpr std::string_view as_sv() { return {data.data()}; }
};
int main() {
constexpr const char *value = "42";
constexpr const char value2[] = "3.14";
const std::string_view conc{
SConcat<MAKEARRAY("value="), MAKEARRAY(value)>::as_sv()};
const std::string_view conc2{
SConcat<MAKEARRAY("value="), MAKEARRAY(value2)>::as_sv()};
#ifndef PROVEOPTIM
std::cout << conc << '\n';
std::cout << conc2 << '\n';
#endif
return static_cast<int>(conc[1] == 'a');
}
它依赖于从指向作为
std::array
或 const char*
传递的空终止字符串的指针显式构建 const char[]
。宏 MAKEARRAY
将两个操作合并为一个:获取大小作为编译时常量,并从用于获取大小的同一源复制数据。
显然,如果“字符串”不是以 null 结尾,则结果是未定义的。