将 std::u16string 转换为 std::wstring,无需复制

Convert std::u16string to std::wstring without copy

本文关键字:std 复制 u16string 转换 wstring      更新时间:2023-10-16

我用下面的接口做了一堆UTF转换函数:

template <typename T, typename U> std::basic_string<T> UTFConvert(std::basic_string_view<U> a_String);

charchar16_tchar32_t的所有组合的实现。但是现在我也需要添加对wchar_t的支持。我知道编译时wchar_t的大小,所以理论上我可以用相同大小的字符调用函数。

问题是我将不得不将结果字符串复制回std::wstring.例如,如果sizeof(wchar_t) == 2我最终会做这样的事情:

template <typename T, typename U>
std::enable_if_t<std::is_same_v<T, wchar_t>, std::basic_string<T>> UTFConvert(std::basic_string_view<U> a_String)
{
const std::u16string utf16 = UTFConvert<char16_t>(a_String);
std::wstring wstr;
wstr.resize(utf16.size());
memcpy(wstr.data(), utf16.data(), utf16.size() * sizeof(wchar_t));
return wstr;
}

像这样复制字符串似乎有点浪费。有没有办法避免这种情况,而无需为不同类型的重新实现相同的代码两次?

这个问题已经通过专门针对 T 的大小而不是特定的字符类型来解决:

template <typename T, typename U>
std::enable_if_t<sizeof(T) == 2, std::basic_string<T>> UTFConvert(std::basic_string_view<U> a_String);

只需在那里撒上几static_assert以保持理智,一切都很完美!

谢谢@MassimilianoJanes的建议。

你处于C++的一个角落,标准有一些粗糙的地方。

这里有一些理论上的陷阱。

首先,在C++char16_t缓冲区上取消引用wchar_t*是不合法的,反之亦然。 此问题称为"严格别名"。

如果您有固定大小的缓冲区,则可以通过仔细来回复制和构造在有限的范围内解决此问题。 但是,C++ 标准中存在一个缺陷,使得创建动态大小的数组无法"手动"完成,而无需对所讨论的类型调用new[](根据用户代码的标准,不可能实现std::vector或类似)。

这是标准中的一个缺陷,但据我所知,目前尚未解决。

所以问题就变成了,你想在多大程度上遵循标准,你有多想要一些有效的东西?

我能得到的最接近符合标准的代码来解决您的问题的是:


编写一个新的UTFConvert_to_sink函数。

template<class T>
struct tag_t {};
template<class CharType, class Sink>
void UTFConvert_to_sink(std::basic_string_view<CharType> from, tag_t<CharType> to, Sink&& sink) {
for (CharType c : from)
sink(c);
}
template<class Sink>
void UTFConvert_to_sink(std::basic_string_view<char> from, tag_t<std::char16_t> to, Sink&& sink); // TODO
template<class Sink>
void UTFConvert_to_sink(std::basic_string_view<char> from, tag_t<std::char32_t> to, Sink&& sink); // TODO

template<class Sink>
void UTFConvert_to_sink(std::basic_string_view<char16_t> from, tag_t<char> to, Sink&& sink); // TODO
template<class Sink>
void UTFConvert_to_sink(std::basic_string_view<char16_t> from, tag_t<std::char32_t> to, Sink&& sink); // TODO
template<class Sink>
void UTFConvert_to_sink(std::basic_string_view<char32_t> from, tag_t<char> to, Sink&& sink); // TODO
template<class Sink>
void UTFConvert_to_sink(std::basic_string_view<char32_t> from, tag_t<std::char16_t> to, Sink&& sink); // TODO

请注意,这些仅在Sink上模板化。Sink的工作原理应该从我的模板"相同到相同"中清楚。

UTFConvert可以写在上面,如下所示:

template<class To, class From>
std::basic_string<To> UTFConvert( std::basic_string_view<From> from ) {
std::basic_string<To> retval;
UTFConvert_to_sink( from, tag_t<To>{}, [&retval]( To c ) {retval.push_back(c);} );
}

它处理有问题的每种类型。

现在剩下的就是wchar_tUTFConvert_to_sink.

using char_type_same_size_as_wchar_t = std::char16_t; // or char32_t depending on platform.
template<class From, class Sink>
void UTFConvert_to_sink(std::basic_string_view<From> from, tag_t<wchar_t> to, Sink&& sink) {
UTFConvert_to_sink( from, tag_t<char_type_same_size_as_wchar_t>{}, [&sink](auto c) {
wchar_t wc = c;
sink( wc );
});
}

我认为一切都在标准方面是可以的。 wchar_t函数应该编译为几乎一无所有。

如果您想从 -wchar_t支持,事情确实会变得混乱,因为标准缺陷是无法在不调用new T[]的情况下创建数组。 我们可以靠近我们清洗每个元素的地方。

template<class U, class T>
U* landry_pod( T* in ) {
static_assert( sizeof(T)==sizeof(U) );
static_assert( std::is_trivially_copyable<T>{} && std::is_trivially_copyable<U>{} );
char buff[sizeof(T)];
std::memcpy( buff, in, sizeof(T) );
U* r = ::new( (void*)in ) U;
std::memcpy( r, buff, sizeof(U) );
return r;
}

landry_pod<OutType>是一个有趣的函数,因为它编译为零指令(试试看),但它是一种合法的方法,可以将指针转到 T 类型的可平凡可复制对象,并获取指向包含完全相同字节的相同大小 U 的平凡可复制对象的指针。

所以我能得到的最接近的是 运行basic_string_view<wchar_t>,依次laundry_pod每个元素,然后获取指针并用它们创建一个basic_string_view<char16_t>,然后将它们提供给UTFConvert_to_sink

现在,所有这些都是荒谬的体操,围绕着标准中严格的混叠规则,它甚至不足以实际生成完全定义的行为。

请注意,我写了一个接受单个字符的sink;也可以编写一个更高级的字符(即,需要与字符分开的长度,和/或允许您按顺序输入)。