【问题标题】:`u8string_view` into a `char` array without violating strict-aliasing?`u8string_view` 到 `char` 数组中而不违反严格混叠?
【发布时间】:2020-08-11 18:40:24
【问题描述】:

前提

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

问题

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

动机

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

  • string_view 不同,它非常清楚地表明文本字段是 UTF8 编码的。
  • 它避免了将其返回为 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 char 很特别,但我认为它不适用于这里。据我所知,char* 可以为任何东西加上别名,但 char8_t* 不能为字符加上别名。
  • 在 C++23 中,我们可能有 std::start_lifetime_as,但我不确定在 C++20 中是否有任何帮助,除了承认你正在努力实现这一目标。
  • 看看隐式对象创建,它可能会让你的程序定义明确。
  • 如果整个 blob 是 UTF-8 数据,为什么不首先将它作为一堆 char8_t 呢?反正我也不会太担心。真实软件reinterpret_casts 从网络接收数据或从文件中读取。这是非常普遍的做法,标准是有缺陷的,因为不承认它。

标签: c++ undefined-behavior c++20 strict-aliasing


【解决方案1】:

当您访问具有没有acceptable type 的泛左值的对象时,会发生严格的别名规则。

首先考虑一个定义明确的案例:

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];

这里没有UB。您创建一个char8_t 数组,然后访问该数组的元素。

现在,如果data 引用的内存对象是由另一个程序创建的,会发生什么?根据标准,这是 UB,因为该对象不是由 specified way to create it 之一创建的。

但是,标准尚未支持您的代码这一事实在这里不是问题。所有编译器都支持此代码。如果不是,那么什么都行不通,你甚至无法进行最简单的系统调用,因为程序和任何内核之间的大部分通信都是通过 char 数组进行的。因此,只要在您的程序内部,您通过 char8_t 类型的 glvalue 访问位于 data+field_offsetdata+field_offset+field_length 之间的内存,您的代码就会按预期工作。

【讨论】:

  • "nothing would work" -- 好吧,您可以使用memcpy 安全地将 C++ 对象序列化/反序列化到/从 char 数组中。这不是通常的做法吗(如果不认为复制有问题)?
  • "所有编译器都支持此代码。" -- 我在哪里可以找到更多关于哪种技术上严格混叠不安全的代码在实践中实际上是安全的信息?我已经阅读了开源库中与严格别名相关的错误,所以我不认为严格别名完全不是问题。如果是的话,GCC 也不会有 -fno-strict-aliasing 开关。
  • @smls 没有严格的别名规则违规,只是访问了一个不是由正在运行的程序创建的对象。
  • @smls 关于您的第一条评论memcpy 不会改变您的问题,因为必须有一个根据规范创建的原始对象。借助bit_cast,使用 C++20 可以符合标准。
  • @smls:当禁用优化时,几乎所有(如果不是所有)非诊断编译器都将支持此类构造。然而,clang 和 gcc 优化器的维护者表示,他们认为没有义务让未来的编译器在所有现有编译器都这样做但标准不要求这样做的情况下发挥作用。
【解决方案2】:

同样的问题偶尔也会出现在其他情况下,例如共享内存的使用。

使用“原始”内存中的位创建对象而不分配内存的技巧是通过 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 实现了这一点)。我们所做的只是通过在内存中创建动态对象来满足语言的对象生命周期规则。

【讨论】:

  • 还有指针运算导致的UB。可以尝试通过使用 char8_t 数组来解决此问题。但我看不出如果没有对语言的编译器扩展(动态大小的数组)或对 alloca 的调用,它是如何完成的。
  • 目前我看不到一些带有指针算法的 UB。你能解释一下吗?
  • @Bernd 指针运算只允许在指向数组元素的指针上使用(单个对象被认为是指针运算规则的大小为 1 的数组)。这里没有数组,所以last - (field_length - 1) 是UB。
  • @Bernd 这是标准的过于严格的措辞,它仅在数组中定义指针运算。示例中未创建数组对象。出于同样的原因,任何编写自定义向量的尝试都是不可能的(从技术上讲,数组放置是新的,但由于其他原因实际上无法使用)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-04
  • 2020-01-05
  • 2018-05-23
相关资源
最近更新 更多