根据this stackoverflow关于C ++ 11/14严格别名规则的回答:
如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 一个cv限定版本的动态类型的对象,
- 与对象的动态类型类似的类型(如4.4中所定义),
- 与对象的动态类型对应的有符号或无符号类型的类型,
- 一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,
- 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),
- 一个类型,它是对象动态类型的(可能是cv限定的)基类类型,
- 一个
char
或unsigned char
类型。
我们可以使用其他类型的存储
(1)char *
(2)char(&)[N]
(3)std::array<char, N> &
不依赖于未定义的行为?
constexpr uint64_t lil_endian = 0x65'6e'64'69'61'6e;
// a.k.a. Clockwise-Rotated Endian which allocates like
// char[8] = { n,a,i,d,n,e,\0,\0 }
const auto& arr = // std::array<char,8> &
reinterpret_cast<const std::array<char,8> &> (lil_endian);
const auto& carr = // char(&)[8]>
reinterpret_cast<const char(&)[8]> (lil_endian);
const auto* p = // char *
reinterpret_cast<const char *>(std::addressof(lil_endian));
int main()
{
const auto str1 = std::string(arr.crbegin()+2, arr.crend() );
const auto str2 = std::string(std::crbegin(carr)+2, std::crend(carr) );
const auto sv3r = std::string_view(p, 8);
const auto str3 = std::string(sv3r.crbegin()+2, sv3r.crend() );
auto lam = [](const auto& str) {
std::cout << str << '\n'
<< str.size() << '\n' << '\n' << std::hex;
for (const auto ch : str) {
std::cout << ch << " : " << static_cast<uint32_t>(ch) << '\n';
}
std::cout << '\n' << '\n' << std::dec;
};
lam(str1);
lam(str2);
lam(str3);
}
所有lambda调用产生:
endian
6
e : 65
n : 6e
d : 64
i : 69
a : 61
n : 6e
godbolt.org/g/cdDTAM(启用-fstrict-aliasing -Wstrict-aliasing = 2)
char(&)[N]
案件和std::array<char, N>
案件都导致未定义的行为。原因已经被你引用了。请注意,char(&)[N]
和std::array<char, N>
都不是char
的类型。
我不确定char
的情况,因为当前标准没有明确说明一个对象可以被视为一个狭窄的字符数组(请参阅here进一步讨论)。
无论如何,如果你想访问对象的底层字节,请使用std::memcpy
,正如标准在[basic.types]/2中明确指出的那样:
对于平凡可复制类型T的任何对象(基类子对象除外),无论对象是否保持类型T的有效值,组成对象的基础字节([intro.memory])都可以复制到char,unsigned char或
std::byte
([cstddef.syn])数组。如果将该数组的内容复制回对象,则该对象应随后保持其原始值。 [实施例:#define N sizeof(T) char buf[N]; T obj; // obj initialized to its original value std::memcpy(buf, &obj, N); // between these two calls to std::memcpy, obj might be modified std::memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type holds its original value
- 结束例子]
严格的别名规则实际上非常简单:如果一个不是另一个的子对象,那么具有重叠生命周期的两个对象不能具有重叠的存储区域。(*)
然而,允许读取对象的内存表示。对象的内存表示是unsigned char
[basic.types] / 4的序列:
类型T的对象的对象表示是由类型T的对象占据的N个
unsigned char
对象的序列,其中N等于sizeof(T)
。对象的值表示是保存类型T的值的位集。
因此在你的例子中:
lam(str1)
是UB(Undefined Behavior);lam(str2)
在标准中没有被称为UB,如果你用pointer interconvertible替换lam(str3)
,你可能会认为你正在阅读对象表示。 (它也没有定义,但它应该适用于所有编译器)因此,使用第三种情况并将char
的声明更改为unsigned char
应始终产生预期结果。对于其他两种情况,它可以使用这个简单的示例,但如果代码更复杂或者在更新的编译器版本上可能会中断。
(*)此规则有两个例外:一个用于具有共同初始化序列的工会成员;和一个p
或const unsigned char*
阵列,为其他物体提供存储。