What Is the Rationale for Limitations on Pointer Arithmetic or Comparison

What is the rationale for limitations on pointer arithmetic or comparison?

There are architectures where program and data spaces are separated, and it's simply impossible to subtract two arbitrary pointers. A pointer to a function or to const static data will be in a completely different address space than a normal variable.

Even if you arbitrarily supplied a ranking between different address spaces, there's a possibility that the diff_t type would need to be a larger size. And the process of comparing or subtracting two pointers would be greatly complicated. That's a bad idea in a language that is designed for speed.

What is the rationale of making subtraction of two pointers not related to the same array undefined behavior?

Speaking more academically: pointers are not numbers. They are pointers.

It is true that a pointer on your system is implemented as a numerical representation of an address-like representation of a location in some abstract kind of memory (probably a virtual, per-process memory space).

But C++ doesn't care about that. C++ wants you to think of pointers as post-its, as bookmarks, to specific objects. The numerical address values are just a side-effect. The only arithmetic that makes sense on a pointer is forwards and backwards through an array of objects; nothing else is philosophically meaningful.

This may seem pretty arcane and useless, but it's actually deliberate and useful. C++ doesn't want to constrain implementations to imbuing further meaning to practical, low-level computer properties that it cannot control. And, since there is no reason for it to do so (why would you want to do this?) it just says that the result is undefined.

In practice you may find that your subtraction works. However, compilers are extremely complicated and make great use of the standard's rules in order to generate the fastest code possible; that can and often will result in your program appearing to do strange things when you break the rules. Don't be too surprised if your pointer arithmetic operation is mangled when the compiler assumes that both the originating value and the result refer to the same array — an assumption that you violated.

Does the pointer arithmetic in this usage cause undefined behavior

It is undefined behavior because there are severe restrictions on what can be done with pointer arithmetic. The edits that you have made and that were suggested do nothing to fix this.

Undefined Behavior in Addition

StructA* a = (StructA*)((char*)copy + offset);

First of all, this is undefined behavior due to the addition onto copy:

When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.

  • (4.1) If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
  • (4.2) Otherwise, if P points to an array element i of an array object x with n elements ([dcl.array]), the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) array element i+j of x if 0 ≤ i + j ≤ n and the expression P - J points to the (possibly-hypothetical) array element i − j of x if 0 ≤ i − j ≤ n.
  • (4.3) Otherwise, the behavior is undefined.

See https://eel.is/c++draft/expr.add#4

In short, performing pointer arithmetic on non-arrays and non-null-pointers is always undefined behavior. Even if copy or its members were arrays, adding onto a pointer so that it becomes:

  • two or more past the end of the array
  • at least one before the first element

is also undefined behavior.

Undefined Behavior in Subtraction

ptrdiff_t offset = (char*)original - (char*)(copy->b);

The subtraction of your two pointers is also undefined behavior:

When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; [...]

  • (5.1) If P and Q both evaluate to null pointer values, the result is 0.
  • (5.2) Otherwise, if P and Q point to, respectively, array elements i and j of the same array object x, the expression P - Q has the value i − j.
  • (5.3) Otherwise, the behavior is undefined.

See https://eel.is/c++draft/expr.add#5

So subtracting pointers from one another, when they are not both null or pointers to elements of the same array is undefined behavior.

Undefined Behavior in C

The C standard has similar restrictions:

(8) [...] If the pointer operand points to an element of
an array object, and the array is large enough, the result points to an element offset from
the original element such that the difference of the subscripts of the resulting and original
array elements equals the integer expression.

(The standard does not mention what happens for non-array pointer addition)

(9) When two pointers are subtracted, both shall point to elements of the same array object,
or one past the last element of the array object; [...]

See §6.5.6 Additive Operators in the C11 standard (n1570).

Using Data Member Pointers Instead

A clean and type-safe solution in C++ would be to use data member pointers.

