27

I was recently answering a question on the undefined behaviour of doing p < q in C when p and q are pointers into different objects/arrays. That got me thinking: C++ has the same (undefined) behaviour of < in this case, but also offers the standard library template std::less which is guaranteed to return the same thing as < when the pointers can be compared, and return some consistent ordering when they cannot.

Does C offer something with similar functionality which would allow safely comparing arbitrary pointers (to the same type)? I tried looking through the C11 standard and didn't find anything, but my experience in C is orders of magnitude smaller than in C++, so I could have easily missed something.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • 1
    Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/200716/discussion-on-question-by-angew-does-c-have-an-equivalent-of-stdless-from-c). – Samuel Liew Oct 11 '19 at 13:30
  • Related: [How does pointer comparison work in C? Is it ok to compare pointers that don't point to the same array?](https://stackoverflow.com/q/59516346) for background on `p – Peter Cordes Nov 21 '21 at 20:57

4 Answers4

24

On implementations with a flat memory model (basically everything), casting to uintptr_t will Just Work.

(But see Should pointer comparisons be signed or unsigned in 64-bit x86? for discussion of whether you should treat pointers as signed or not, including issues of forming pointers outside of objects which is UB in C.)

But systems with non-flat memory models do exist, and thinking about them can help explain the current situation, like C++ having different specs for < vs. std::less.


Part of the point of < on pointers to separate objects being UB in C (or at least unspecified in some C++ revisions) is to allow for weird machines, including non-flat memory models.

A well-known example is x86-16 real mode where pointers are segment:offset, forming a 20-bit linear address via (segment << 4) + offset. The same linear address can be represented by multiple different seg:off combinations.

C++ std::less on pointers on weird ISAs might need to be expensive, e.g. "normalize" a segment:offset on x86-16 to have offset <= 15. However, there's no portable way to implement this. The manipulation required to normalize a uintptr_t (or the object-representation of a pointer object) is implementation-specific.

But even on systems where C++ std::less has to be expensive, < doesn't have to be. For example, assuming a "large" memory model where an object fits within one segment, < can just compare the offset part and not even bother with the segment part. (Pointers inside the same object will have the same segment, and otherwise it's UB in C. C++17 changed to merely "unspecified", which might still allow skipping normalization and just comparing offsets.) This is assuming all pointers to any part of an object always use the same seg value, never normalizing. This is what you'd expect an ABI to require for a "large" as opposed to "huge" memory model. (See discussion in comments).

