Has signed/unsigned aliasing rule ever worked as intended?

Viewed 286

Here is the rule ([basic.lval]/8) in its C++17 form, but it looks similar in the other standards ("lvalue" instead of "glvalue" in C++98):

8 If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

(8.4) — a type that is the signed or unsigned type corresponding to the dynamic type of the object

The rule sounds like "You'll have UB, unless you do X", but this does not mean that if you do X, you won't get UB, as one might expect! And indeed, doing X is conditional or unconditional UB, depending on the version of the standard.

Lets look at the following code:

int i = -1;
unsigned j = reinterpret_cast<unsigned&>(i);

What is the behavior of this code?

C++98 and C++11

[expr.reinterpret.cast]/10 (/11 in C++11) (emphasis is mine):

An lvalue expression of type T1 can be cast to the type “reference to T2” if an expression of type “pointer to T1” can be explicitly converted to the type “pointer to T2” using a reinterpret_cast. That is, a reference cast reinterpret_cast(x) has the same effect as the conversion *reinterpret_cast(&x) with the built-in & and * operators. The result is an lvalue that refers to the same object as the source lvalue, but with a different type.

So reinterpret_cast<unsigned&>(i) lvalue refers to the int object i, but with usigned type. Initialization needs the value of the initializing expression, which formally means that the lvalue-to-rvalue conversion is applied to the lvalue.

[conv.lval]/1:

An lvalue of a non-function, non-array type T can be converted to an rvalue. If T is an incomplete type, a program that necessitates this conversion is ill-formed. If the object to which the lvalue refers is not an object of type T and is not an object of a type derived from T, or if the object is uninitialized, a program that necessitates this conversion has undefined behavior.

Our lvalue of unsigned type does not refer to an object of unsigned type which means that the behavior is undefined.

C++14 and C++17

In these standards the situation is a bit more complicated, but the rules have been slightly relaxed. [expr.reinterpret.cast]/11 tells the same:

The result refers to the same object as the source glvalue, but with the specified type.

The offending wording about UB has been deleted from [conv.lval]/1:

A glvalue of a non-function, non-array type T can be converted to a prvalue. If T is an incomplete type, a program that necessitates this conversion is ill-formed. If T is a non-class type, the type of the prvalue is the cv-unqualified version of T. Otherwise, the type of the prvalue is T.

But which value the L-to-R conversion reads? [conv.lval]/(2.6) (/(3.4) in C++17) answers this question:

… the value contained in the object indicated by the glvalue is the prvalue result

unsigned lvalue reinterpret_cast<unsigned&>(i) indicates the i int object with the value -1 and the prvalue resulting from L-to-R conversion has unsigned type. [expr]/4 says:

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.

-1 is definitely is not in the range of representable values for the unsigned type of the prvalue expression, so the behavior is undefined.

The behavior would be defined if the i object contained a value from the [0, INT_MAX] range.

The same reasoning is applicable in the case when an unsigned object is accessed through an int glvalue. This is UB in C++98 and C++11 and UB in C++14 and C++17 unless the value of the object in the [0, INT_MAX] range.

So, in contrast to a popular belief that this aliasing rule allows to reinterpret an object as containing a value of corresponding signed/unsigned type, it does not allow it. For the values in [0, INT_MAX] range, objects of signed and unsigned types have the same representation ("The range of non-negative values of a signed integer type is a subrange of the corresponding unsigned integer type, the representation of the same value in each of the two types is the same" says [basic.fundamental]/3 in C++17). It is hard to call such an access a "reinterpretation", not to mention this was unconditional UB prior to C++14.

What is the purpose of the rule ([basic.lval]/(8.4)) then?

1 Answers

This was the subject of the defect report 2214, which says:

Section: 6.9.1 [basic.fundamental] Status: C++17 Submitter: Richard Smith Date: 2015-12-15

[Adopted at the February/March, 2017 meeting.]

According to 6.9.1 [basic.fundamental] paragraph 3,

The range of non-negative values of a signed integer type is a subrange of the corresponding unsigned integer type, and the value representation of each corresponding signed/unsigned type shall be the same. (This is the wording in C++11 and C++14 versions, though the paragraph numbers may be different -- n.m.)

The corresponding wording from C11 is,

The range of nonnegative values of a signed integer type is a subrange of the corresponding unsigned integer type, and the representation of the same value in each type is the same.

The C wording is arguably clearer, but it loses the implication of the C++ wording that the sign bit of a signed type is part of the value representation of the corresponding unsigned type.

Proposed resolution (January, 2017):

Change 6.9.1 [basic.fundamental] paragraph 3 as follows:

...The standard and extended unsigned integer types are collectively called unsigned integer types. The range of non-negative values of a signed integer type is a subrange of the corresponding unsigned integer type, the representation of the same value in each of the two types is the same, and the value representation of each corresponding signed/unsigned type shall be the same. The standard signed integer types...

So this apparently was the intent all along. C++17 has just fixed the wording.

C and C++ standards never intended to allow reinterpreting negative values as unsigned or vice versa. There are several signed integer representations in the wild (e.g. one's complement, two's complement, sign-and-magnitude) and the standard doesn't mandate any of them, so it cannot prescribe effects of such reinterpretation. They could have been made implementation-defined, but considering the possibility of trap representations, there's no real benefit in that. "An implementation-defined result or a trap" is as good as "undefined".

Related