typedef struct StructB {
StructA a;
StructA StructB::*b_offset;
} StructB;

int main() {
StructB* original = (StructB*) malloc(sizeof(StructB));
original->a.a = 5;
original->b_offset = &StructB::a;

StructB* copy = (StructB*) malloc(sizeof(StructB));
memcpy(copy, original, sizeof(StructB));
free(original);
printf("%i\n", (copy->*(copy->b_offset)).a);
free(copy);
}

Notes

The standard citations are from a C++ draft. The C++11 which you have cited does not appear to have any looser restrictions on pointer arithmetic, it is just formatted differently. See C++11 standard (n3337).

What harm would arise by pointer arithmetic beyond a valid memory range?

The simplest answer is that it is conceivable that a machine traps integer overflow. If that were the case, then any pointer arithmetic which wasn't confined to a single storage region might cause overflow, which would cause a trap, disrupting execution of the program. C shouldn't be obliged to check for possible overflow before attempting pointer arithmetic, so the standard allows a C implementation on such a machine to just allow the trap to happen, even if chaos ensues.

Another case is an architecture where memory is segmented, so that a pointer consists of a segment address (with implicit trailing 0s) and an offset. Any given object must fit in a single segment, which means that valid pointer arithmetic can work only on the offset. Again, overflowing the offset in the course of pointer arithmetic might produce random results, and the C implementation is under no obligation to check for that.

Finally, there may well be optimizations which the compiler can produce on the assumption that all pointer arithmetic is valid. As a simple motivating case:

if (iter - 1 < object.end()) {...}

Here the test can be omitted because it must be true for any pointer iter whose value is a valid position in (or just after) object. The UB for invalid pointer arithmetic means that the compiler is not under any obligation to attempt to prove that iter is valid (although it might need to ensure that it is based on a pointer into object), so it can just drop the comparison and proceed to generate unconditional code. Some compilers may do this sort of thing, so watch out :)

Here, by the way, is the important difference between unspecified behaviour and undefined behaviour. Comparing two pointers (of the same type) with == is defined regardless of whether they are pointers into the same object. In particular, if a and b are two different objects of the same type, end_a is a pointer to one-past-the-end of a and begin_b is a pointer to b, then

end_a == begin_b

is unspecified; it will be 1 if and only if b happens to be just after a in memory, and otherwise 0. Since you can't normally rely on knowing that (unless a and b are array elements of the same array), the comparison is normally meaningless; but it is not undefined behaviour and the compiler needs to arrange for either 0 or 1 to be produced (and moreover, for the same comparison to consistently have the same value, since you can rely on objects not moving around in memory.)

How does pointer comparison work in C? Is it ok to compare pointers that don't point to the same array?

According to the C11 standard, the relational operators <, <=, >, and >= may only be used on pointers to elements of the same array or struct object. This is spelled out in section 6.5.8p5:

When two pointers are compared, the result depends on the
relative locations in the address space of the objects pointed to.
If two pointers to object types both point to the same object, or
both point one past the last element of the same array
object, they compare equal. If the objects pointed to are
members of the same aggregate object,pointers to structure
members declared later compare greater than pointers to
members declared earlier in the structure, and pointers to
array elements with larger subscript values compare greater than
pointers to elements of the same array with lower subscript values.
All pointers to members of the same union object compare
equal. If the expression P points to an element of an array
object and the expression Q points to the last element of the
same array object, the pointer expression Q+1 compares greater than P.
In all other cases, the behavior is undefined.

Note that any comparisons that do not satisfy this requirement invoke undefined behavior, meaning (among other things) that you can't depend on the results to be repeatable.

In your particular case, for both the comparison between the addresses of two local variables and between the address of a local and a dynamic address, the operation appeared to "work", however the result could change by making a seemingly unrelated change to your code or even compiling the same code with different optimization settings. With undefined behavior, just because the code could crash or generate an error doesn't mean it will.

