通过 uint8_t 引用更新更宽整数的一部分
Updating part of a wider integer through a uint8_t reference
#include <cstdio>
#include <cstdint>
struct Registers {
Registers() : af(0),
f(*(reinterpret_cast<uint8_t *>(&af))),
a(*(reinterpret_cast<uint8_t *>(&af) + 1)) {
}
std::uint16_t af;
std::uint8_t& f;
std::uint8_t& a;
};
int main() {
Registers r;
r.af = 0x00FF;
r.a = 0xAA;
std::printf("AF: %04X A: %02X F: %02X\n", r.af, r.a, r.f);
return 0;
}
不管字节序问题,这是合法的 c++,还是它调用了某种类型的未定义行为?我认为这对于指针应该没问题并且不违反严格的别名,因为 uint8_t 是一个 char 类型,但我不确定这是否通过引用合法。
这似乎在打开大多数编译器标志的情况下工作正常,并且不会引发任何警告:
$ clang++ reg.cpp -O3 -fsanitize=undefined -fstrict-aliasing -Wall && ./a.out
AF: AAFF A: AA F: FF
正如您在问题中指出的那样,转换为“字节”类型这一事实几乎肯定会消除任何违反严格别名要求的问题。
然而,从严格的“语言律师”的角度来看,reinterpret_cast<uint8_t *>(&af) + 1
表达式 可能 调用未定义的行为——因为指针操作数是 不是数组元素的地址,数组元素是唯一类型,标准明确定义了这种指针算法。
来自 this Draft C++17 Standard(大胆强调我的):
8.5.6 Additive operators [expr.add]
…
4 When an expression that has integral type is added to or subtracted from a
pointer, the result has the type of the pointer operand. If the expression P points to element x[i] of an array object x with n elements
the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i + j] if 0 ≤ i + j ≤
n; otherwise, the behavior is undefined. Likewise, the expression P -
J points to the (possibly-hypothetical) element x[i − j] if 0 ≤ i − j
≤ n; otherwise, the behavior is undefined.
然而,将 uint16_t
变量视为(两个)uint8_t
元素的数组是否合法和明确定义可能存在争论空间。
Notes/Discussion
经过一些非常有帮助的评论后,我现在(甚至更加)确信,即使在指针加法表达式中可能 正式 未定义行为,也没有任何情况 (至少,在任何健全的平台上)原始问题中提供的代码将无法按预期工作。
首先,正如 Chris Dodd 所指出的,上述标准草案的 §6.7(第 2 段)是这样的:
2 For any object (other than a base-class
subobject) of trivially copyable type T, whether or not the object
holds a valid value of type T, the underlying bytes (6.6.1) making up
the object can be copied into an array of char
, unsigned char
, or
std::byte
. If the content of that array is copied back into the
object, the object shall subsequently hold its original value.
这证实了 uint16_t
数据 可以 被处理 – 至少,就其内存布局而言 – 作为 [=12= 的 2 元素数组]数据。
其次,Language Lawyer指出,非数组类型的对象可以认为是单个元素的数组;此外,允许 对此类对象的地址进行指针运算,以得出“一次性通过”假设元素的地址。来自 slightly later Draft Standard (§6.8.3) [basic.compund]:
3.4 For purposes of pointer arithmetic ([expr.add]) and comparison
([expr.rel], [expr.eq]), a pointer past the end of the last element of
an array x of n elements is considered to be equivalent to a pointer
to a hypothetical array element n of x and an object of type T that is
not an array element is considered to belong to an array with one
element of type T.
因此,结合以上两者,指针加法的结果所引用的假设的“一次通过最后”[=12=]元素将 是 uint16_t
数据对象的第二个字节。
#include <cstdio>
#include <cstdint>
struct Registers {
Registers() : af(0),
f(*(reinterpret_cast<uint8_t *>(&af))),
a(*(reinterpret_cast<uint8_t *>(&af) + 1)) {
}
std::uint16_t af;
std::uint8_t& f;
std::uint8_t& a;
};
int main() {
Registers r;
r.af = 0x00FF;
r.a = 0xAA;
std::printf("AF: %04X A: %02X F: %02X\n", r.af, r.a, r.f);
return 0;
}
不管字节序问题,这是合法的 c++,还是它调用了某种类型的未定义行为?我认为这对于指针应该没问题并且不违反严格的别名,因为 uint8_t 是一个 char 类型,但我不确定这是否通过引用合法。
这似乎在打开大多数编译器标志的情况下工作正常,并且不会引发任何警告:
$ clang++ reg.cpp -O3 -fsanitize=undefined -fstrict-aliasing -Wall && ./a.out
AF: AAFF A: AA F: FF
正如您在问题中指出的那样,转换为“字节”类型这一事实几乎肯定会消除任何违反严格别名要求的问题。
然而,从严格的“语言律师”的角度来看,reinterpret_cast<uint8_t *>(&af) + 1
表达式 可能 调用未定义的行为——因为指针操作数是 不是数组元素的地址,数组元素是唯一类型,标准明确定义了这种指针算法。
来自 this Draft C++17 Standard(大胆强调我的):
8.5.6 Additive operators [expr.add]
…
4 When an expression that has integral type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the expression P points to element x[i] of an array object x with n elements the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i + j] if 0 ≤ i + j ≤ n; otherwise, the behavior is undefined. Likewise, the expression P - J points to the (possibly-hypothetical) element x[i − j] if 0 ≤ i − j ≤ n; otherwise, the behavior is undefined.
然而,将 uint16_t
变量视为(两个)uint8_t
元素的数组是否合法和明确定义可能存在争论空间。
Notes/Discussion
经过一些非常有帮助的评论后,我现在(甚至更加)确信,即使在指针加法表达式中可能 正式 未定义行为,也没有任何情况 (至少,在任何健全的平台上)原始问题中提供的代码将无法按预期工作。
首先,正如 Chris Dodd 所指出的,上述标准草案的 §6.7(第 2 段)是这样的:
2 For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes (6.6.1) making up the object can be copied into an array of
char
,unsigned char
, orstd::byte
. If the content of that array is copied back into the object, the object shall subsequently hold its original value.
这证实了 uint16_t
数据 可以 被处理 – 至少,就其内存布局而言 – 作为 [=12= 的 2 元素数组]数据。
其次,Language Lawyer指出,非数组类型的对象可以认为是单个元素的数组;此外,允许 对此类对象的地址进行指针运算,以得出“一次性通过”假设元素的地址。来自 slightly later Draft Standard (§6.8.3) [basic.compund]:
3.4 For purposes of pointer arithmetic ([expr.add]) and comparison ([expr.rel], [expr.eq]), a pointer past the end of the last element of an array x of n elements is considered to be equivalent to a pointer to a hypothetical array element n of x and an object of type T that is not an array element is considered to belong to an array with one element of type T.
因此,结合以上两者,指针加法的结果所引用的假设的“一次通过最后”[=12=]元素将 是 uint16_t
数据对象的第二个字节。