Logo

Blog


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

This post is a short version of Chapter 8 Aggregate initialization from my latest book Programming with C++20. The book contains a more detailed explanation and more information about this topic.

In this post, I would 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, making it memcopy-able and supporting static initialization.

We can copy a trivial type into a char or unsigned char array and back. Alignment and the size of the type may differ 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 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 that takes the opportunity 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, 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 will notice 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 that does not use specific C++ features unavailable in C. Such a type is memcopy-able, and its layout is defined so 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.

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

  • No virtual base classes A;
  • No virtual functions B;
  • No reference-members C;
  • The same access control for all non-static data members D. 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 E;
  • All non-static data members are standard-layout as well F;
  • 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. Even though 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;  C 
};

struct NotStandardLayout : virtual Base,                   A 
                           public NotStandardLayoutBase {  E 

  virtual void Fun();  B Virtual function

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

POD

Before C++20, we had the definition of a POD type. The specification was that a POD is trivial type with a 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 certainly 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 (met by standard layout);
  • a POD supports static initialization (met 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 in 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 the two definitions for standard layout and trivial type. Consequently, 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 themselves. 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 can still 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 a 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
Aggregatemaybemaybe

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 exchanged 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 compilers or a different compiler on the other side rearrange data members. Then a receiver may have a different layout understanding than the sender.

[2023-12-12]: Updated the table, aggregates are not blindly memcopyable. Corrected spelling and various typos.

Andreas