As an example, an x86 processor running in 8086 real mode has a segmented memory model using a 16-bit segment and a 16-bit offset to build a 20-bit address. So in this case an address doesn't convert exactly to an integer.

The equality operators == and != however do not have this restriction. They can be used between any two pointers to compatible types or NULL pointers. So using == or != in both of your examples would produce valid C code.

However, even with == and != you could get some unexpected yet still well-defined results. See Can an equality comparison of unrelated pointers evaluate to true? for more details on this.

Regarding the exam question given by your professor, it makes a number of flawed assumptions:

  • A flat memory model exists where there is a 1-to-1 correspondence between an address and an integer value.
  • That the converted pointer values fit inside an integer type.
  • That the implementation simply treats pointers as integers when performing comparisons without exploiting the freedom given by undefined behavior.
  • That a stack is used and that local variables are stored there.
  • That a heap is used to pull allocated memory from.
  • That the stack (and therefore local variables) appears at a higher address than the heap (and therefore allocated objects).
  • That string constants appear at a lower address then the heap.

If you were to run this code on an architecture and/or with a compiler that does not satisfy these assumptions then you could get very different results.

Also, both examples also exhibit undefined behavior when they call strcpy, since the right operand (in some cases) points to a single character and not a null terminated string, resulting in the function reading past the bounds of the given variable.

Arithmetic of pointer MISRA violation

This would be from MISRA-C:2004 chapter 17, which was rather irrational about the use of pointers and arrays. This chapter was rewritten from scratch in MISRA-C:2012 (chapter 18). I would strongly recommend to upgrade, since MISRA-C:2004 simply doesn't make much sense here.

As for how to make your code MISRA-C:2004 compliant, do this:

int math(int address[], int size)
{
int sum = 0;
int i; // declaration must be here, MISRA-C:2004 does not allow C99
for (i = 0; i < size; i++)
{
sum += address[i];
}
return sum;
}

Yes it does the very same thing. But at least it made your code slightly more readable.


To make your code even safer, although not compliant with any MISRA, do this:

// better than MISRA-C but not compliant
int math(size_t size, int address[size])
{
int sum = 0;
for (size_t i = 0; i < size; i++)
{
sum += address[i];
}
return sum;
}

Or in case of high integrity systems, you could even do:

int math(size_t size, int (*array)[size])
{
int* address = *array;
...

Both of these alternatives give safer code than MISRA-C.

Pointer arithmetics with two different buffers

To add the standard quote:

expr.add#5

When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; this type shall be the same type that is defined as std::ptrdiff_­t in the <cstddef> header ([support.types]).

  • (5.1)
    If P and Q both evaluate to null pointer values, the result is 0.

  • (5.2)
    Otherwise, if P and Q point to, respectively, elements x[i] and x[j] of the same array object x, the expression P - Q has the value i−j.

  • (5.3)
    Otherwise, the behavior is undefined.
    [ Note: If the value i−j is not in the range of representable values of type std::ptrdiff_­t, the behavior is undefined.
    — end note
     ]

(5.1) does not apply as the pointers are not nullptrs. (5.2) does not apply because the pointers are not into the same array. So, we are left with (5.3) - UB.

Why its not recommended to use pointer for array access in C

This code is not recommended:

int    *p_array;
p_array = (int *)malloc(sizeof(int)*50); // allocate 50 ints

int *dptr = p_array;

for(i=0; i < 50; i++) {
*dptr = 0;
dptr++;
}

because 1) for no reason you have two different pointers that point to the same place, 2) you don't check the result of malloc() -- it's known to return NULL occasionally, 3) the code is not easy to read and 4) it's easy to make a silly mistake very hard to spot later on.

All in all, I'd recommend to use this instead:

int array[50] = { 0 };  // make sure it's zero-initialized
int* p_array = array; // if you must =)


Related Topics



Leave a reply



Submit