c++memorylanguage-lawyermemory-alignmentalignas

alignas specifier: on the type / on the member data


Are the following two cases equivalent in all situations?

1) alignas on the type

template <typename T, size_t N, size_t Alignment>
struct alignas(Alignment) A
{
    T data[N];
};

2) alignas on the member array

template <typename T, size_t N, size_t Alignment>
struct A
{
    alignas(Alignment) T data[N];
};

Solution

  • From a language lawyer perspective, they are not equivalent. The layout of a class is implementation-defined, and there are few guarantees. One guarantee is that if T is a standard-layout type, then both versions of your A class are standard-layout types, which implies that they do not have padding at the beginning ([class.mem.general]/27). Depending on the alignment you specify, A might have to have padding at the end to ensure that its size is a multiple of that alignment. But there is no guarantee in the standard that the implementation uses the smallest amount of padding necessary. For example, if T has an alignment requirement of 4 and N is 3, and you requested an alignment of 8, then either A class could have 4, or 12, or 20, ... bytes of padding at the end. In practice, the implementation will use the smallest amount of padding necessary, and the two versions of your A will have the same layout.

    If T is not a standard-layout type, the implementation is allowed to insert padding at the beginning of A, although it's unlikely that an implementation would actually do so. The amount of padding the implementation inserts may differ between the two versions of your A.

    Practical considerations generally force two struct types that have a common initial sequence to have the same offset for corresponding data members in order to implement [class.mem.general]/26 efficiently. Consider, for example:

    struct T1 {
        int t11;
        int t12;
    };
    struct T2 {
        int t21;
        int t22;
    };
    union U {
        T1 t1;
        T2 t2;
    };
    int foo(const U& u) {
        return u.t1.t12;
    }
    

    The standard requires that, if u's active member is actually t2 instead of t1, then this code behaves as if it returns u.t2.t22. In order to implement this, the compiler will ensure that t12's offset within T1 is the same as t22's offset within T2, and simply access the int that is located at that offset from the address of u.

    But if I'm speaking as a language lawyer, I have to say that the standard does not guarantee this. A C++ implementation could, conceivably, give T1 and T2 different layouts, store some extra information somewhere that lets it keep track of the active member of a U object, and when it evaluates u.t1.t12, check if the active member is t2, and if it is, then use the offset of t22 instead of that of t12.

    In practice, all known C++ implementations will give the common initial sequence of two standard-layout structs equal offsets for corresponding members. However, your A is only standard-layout when T is standard-layout, and even when your A's are standard-layout, the common initial sequence of the two different versions of A is not guaranteed to include the data member because, in the case where the alignment specifier is applied to A itself, it is not clear what the standard considers the alignment requirement of the data member is, and that means [class.mem.general]/23.2 might not be satisfied.

    All that being said, I would find it surprising if any implementation laid out the two versions of your A differently (given identical template arguments). Practically speaking, you can depend on them having the same layout, but you shouldn't, because I see no good reason to not just write what you mean. Do you care about the alignment of A, or the alignment of its member? Whichever one you actually care about, write that. Or, if you care about both, you can put the alignas in both places.