(Such a memory model might have a max object size of 64kiB for example, but a much larger max total address space that has room for many such max-sized objects. ISO C allows implementations to have a limit on object size that's lower than the max value (unsigned) size_t can represent, SIZE_MAX. For example even on flat memory model systems, GNU C limits max object size to PTRDIFF_MAX so size calculation can ignore signed overflow.) See this answer and discussion in comments.

If you want to allow objects larger than a segment, you need a "huge" memory model that has to worry about overflowing the offset part of a pointer when doing p++ to loop through an array, or when doing indexing / pointer arithmetic. This leads to slower code everywhere, but would probably mean that p < q would happen to work for pointers to different objects, because an implementation targeting a "huge" memory model would normally choose to keep all pointers normalized all the time. See What are near, far and huge pointers? - some real C compilers for x86 real mode did have an option to compile for the "huge" model where all pointers defaulted to "huge" unless declared otherwise.

x86 real-mode segmentation isn't the only non-flat memory model possible, it's merely a useful concrete example to illustrate how it's been handled by C/C++ implementations. In real life, implementations extended ISO C with the concept of far vs. near pointers, allowing programmers to choose when they can get away with just storing / passing around the 16-bit offset part, relative to some common data segment.

But a pure ISO C implementation would have to choose between a small memory model (everything except code in the same 64kiB with 16-bit pointers) or large or huge with all pointers being 32-bit. Some loops could optimize by incrementing just the offset part, but pointer objects couldn't be optimized to be smaller.


If you knew what the magic manipulation was for any given implementation, you could implement it in pure C. The problem is that different systems use different addressing and the details aren't parameterized by any portable macros.

Or maybe not: it might involve looking something up from a special segment table or something, e.g. like x86 protected mode instead of real mode where the segment part of the address is an index, not a value to be left shifted. You could set up partially-overlapping segments in protected mode, and the segment selector parts of addresses wouldn't necessarily even be ordered in the same order as the corresponding segment base addresses. Getting a linear address from a seg:off pointer in x86 protected mode might involve a system call, if the GDT and/or LDT aren't mapped into readable pages in your process.

(Of course mainstream OSes for x86 use a flat memory model so the segment base is always 0 (except for thread-local storage using fs or gs segments), and only the 32-bit or 64-bit "offset" part is used as a pointer.)

You could manually add code for various specific platforms, e.g. by default assume flat, or #ifdef something to detect x86 real mode and split uintptr_t into 16-bit halves for seg -= off>>4; off &= 0xf; then combine those parts back into a 32-bit number.

Sasha
  • 102
  • 8
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • Why would it be UB if the segment isn't equal? – Acorn Oct 11 '19 at 13:18
  • @Acorn: Meant to say that the other way around; fixed. pointers into the same object will have the same segment, else UB. – Peter Cordes Oct 11 '19 at 14:05
  • But why do you think it is UB in any case? (inverted logic or not, actually I didn't notice either) – Acorn Oct 11 '19 at 17:53
  • `p < q` is UB in C if they point to different objects, isn't it? I know `p - q` is. – Peter Cordes Oct 11 '19 at 17:55
  • In C, yeah; but you are discussing C++ in that paragraph, no? (In C++17, it is unspecified.) – Acorn Oct 11 '19 at 18:07
  • @Acorn: ah thanks. I was thinking about and trying to explain how C's `<` might work on a weird machine because this is a C question, but that's not how I wrote that paragraph. Will update. – Peter Cordes Oct 11 '19 at 18:11
  • Thinking about the C case: why can't objects span multiple segments? (i.e. it would mean it wouldn't be UB even if the segments differ). – Acorn Oct 11 '19 at 18:21
  • @Acorn: because I specified a "large" memory model where objects fit in a segment. Huge is possible, but makes `<` more expensive. (Along with looping through arrays). Updated my answer with more about large vs. huge. You're probably not the only one that would appreciate a concrete example of the kinds of things real implementations have done on x86 real mode. – Peter Cordes Oct 11 '19 at 18:40
  • But even in the large memory model an object can span several segments without being larger than a segment itself, no? – Acorn Oct 11 '19 at 19:12
  • @Acorn: no. The point and definition of a large memory model (in this context) is that objects *don't* span segments, allowing efficient pointer math (and comparisons) without renormalization after every `p++`. A side-effect is setting the max object size = (max) segment size, e.g. 64kiB. (Some machines allow variable-size segments.) – Peter Cordes Oct 11 '19 at 19:15
  • I don't understand. As soon as your object is bigger than 2^4 bytes, it will be able to be addressed in more than one segment, which means you can indeed have two pointers `p` and `q` pointing to the same byte, which by C++'s definition it means the pointers are equal. Which, in turn, it will mean `<` will return `false` for them (i.e. defined behavior in this case, rather than unspecified). At least that is my reading of C++17 (which may be wrong). – Acorn Oct 11 '19 at 19:15
  • @Acorn: No, the `(seg<<4) + off` formula means 64k segments have to be "paragraph aligned". A segment is the memory you can address with a range of offsets from the same seg value. For x86 real mode, that's 64kiB. You *don't* renormalize all the time, so you do have addresses like `0100:00ff`. We say that's part of the segment starting at `0100:0000` *because* we address it relative to the `0100:` segment base. It doesn't matter that `010f:000f` has the same linear address. It's up to the linker to not overlap any segments we actually do use. – Peter Cordes Oct 11 '19 at 19:21
  • @Acorn: The word "segment" gets overloaded with multiple meanings, too: in assembly you can manually define different data segments or sections just like you would for a flat memory model. So in those terms you can have a "segment" that's not paragraph aligned, i.e. the first offset that's part of that segment might not be `0`. Since you normally only access data at labels (symbols) within segments, the assembler+linker can take care of getting the right offset. e.g. `mov ax, @my_data` / `mov ds, ax` sets up DS with the right value for `mov ax, [ds:foo]` to work, for `foo` in `my_data`. – Peter Cordes Oct 11 '19 at 19:28
  • @Acorn: see also [How does x86 real-mode segments overlap help memory saving?](//stackoverflow.com/posts/comments/31863932) (especially Supercat's comment that I linked to, but also the answers) Also interesting, [Why didn't the 8086 use linear addressing?](//retrocomputing.stackexchange.com/q/6979) for more about how segmentation got used. And finally: [Calculating the segment address range of an 8086 assembly programm](//stackoverflow.com/q/54394486) shows MASM syntax for data segments with byte/word/para alignment, with a good answer. – Peter Cordes Oct 11 '19 at 19:42
  • @Acorn: Yes, I think only UB like running a pointer off the end of an array into another object (in a different logical segment) can create such pointers. Or memcpy / `char*` manipulation of the object-representation. But violating the ABI (where you'd spec that all pointers must use the "right" segment base) is always UB. Note that if you guarantee that comparing such pointers works, do you also guarantee you can `p++` all the way to the end of an array? But what if your `p` was `0000:FFFD`? You have to normalize all over the place, defeating the benefit of large vs. huge. – Peter Cordes Oct 11 '19 at 19:54
  • Oops, sorry, I deleted the comment because I kept going in circles writing it. Yeah, if you are trying to implement the large memory model, then you would definitely need to ensure pointers would never get formed to the same byte with different segments. So the compiler/linker has to keep track of that. Otherwise, there is no point to begin with. – Acorn Oct 11 '19 at 19:56
  • @Acorn: see [Does the C++ standard allow for an uninitialized bool to crash a program?](//stackoverflow.com/q/54120862) re: invalid object-representations (i.e. violating the ABI) being UB, whether you get that from uninitialized, hand-written asm, or memcpy / `char*` manipulation. Having to handle aliases would defeat much of the purpose of having a "large" model, except when the compiler can see a static array so it knows it has the expected seg:off for the loop. – Peter Cordes Oct 11 '19 at 19:56
  • 1
    @Acorn: Anyway, I don't see a mechanism that would generate aliases (different seg:off, same linear address) in a program without UB. So it's not like the compiler has to go out of its way to avoid that; every access to an object uses that object's `seg` value and an offset that's >= the offset within segment where that object starts. C makes it UB to do much of anything between pointers to different objects, including stuff like `tmp = a-b` and then `b[tmp]` to access `a[0]`. This discussion about segmented pointer aliasing is a good example of why that design-choice makes sense. – Peter Cordes Oct 11 '19 at 20:04
18

I once tried to find a way around this and I did find a solution that works for overlapping objects and in most other cases assuming the compiler does the "usual" thing.

You can first implement the suggestion in How to implement memmove in standard C without an intermediate copy? and then if that doesn't work cast to uintptr (a wrapper type for either uintptr_t or unsigned long long depending on whether uintptr_t is available) and get a most-likely accurate result (although it probably wouldn't matter anyway):

#include <stdint.h>
#ifndef UINTPTR_MAX
typedef unsigned long long uintptr;
#else
typedef uintptr_t uintptr;
#endif

int pcmp(const void *p1, const void *p2, size_t len)
{
    const unsigned char *s1 = p1;
    const unsigned char *s2 = p2;
    size_t l;

    /* Check for overlap */
    for( l = 0; l < len; l++ )
    {
        if( s1 + l == s2 || s1 + l == s2 + len - 1 )
        {
            /* The two objects overlap, so we're allowed to
               use comparison operators. */
            if(s1 > s2)
                return 1;
            else if (s1 < s2)
                return -1;
            else
                return 0;
        }
    }

    /* No overlap so the result probably won't really matter.
       Cast the result to `uintptr` and hope the compiler
       does the "usual" thing */
    if((uintptr)s1 > (uintptr)s2)
        return 1;
    else if ((uintptr)s1 < (uintptr)s2)
        return -1;
    else
        return 0;
}
S.S. Anne
  • 15,171
  • 8
  • 38
  • 76
5

Does C offer something with similar functionality which would allow safely comparing arbitrary pointers.

No


First let us only consider object pointers. Function pointers bring in a whole other set of concerns.

2 pointers p1, p2 can have different encodings and point to the same address so p1 == p2 even though memcmp(&p1, &p2, sizeof p1) is not 0. Such architectures are rare.

Yet conversion of these pointer to uintptr_t does not require the same integer result leading to (uintptr_t)p1 != (uinptr_t)p2.

(uintptr_t)p1 < (uinptr_t)p2 itself is well legal code, by may not provide the hoped for functionality.


If code truly needs to compare unrelated pointers, form a helper function less(const void *p1, const void *p2) and perform platform specific code there.

Perhaps:

// return -1,0,1 for <,==,> 
int ptrcmp(const void *c1, const void *c1) {
  // Equivalence test works on all platforms
  if (c1 == c2) {
    return 0;
  }
  // At this point, we know pointers are not equivalent.
  #ifdef UINTPTR_MAX
    uintptr_t u1 = (uintptr_t)c1;
    uintptr_t u2 = (uintptr_t)c2;
    // Below code "works" in that the computation is legal,
    //   but does it function as desired?
    // Likely, but strange systems lurk out in the wild. 
    // Check implementation before using
    #if tbd
      return (u1 > u2) - (u1 < u2);
    #else
      #error TBD code
    #endif
  #else
    #error TBD code
  #endif 
}
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
2

The C Standard explicitly allows implementations to behave "in a documented manner characteristic of the environment" when an action invokes "Undefined Behavior". When the Standard was written, it would have been obvious to everyone that implementations intended for low-level programming on platforms with a flat memory model should do precisely that when processing relational operators between arbitrary pointers. It also would have been obvious that implementations targeting platforms whose natural means of pointer comparisons would never have side effects should perform comparisons between arbitrary pointers in ways that don't have side effects.

There are three general circumstances where programmers might perform relational operators between pointers:

  1. Pointers to unrelated objects will never be compared.

  2. Code may compare pointers within an object in cases where the results would matter, or between unrelated objects in cases where the results wouldn't matter. A simple example of this would be an operation that can act upon possibly-overlapping array segments in either ascending or descending order. The choice of ascending or descending order would matter in cases where the objects overlap, but either order would be equally valid when acting upon array segments in unrelated objects.

  3. Code relies upon comparisons yielding a transitive ordering consistent with pointer equality.

The third type of usage would seldom occur outside of platform-specific code, which would either know that relational operators would simply work, or would know a platform-specific alternative. The second type of usage could occur in code which should be mostly portable, but almost all implementations could support the second type of usage just as cheaply as the first and there would be no reasons for them to do otherwise. The only people who should have any reason to care about whether the second usage was defined would be people writing compilers for platforms where such comparisons would be expensive or those seeking to ensure that their programs would be compatible with such platforms. Such people would be better placed than the Committee to judge the pros and cons of upholding a "no side effects" guarantee, and thus the Committee leaves the question open.

To be sure, the fact that there would be no reason for a compiler not to process a construct usefully is no guarantee that a "Gratuitously Clever Compiler" won't use the Standard as an excuse to do otherwise, but the reason the C Standard doesn't define a "less" operator is that the Committee expected that "<" would be adequate for almost all programs on almost all platforms.

supercat
  • 77,689
  • 9
  • 166
  • 211