Is it legal to convert a pointer/reference to a fixed array size to a smaller size

Viewed 669

Is it legal as per the C++ standard to convert a pointer or reference to a fixed array (e.g. T(*)[N] or T(&)[N]) to a pointer or reference to a smaller fixed array of the same type and CV qualification (e.g. T(*)[M] or T(&)[M])?

Basically, would this always be well-formed for all instantiations of T (regardless of layout-type):

void consume(T(&array)[2]);

void receive(T(&array)[6])
{
  consume(reinterpret_cast<T(&)[2]>(array));   
}

I don't see any references to this being a valid conversion in:

However, it appears that all major compilers accept this and generate proper code even when optimized when using T = std::string (compiler explorer)(not that this proves much, if it is undefined behavior).

It's my understanding that this should be illegal as per the type-system, since an object of T[2] was never truly created, which means a reference of T(&)[2] would be invalid.


I'm tagging this question because this is the version I am most interested in the answer for, but I would be curious to know whether this answer is different in newer versions a well.

2 Answers

There’s not much to say here except no, in any language version: the types are simply unrelated. C++20 does allow conversion from T (*)[N] to T (*)[] (and similarly for references), but that doesn’t mean you can treat two different Ns equivalently. The closest you’re going to get to a “reference” for this rule is [conv.array]/1 (“The result is a pointer to the first element of the array.”, which T[2] does not exist in your example) and a note in [defns.undefined] (“Undefined behavior may be expected when this document omits any explicit definition of behavior”).

Part of the reason that compilers don’t “catch” you is that such reinterpret_casts are valid to return to the real type of an object after another reinterpret_cast used to “sneak” it through an interface that expects a pointer or reference to a different type (but doesn’t use it as that type!). That means that the code as given is legitimate, but the obvious sort of definition for consume and caller for receive would together cause undefined behavior. (The other part is that optimizers often leave code alone that’s always undefined unless it can eliminate a branch.)

A late additional answer, that rather yields the quality of a comment but would exceed the allowed content amount by far:

At first: Great question! It's remarkable, that such a quite obvious issue is hard to be verified and generates a lot of confusion even among experts. Worth to mention, that I've seen code of that category quite often already...

Some words about undefined behavior first

I think at least the question about the pointer usage is a great example where one has to admit, that theoretical undefined behavior from one aspect of the language can sometimes be "beaten" by two other strong aspects:

  1. Are there other standard clauses that reduce the degree of UB for the aspect of interest for several cases? Are there maybe clauses whose priorities within the standard are ambiguous to each other even? (There are several prominent examples still existing in C++20, see conversion-type-id handling for operator auto() for instance...).
  2. Are there (Turing-) provable arguments, that any theoretical and practical compiler realization has to behave as you expect since there are other constraints from the language, that have to determine it that way? Saying that even if UB can quirky mean, the compiler could apply "I can do what I want here, even the biggest mess" for your case, it might be provable, that the ensuring of other specified(!) language aspects determines that to be at least effectively impossible.

So with respect to point 2, there's an often underrated aspect: What are the constraints (if definable) by the model of the abstract machine, that determine the outcome of any theoretical (compiler-) implementation for the given code?

So, many words so far, but does anything from 1) apply to your concrete case (the pointer way)?

As multiple times users mentioned within the comments, a chance for that lies here basic.types#basic.compound-4:

Two objects a and b are pointer-interconvertible if:

...

(4.4) there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.

That's the simple rule of transitivity. Can we actually find such a c (for arrays)?

Within the same section, the standard says further on:

If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_­cast. [ Note: An array object and its first element are not pointer-interconvertible, even though they have the same address.  — end note ]

demolishing our dreams here of our approach via the pointer-to-the-first-element - usage. There isn't such a c for arrays.

Do we have another chance? You mentioned expr.reinterpret.cast#7 :

An object pointer can be explicitly converted to an object pointer of a different type.70 When a prvalue v of type “pointer to T1” is converted to the type “pointer to cv T2”, the result is static_cast<cv T2*>(static_cast<cv void*>(v)) if both T1 and T2 are standard-layout types ([basic.types]) and the alignment requirements of T2 are no stricter than those of T1, or if either type is void. Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. The result of any other such pointer conversion is unspecified.

This looks promising at first glance but the devil is in the details. That solely ensures that you can apply the pointer conversion since the alignment requirements for both arrays are equal, but not refer to interconvertibility (i.e. object usage itself) a priori. As Davis already said: with the pointer to the first element, one could still use reinterpret_cast as some kind of a fake fascade fully standard compliant as long as the wrong type pointer to T[2] is only really used as a forwarder and all actual use cases refer to the element pointer via an according reinterpret_cast and as long as all use cases "are aware" of the fact, that the actual type was a T[4]. Trivial to see, that this is still hacky as hell for many scenarios. At least a type aliasing in order to emphasize the forwarding quality would be recommended here.

So a strict interpretation of the standard here is: It's undefined behavior with the note that we all know that it should work well with all common modern compilers on many common platforms (I know, the latter was not your question).

Do we have some chances according to my point 2) about effective "weak UB" from above?

I don't think so as long as only the abstract machine is on focus here. For instance, IMO there's no restriction from the standard, a compiler/environment could not handle (abstract) allocation schemes differently between arrays of different size (changed intrinsics for threshold sizes for instance) while still ensuring alignment requirements. To be very quirky here, one could say a very exotic compiler could be allowed to refer to underlying dynamic storage duration mechanisms even for scoped objects that appear to be on that what we know as stack. Another related possible issue could be the question about proper deallocation of arrays of dynamic storage duration here (see the similar debate about UB in the context of inheritance from classes that do not provide virtual destructors). I highly doubt that it's trivial to validate, that the standard guarantees a valid cleanup here a priori, i.e. effectively calling ~T[4] for your example for all cases.

Related