Stack Exchange
Pointer arithmetic on uintptr_t
I have to implement a "bandpass" filter. Let a and b denote two integers that induce a half-open interval [a, b). If some argument x lies within this interval (i.e., a <= x < b), I return a pointer to a C string const char* high, otherwise I return a pointer const char* low. The vanilla implementation of this function looks like
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);
}

which when compiled with -O3 -march=znver2 on Godbolt gives the following assembly code
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

Now, I've looked into creating a version without a jump/branch, which looks like this
#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;
}

which just is ultimately just a "chord" between two pointers. Using the same options as before, this code compiles to
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

While at first glance, this function has more instructions, careful benchmarking shows that it's 1.8x to 1.9x faster than the vanilla_bandpass implementation.
Is this use of uintptr_t valid and free of undefined behavior? I'm well aware that the language around uintptr_t is vague and ambiguous to say the least, and that anything that isn't explicitly specified in the standard (like arithmetic on uintptr_t) is generally considered undefined behavior. On the other hand, in many cases, the standard explicitly calls out when something has undefined behavior, which it also doesn't do in this case. I'm aware that the "blending" that happens when adding together low_ptr and high_ptr touches on topics just as pointer provenance, which is a murky topic in and of itself.

I can't tell out of the c++ standard specification perspective, but uintptr_t is just a 32/64 bit unsigned number so multiplying it with another number should be no issue at all. reinterpret_cast could be considered in general as invoking undefined behaviours, manly because after recasting an object specially the lifetime of the resulting object is undefined, but using it with pointers to a char* should not be an issue as well.

@WolverinDEV reinterpret_cast is not considered in general as invoking undefined behaviours. The lifetime of the resulting object is well defined. It is simply a prvalue (unless casting to a reference).

https://godbolt.org/z/tk3Uxq won't this be faster (it writes/reads memory, though...)

https://godbolt.org/z/NzH4sC clang does better

@LanguageLawyer even more interesting, Clang compiles both versions to exactly the same assembly. This one definitely goes to clang. Benchmarking shows that obviously give the same performance.


Is this use of uintptr_t valid and free of undefined behavior?

Yes. Conversion from pointer to integer (of sufficient size such as uintptr_t) is well defined, and integer arithmetic is well defined.
Another thing to be wary about is whether converting a modified uintptr_t back to a pointer gives back what you want. Only guarantee given by the standard is that pointer converted to integer  converted back gives the same address. Luckily, this guarantee is sufficient for you, because you always use the exact value from a converted pointer.
If you were using something other than pointer to narrow character, I think you would need to use std::launder on the result of the conversion. 

@MaximEgorushkin The integer is never incremented. It is multiplied by 1 or 0, and added to 0.

Are you sure about std::launder? I recall std::launder being required for accessing objects that have been allocated in another object's storage, the lifetime of the object of which has been ended. My example involves no lifetime and/or storage semantics, just pointer provenance and arithmetic.

@JonasMüller It's just because the standard says that converting integer to pointer gives the same value as the original pointer, but having the correct value is technically not sufficient to have a pointer to the object. I'm not 100% certain it's necessary. But it never hurts and the rules are complex, so better safe than sorry :)

The Standard may regard conversion from any valid pointer to uintptr_t as defined behavior in all cases, and conversion between any two uintptr_t values as defined behavior in all cases, but that doesn't mean that code which produces a uintptr_t from a pointer "just past" the end of one array and another from a pointer to an array which immediately follows it, and compares those two uintptr_t values, will never cause clang to process nearby code nonsensically.

@supercat The converted pointers are never compared in the program. The comparisons are on the other argument integers. Only operations on the integers converted from pointers are multiplication by 1, multiplication by 0, and addition of 0. The result of the calculation is always exactly one of the converted values. Which is then converted back to a pointer.

@eerorika: See https://godbolt.org/z/7Sa_KZ for an example of the craziness I'm talking about.  The Standard doesn't specify anything about the effect of doing anything with the result of a round-tripped pointer-to-uintptr_t-to-pointer conversion other than that it will compare equal to the original pointer.  Given how clang treats the linked example, I wouldn't trust it to behave usefully in cases where it wouldn't be required to do so.

The Standard doesn't require that implementations process uintptr_t-to-pointer conversions in useful fashion even in cases where the uintptr_t values are produced from pointer-to-integer conversions.  Given e.g.
extern int x[5],y[5];
int *px5 = x+5, *py0 = y;

the pointers px5 and py0 might compare equal, and regardless of whether they do or not, code may use px5[-1] to access x[4], or py0[0] to access y[0], but may not access px5[0] nor py0[-1].  If the pointers happen to be equal, and code attempts to access ((int*)(uintptr_t)px5)[-1], a compiler could replace (int*)(uintptr_t)px5) with py0, since that pointer would compare equal to px5, but then jump the rails when attempting to access py0[-1].  Likewise if code tries to access ((int*)(uintptr_t)py0)[0], a compiler could replace (int*)(uintptr_t)py0 with px5, and then jump the rails when attempting to access px5[0].
While it may seem obtuse for a compiler to do such a thing, clang gets even crazier.  Consider:
#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];
}

If px and py are coincidentally equal and i is zero, that will cause clang to set y[0] to 2 but return 1.  See https://godbolt.org/z/7Sa_KZ for generated code ("mov eax,1 / ret" means "return 1").

I know you have an axe to grind with GCC's and Clang's 2020-era C and C++ optimizations, but this doesn't actually answer the question (and goes on a pointer provenance diatribe).

@JonasMüller: My points are that (1) the Standard doesn't require that implementations regard pointers produced by conversion from uintptr_t as usable for any particular purpose, and (2) while many people might expect compilers to be cautious around conversions to/from uintptr_t, whether or not the Standard would compel them to do so, some compilers optimize very aggressively even around such conversions.  Since the only reason the OP's code would fail would be if compilers make unwarranted assumptions about pointer provenance, the fact that compilers make such assumptions...

...would seem highly relevant to the safety of such constructs.  Do you believe clang should be relied upon to process the OP's code correctly?

@JonasMüller: In most cases where one asks "Is this tricky construct defined", the correct answer would be "Technically no, but the authors of the Standard would have expected quality implementations to process it usefully anyway".  I would regard the OP's question as such a case.  Such an answer would seem dishonest, however, if I didn't mention reasons why I would not trust some compilers to process the behavior usefully.  Since this code would certainly work in the absence of dodgy pointer-provenance optimizations, the fact that clang applies pointer-provenance optimizations to...

...values of type uintptr_t would seem highly relevant to the question of whether it should be expected to refrain from applying such "optimizations" to the OP's code.
