c++language-lawyerstrict-aliasingc++23

Is type-punning pointer-interconvertible types exempt from strict aliasing in C++23?


I'm trying to determine from the C++23 specification whether code such as the following is legal:

#include <type_traits>

template<bool B> struct S {
  int x;

  void go() {
    ++x;
    if constexpr (B)
      reinterpret_cast<S<false>*>(this)->go();
  }
};
static_assert(std::is_standard_layout_v<S<true>>
          && std::is_standard_layout_v<S<false>>);

int
main()
{
  S<true>{}.go();
  return 0;
}

[class.mem.general] states that S<true> is layout-compatible with S<false>.

Moreover, I think [basic.compound] implies that S<true> and S<false> are pointer-interconvertible through transitivity, because S<true> is pointer-interconvertible with int (because of S<true>::x), and by the same reasoning int is pointer-interconvertible with S<false>.

On the other hand, I don't see how the strict aliasing rule allows this, since S<true> and S<false> do not have the same dynamic type and are not similar (where similarity means being identical up to things like cv qualifiers and array to pointer decomposition).

Am I missing something about the strict aliasing rule? If I can't reinterpret_cast between pointers to layout-compatible types, then what's the point of layout compatibility in the first place? Would my example be legal if I put x in a non-templated supertype of the template (leaving no non-static members of the template, so it is still standard-layout)?


Solution

  • [class.mem.general] states that S is layout-compatible with S.

    Layout-compatibility has nothing to do with strict aliasing rule. It is purely used to determine whether accessing through an inactive member of a union is permitted in certain special cases (see below), while the strict aliasing rule is about accessing an object through an glvalue expression of a different type than the object's type.

    Moreover, I think [basic.compound] implies that S<true> and S<false> are pointer-interconvertible through transitivity, because S<true> is pointer-interconvertible with int (because of S<true>::x), and by the same reasoning int is pointer-interconvertible with S<false>.

    The pointer-inconvertible property is defined between objects, not types. For reinterpret_cast<S<false>*>(this) to be able to result in a pointer to a S<false> object, there must exist a S<false> object at the address of this in the first place. Then, additionally, that object must be pointer-interconvertible with the object that *this refers to. No S<false> object exists in your example, so it is pointless to consider pointer-interconvertibility.

    On the other hand, I don't see how the strict aliasing rule allows this, since S<true> and S<false> do not have the same dynamic type and are not similar (where similarity means being identical up to things like cv qualifiers and array to pointer decomposition).

    The strict aliasing rule is relevant only if pointer-interconvertibility didn't result in reinterpret_cast producing a pointer to an object of the correct type. reinterpret_cast will produce a pointer to the original object in that case, but with the mismatching expression type that reinterpret_cast was requested to cast to (assuming that the pointer is suitably aligned for the target type, otherwise the result is unspecified).

    The strict aliasing rule specifies under which circumstances it is allowed to still access through the result of the reinterpret_cast in that last case, i.e. when the result is a pointer of type T* pointing to an object of type U instead of an object of type T. If pointer-interconvertibility caused it to result in a pointer of type T* pointing to an object of type T, then the strict aliasing rule can't be relevant.

    Also, technically the strict aliasing rule in [basic.lval]/11 is specifically only about access, which is a defined term that applies only to reads and writes of scalar objects. The rule never has relevance between class types (contrary to its equivalent in C). Instead, expressions that take a class type operand have their own restrictions on whether the glvalue expression being used must refer to an object matching the type of the expression. ([basic.lval]/11 also includes one additional requirement of that kind for default copy/move constructors of unions)

    For example non-static member access with . (or indirctly ->) requires the left-hand side to actually refer to an object with type that is similar to that of the expression's type in its own rule (see [expr.ref]/9). And that is exactly the rule causing your member access expression reinterpret_cast<S<false>*>(this)->go to have undefined behavior: this points to a S<true> object. There exists no (pointer-interconvertible) S<false> object at the same address, so *reinterpret_cast<S<false>*>(this) is a glvalue expression of type S<false> referring to an object of type S<true>, but S<false> and S<true> are not similar.

    If I can't reinterpret_cast between pointers to layout-compatible types, then what's the point of layout compatibility in the first place?

    Layout-compatibility is used in one specific situation to allow reading (but not modifying!) members of inactive members of unions as if they referred to the corresponding member of the active member instead, if the union members are standard-layout classes with a common initial sequence of layout-compatible members. That is the only relevance of that property in the standard language. For example:

    union U {
        S<true> a;
        S<false> b;
    };
    
    int main() {
        U u;
        u.a = {1};
    
        // Allowed because `S<true>` and `S<false>` are standard-layout and
        // share a common initial sequence of members that are layout-compatible and
        // `x` is part of that initial sequence.
        return u.b.x; 
    }