Logo

Blog


C++20: Aggregate, POD, trivial type, standard layout class, what is what

In this post, I like to give you some details about the new definition in C++20 of a:

  • Aggregate
  • POD
  • Trivial type
  • Standard layout class

You may already know some of these terms, others may be new, and for some, the definition has changed.

A trivial type

In short, a trivial type is a class or struct for which the compiler provides all the special members, either implicitly or because they are explicitly defaulted by us. Once we provide our own default constructor for a class, such a type is no longer a trivial type. Another property of a trivial type is that such a type occupies a contiguous memory area, which makes it memcopy-able, and it supports static initialization.

We can copy a trivial type into a char or unsigned char array and back. Alignment and also the size of the type may be different from char due to alignment and padding rules.

One important property of trivial type is that they can have mixed access specifiers. We all know the rule that classes and structs are have the same layout order in memory as the order of the defined data members. However, whenever we have different access specifiers in a class, the standard specifies that, in this case, the order is unspecified.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Unspecified {
public:
    int a;

private:
    int b;

public:
    int c;
};

This allows a compiler, at least in theory, because I don't know one compiler which takes the opportunity of this, to reorder the data members, of a struct. One way would be that the struct starts with all public members followed by all private data members:

1
2
3
4
5
6
7
8
struct Unspecified {
public:
    int a;
    int c; A Compiler reordered c

private:
    int b;
};

As shown in |#A], this reordering causes an incompatibility with C, which doesn't know about private and hence that reordering rule. The result is that a trivial type is not usable in C code.

When you have read the above carefully, you noticed that I talked only about special members, which we must either default or let the compiler provide. However, a trivial type can still have a user-provided constructor, as long as this is not the default constructor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Trivial {
    int a;

    Trivial() = default;
    Trivial(int _a) A A user provided constructor
    : a{_a+1}
    {}
};

static_assert(std::is_trivial_v<Trivial>);

A standard-layout class

The term layout refers to the arrangement of members of classes, structs, or unions in memory. A standard-layout class defines a type, which does not use certain specific C++ features that are not available in C. Such a type is memcopy-able, and its layout is defined in a way that the same type can be used in a C program. So in more general speak, a standard-layout class (or type) is compatible with C and can be exchanged over a C-API.

With all that said, we have a standard-layout class if it does not contain any language elements which are not present in C. Here is a more complete definition. The numbers refer to the code example that follows:

  • No virtual base classes 1;
  • No virtual functions 2;
  • No reference-members 3;
  • The same access control for all non-static data members 4. Right this is different from the definition of a trivial type, we can have a struct with only private or protected members, and it is still standard-layout;
  • Has a standard-layout base class or classes 5;
  • All non-static data members are standard-layout as well 6;
  • Meets one of these conditions:
  • no non-static data member in the most-derived class and no more than one base class with non-static data members, or
  • has no base classes with non-static data members

Please note that at this point, we talk about the layout in the memory and the interoperability with C. What you do not see in the definition above is that a standard-layout class could have special members. They do not change the memory layout. Special members only help to initialize an object. Despite that C has no special members, we can have them in C++ in a standard-layout type because it is just about the layout, nothing else.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Base {};

struct NotStandardLayoutBase {
  int& ref;  3 
};

struct NotStandardLayout : virtual Base,                   1 
                           public NotStandardLayoutBase {  5 

  virtual void Fun();  2 Virtual function

private:
  int                   a;  4 Mixing of access specifiers
  NotStandardLayoutBase b;  6 A non-standard-layout member
};

POD

Before C++20, we had the definition of a POD type. The specification was that a POD is a type that is trivial and standard-layout. With C++20, the definition of POD, as well as the type-trait std::is_pod, is gone. No worries, your favorite STL vendor will for sure provide the type-trait for some time before it actually gets removed.

The idea of a POD was that it supports two distinct properties:

  • we can compile a POD in C++ and still use it in a C program, as it has the same memory layout in both languages (meet by standard-layout);
  • a POD supports static initialization (meet by trivial type).

While a standard-layout type has a C compatible memory layout, it can have a user-defined default constructor. This is something C doesn't have. Hence we need the second property, a trivial type. As we learned above, such a type is default constructible the same way as a C struct.

As far as the C++20 standard is concerned, the term POD no longer exists. POD is replaced by standard-layout and trivial type. As a consequence, the type-trait std::is_pod is deprecated in C++20, and you are encouraged to use the two type-traits std::is_trivial and std::is_standard_layout.

Aggregate

An aggregate can be seen as a composition of other types. All data members of an aggregate must be public. The interesting thing is that since C++17 aggregates can have public base classes as long as they are not virtual. These base classes do not need to be aggregates themself. If they are not, they are list-initialized.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
A Base is no aggregate due to the deleted default constructor
struct Base
{
  int i;
  Base(int a)
  : i(a)
  {}
};

B A is no aggregate as well
struct A
{
  int i;
  A(int a)
  : i(a)
  {}
};

C SomeType is an aggregate
struct SomeType : Base
{
  int x;
  A   y;
};

SomeType a{2, 3, 4};  D Base is 2, x=3 and y=4

static_assert(not std::is_aggregate_v<Base>);
static_assert(not std::is_aggregate_v<A>);
static_assert(std::is_aggregate_v<SomeType>);

Above, we see such an aggregate. SomeType derives public from Base, which is not an aggregate. Due to the constructor, we provided we lost the default constructor, which makes Base no longer an aggregate. In SomeType, we have another non-aggregate example, the member y of type A. The rules from Base apply to A as well. We provided a constructor hence we lost the default constructor. In D, we see that we still can initialize all members and base classes of SomeType.

An aggregate can be standard-layout and trivial. However, as an aggregate can contain references, it is not always standard-layout or a trivial type.

Another interesting property of an aggregate is that aggregates are always decomposable in a structured binding.

Properties Overview

Typememcopy-ableC compatible memory layout
Trivial typeYesNo
Standard-layoutYesYes
AggregateYesmaybe

Why should you care?

Why do you need to differentiate? One apparent reason is compatibility with C. Should that be the purpose, you must ensure that the type in question is trivial and standard-layout. For data exchange via network or file, you can use a type that satisfies only trivial or standard-layout. I would recommend either aiming for both or at least standard-layout. Because with only trivial, you risk that future compiler's or a different compiler on the other side, do rearrange data members, and then a receiver may have a different layout understanding than the sender.

Andreas