如何在不违反严格别名规则的情况下将`u8string_view`转换为`char`数组?

8

前提

  • 我有一个二进制数据块在内存中,以char*表示(可能从文件中读取或通过网络传输)。
  • 我知道它包含某个偏移量处特定长度的UTF8编码文本字段。

问题

如何(安全和便携地)获取u8string_view来表示此文本字段的内容?

动机

将该字段作为u8string_view传递给下游代码的动机是:

  • 它非常清楚地表明了文本字段是UTF8编码的,不像string_view
  • 它避免了将其作为u8string返回的成本(可能是自由存储器分配+复制)。

我尝试过的方法

实现此目的的简单方法是:

char* data = ...;
size_t field_offset = ...;
size_t field_length = ...;

char8_t* field_ptr = reinterpret_cast<char8_t*>(data + field_offset);
u8string_view field(field_ptr, field_length);

然而,如果我正确理解C++的严格别名规则,这是未定义行为,因为它通过由reinterpret_cast返回的char8_t*指针访问char*缓冲区的内容,而char8_t不是一种别名类型。

这是真的吗?

有没有安全的方法可以做到这一点?


据我所知,char 在这里是特殊的。gcc/clang 是否发出警告? - Bernd
1
@Bernd,“char”是特殊的,但我不认为它适用于这里。据我所知,一个“char”可以别名任何东西,但一个“char8_t”不能别名一个字符。 - Guillaume Racicot
1
在C++23中,我们可能会有std::start_lifetime_as,但我不确定除了承认你正在UB(未定义行为)的情况下是否有任何帮助解决这个问题的东西在C++20中。 - Guillaume Racicot
看一下隐式对象创建,它可能会使你的程序定义良好。 - geza
1
如果整个 blob 是 UTF-8 数据,为什么不一开始就将其作为 char8_t 的一堆呢?无论如何,我不会太担心这个问题。真正的软件会对从网络接收或从文件读取的数据进行 reinterpret_cast。这是非常普遍的做法,而标准却没有承认它,这是有缺陷的。 - n. m.
显示剩余2条评论
2个回答

1

当你使用一个没有可接受类型的glvalue访问一个对象时,就会发生严格别名规则。

首先考虑一个定义良好的情况:

char* data = reinterpret_cast <char *> (new char8_t[10]{})
size_t field_offset = 0;
size_t field_length = 10;
char8_t* field_ptr = reinterpret_cast<char8_t*>(data + field_offset);
u8string_view field(field_ptr, field_length);
field [0]+field[1];

这里没有未定义行为。您创建了一个char8_t数组,然后访问数组的元素。

现在,如果由另一个程序创建了data引用的内存对象会发生什么?根据标准,这是未定义行为,因为该对象不是通过指定的方式之一来创建的

但是,您的代码尚未得到标准支持并不是问题。所有编译器都支持此代码。如果不支持,那么什么也做不了,甚至不能执行最简单的系统调用,因为程序与任何内核之间的大多数通信都是通过字符数组进行的。因此,只要在程序内部通过类型为char8_t的glvalue访问位于data+field_offsetdata+field_offset+field_length之间的内存,您的代码将按预期工作。


“什么都不会工作” - 好吧,你可以使用 memcpy 将 C++ 对象安全地序列化/反序列化到/从 char 数组。如果拷贝不是问题的话,这通常是怎么做的。 - smls
1
"这段代码被所有编译器支持。"-- 我在哪里可以找到更多关于哪种技术上不安全的严格别名代码实际上是安全的信息?我已经阅读了开源库中与严格别名相关的错误,因此我认为严格别名并非完全没有问题。如果是这样,GCC也不会有-fno-strict-aliasing开关。 - smls
@smls 关于您的第一条评论,“memcpy”不会改变您的问题,因为必须根据规范创建原始对象。有了C++20,通过使用“bit_cast”,可以实现标准兼容性。 - Oliv
@smls:几乎所有非诊断编译器在禁用优化时都支持这样的结构。然而,clang和gcc优化器的维护者已经声明,他们不感到有义务使未来的编译器在所有现有编译器都能做到但标准不要求的情况下表现得有用。 - supercat
@supercat,希望标准能够使这样的代码符合标准!但是现在所有编译器都支持它,并且支持所有优化,甚至包括LTO。 - Oliv
显示剩余2条评论

1
这个问题有时也会在其他情况下出现,比如使用共享内存。
在“原始”内存中使用位来创建对象而不分配内存的技巧是通过memcpy创建一个本地对象,然后在“原始”内存上创建该本地对象的动态副本。例如:
char* begin_raw = data + field_offset;
char8_t* last {};
for(std::ptrdiff_t i = 0; i < field_length; i++) {
    char* current = begin_raw + i;
    char8_t local {};
    std::memcpy(&local, current, sizeof local);
    last = new (current) char8_t(local);
}
char8_t* begin = last - (field_length - 1);
std::u8string_view field(begin, field_length);

在你反对复制之前,请注意最终结果不会对“原始”内存的表示造成任何更改。编译器也可以注意到这一点,并将整个循环编译为零条指令(在我的测试中,GCC和Clang使用-O2实现了这一点)。我们所做的只是通过在内存中创建动态对象来满足语言的对象生命周期规则。


仍然存在由指针算术引起的未定义行为。可以尝试使用char8_t数组来修复此问题。但是我不知道如何在没有编译器扩展语言(动态大小的carray)或调用alloca的情况下完成此操作。 - Oliv
目前我无法通过指针算术运算看到一些未定义行为。你能解释一下吗? - Bernd
1
@Bernd 指针算术仅允许在指向数组元素的指针上执行(对于指针算术规则,将单个对象视为大小为一的数组)。这里没有数组,因此 last - (field_length - 1) 是未定义行为。 - Oliv
@Bernd,这是标准过于严格的措辞,仅将指针算术定义在数组内。在示例中没有创建任何数组对象。出于同样的原因,任何编写自定义向量的尝试都是不可能的(技术上有数组放置新方法,但由于其他原因实际上无法使用)。 - eerorika

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接