![](/img/trans.png)
[英]Can std::uintptr_t be used to avoid undefined behavior of out-of-bounds pointer arithmetic?
[英]Pointer arithmetic on uintptr_t
我必须实施“带通”滤波器。 令a
和b
表示产生半开区间[a, b)
的两个整数。 如果某个参数x
位于此区间内(即a <= x < b
),我返回一个指向 C 字符串const char* high
的指针,否则我返回一个指针const char* low
。 这个 function 的香草实现看起来像
const char* vanilla_bandpass(int a, int b, int x, const char* low,
const char* high)
{
const bool withinInterval { (a <= x) && (x < b) };
return (withinInterval ? high : low);
}
当在 Godbolt 上使用-O3 -march=znver2
编译时,会给出以下汇编代码
vanilla_bandpass(int, int, int, char const*, char const*):
mov rax, r8
cmp edi, edx
jg .L4
cmp edx, esi
jge .L4
ret
.L4:
mov rax, rcx
ret
现在,我研究了创建一个没有跳转/分支的版本,看起来像这样
#include <cstdint>
const char* funky_bandpass(int a, int b, int x, const char* low,
const char* high)
{
const bool withinInterval { (a <= x) && (x < b) };
const auto low_ptr = reinterpret_cast<uintptr_t>(low) * (!withinInterval);
const auto high_ptr = reinterpret_cast<uintptr_t>(high) * withinInterval;
const auto ptr_sum = low_ptr + high_ptr;
const auto* result = reinterpret_cast<const char*>(ptr_sum);
return result;
}
这最终只是两个指针之间的“和弦”。 使用与之前相同的选项,此代码编译为
funky_bandpass(int, int, int, char const*, char const*):
mov r9d, esi
cmp edi, edx
mov esi, edx
setle dl
cmp esi, r9d
setl al
and edx, eax
mov eax, edx
and edx, 1
xor eax, 1
imul rdx, r8
movzx eax, al
imul rcx, rax
lea rax, [rcx+rdx]
ret
虽然乍一看,这个 function 有更多的指令,但仔细的基准测试表明它比vanilla_bandpass
实现快 1.8 到 1.9 倍。
uintptr_t
的这种使用是否有效且没有未定义的行为? 我很清楚围绕uintptr_t
的语言至少可以说是模糊和模棱两可的,并且标准中未明确指定的任何内容(如uintptr_t
上的算术)通常被认为是未定义的行为。 另一方面,在许多情况下,标准会在某些事物具有未定义的行为时明确调用,而在这种情况下它也不会这样做。 我知道将low_ptr
和high_ptr
加在一起时发生的“混合”触及主题,就像指针出处一样,这本身就是一个模糊的主题。
uintptr_t
的这种使用是否有效且没有未定义的行为?
是的。 从指针到 integer(具有足够大小,例如uintptr_t
)的转换定义明确,integer 算术定义明确。
另一件需要注意的事情是将修改后的uintptr_t
回指针是否会返回您想要的内容。 标准给出的唯一保证是转换为 integer 的指针转换回给出相同的地址。 幸运的是,这种保证对你来说已经足够了,因为你总是使用来自转换指针的精确值。
如果您使用的不是指向窄字符的指针,我认为您需要对转换结果使用std::launder
。
该标准不要求实现以有用的方式处理uintptr_t
到指针的转换,即使在uintptr_t
值是从指针到整数转换产生的情况下也是如此。 给例如
extern int x[5],y[5];
int *px5 = x+5, *py0 = y;
指针px5
和py0
可能比较相等,无论它们是否相等,代码都可以使用px5[-1]
访问x[4]
,或py0[0]
访问y[0]
,但不能访问px5[0]
也不py0[-1]
。 如果指针恰好相等,并且代码尝试访问((int*)(uintptr_t)px5)[-1]
,编译器可以将(int*)(uintptr_t)px5)
替换为py0
,因为该指针比较相等到px5
,然后在尝试访问py0[-1]
时跳过轨道。 同样,如果代码尝试访问((int*)(uintptr_t)py0)[0]
,编译器可以将(int*)(uintptr_t)py0
为px5
,然后在尝试访问px5[0]
时跳过轨道。
虽然编译器做这样的事情看起来很迟钝,但 clang 变得更加疯狂。 考虑:
#include <stdint.h>
extern int x[],y[];
int test(int i)
{
y[0] = 1;
uintptr_t px = (uintptr_t)(x+5);
uintptr_t py = (uintptr_t)(y+i);
int flag = (px==py);
if (flag)
y[i] = 2;
return y[0];
}
如果px
和py
恰好相等且i
为零,这将导致 clang 将y[0]
设置为 2 但返回 1。有关生成的代码,请参阅https://godbolt.org/z/7Sa_KZ ("mov eax,1 / ret”的意思是“返回 1”)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.