Not in a portable way. There are several false negatives.
Counterexample #1: Memory aliasing
It is unusual for a device (e.g. RAM, ROM, or memory-mapped I/O) to use all of the address pins coming out of the processor. Typically, whatever number of address lines are needed by the device are connected to the lowest-order address lines of the processor, the highest address lines are used to select the device, and the address lines in between are not connected:
MSB -------- Address bus -------- LSB
| | ... | | x x ... x x | | ... | |
chip select unconnected to device
Such a device can be addressed as a block in the address space. However, the device also appears as several other blocks in the address space; each of these blocks physically point to the same locations on the device! The effect is called memory aliasing, and is much more common than you may realize.
For example, imagine a system with 16-bit addresses. Perhaps the top 4 address lines are used to select which chip is being addressed. Suppose we have a device assigned to A15:A12 == 0xE. Furthermore, this device only has 8 address lines coming out of it, so we connect those to A7:A0.
This device appears as addresses 0xE000 through 0xE0FF. However, it also appears at 0xE100 through 0xE1FF. Indeed, it appears 16 times in the address space, at any block 0xEz00 through 0xEzFF. Worse, each of these blocks physically point to the same thing. An access to 0xE123 is the same as an access to 0xE223, 0xE323, 0xE423, and so on.
So you can have two objects in memory that seem to point to different areas of memory, but in fact actually point to the same thing:
char *x = (char *)0xE000;
char *y = (char *)0xE300;
if (overlap(x, y, 16)) { ... }
A naive implementation of overlap()
would report these as two different objects. But they are the same object; writing to x[]
changes y[]
. Therefore, in this case you will get a false negative. A correct implementation of overlap()
would require and depend upon full knowledge of the system's memory map, making such a function completely non-portable.
Counterexample #2: Shared memory
Suppose x
and y
are overlapping objects in process A. We then use the operating system to create shared memory between process A and process B. Specifically, xx
is a shared memory pointer in process B that points to x
, and yy
is a shared memory pointer in process B that points to y
.
Back in process A, it's not hard to write a function that determines that x
and y
indeed overlap.
But depending on the operating system, pointers xx
and yy
in process B may look nothing like overlapping objects. But in reality, they do indeed point to overlapping objects. So you will get a false negative.
Is it theoretically possible to write a function that checks across processes for overlap? Probably, but keep in mind that I can make the problem even more difficult. I can create subsets of xx
and yy
that still overlap; I can share memory from process B to a third process; and so on. In any case, any such solution is not portable.
Counterexample #3: 8086 far pointers
The 8086 architecture on the original IBM PC used a type of memory mapping called "segmentation". A 16-bit register called the "segment" was multiplied by 16 and then added to another 16-bit register with the "base address" to obtain the 20-bit physical address.
Programs needing less than 64k of memory could get away with just the 16-bit base addresses, called "near pointers". But programs that needed more than 64k of memory had to maintain 32-bit "far pointers" which contained both the segment and the base address.
Because of the pointer arithmetic of segmentation, it is quite easy to make two far pointers that seem to be quite different, yet point to the same object:
far char *x = (far char *)0x12340005L;
far char *y = (far char *)0x10002345L;
In this case, x
and y
both point to the same physical address 0x12345, even though they are very different bit patterns.
Some compilers would treat x == y
as false because they have different bit patterns. Other compilers would do the math (with a performance penalty) and return true. Yet other compilers let you choose either behavior with a command-line switch or #pragma
.
The OP complains that these examples represent compilers that are not "standard-conforming". The argument is that if two pointers actually do point to the same object, then the standard says they must compare ==
.
If you're going to be such a language-lawyer, then no compiler has even conformed to the standard. Not gcc, not Microsoft C (two compilers proud of their conformance). Basically every system that has had a C compiler has had some degree of memory aliasing (counterexample #1). So every C compiler is guilty of allowing two !=
pointers point to the same thing.
On the other hand, if you interpret the standard by its intended meaning instead of its literal meaning, then those compilers do conform to the standard.
Sure, these are edge cases. Most programs are in user space, where #1 is hidden away. Few programs use shared memory (#2). And no one likes programming in a segmented memory model (#3). But exceptions like these are why the standard has so many instances of undefined behavior; many things which work in one case cannot be made to work that way on other cases.