![](/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.