C++ Aggregate Initialization and Aggregate Types

Aggregate initialization is a form of list-initialization in C++ that allows direct, member-by-member initialization of arrays and simple class types without requiring an explicit constructor. This article explains the mechanics of aggregate initialization, demonstrates how it works in modern C++, and outlines the strict criteria a class, struct, or union must satisfy to qualify as an aggregate across modern C++ standards (C++11 through C++20).

What is Aggregate Initialization?

Aggregate initialization initializes the elements of an array or the members of an aggregate class using a braced-init-list:

struct Point {
    int x;
    int y;
};

Point p = {10, 20}; // Aggregate initialization

When aggregate initialization takes place:

  1. Members are initialized in declaration order.
  2. If the number of initializer clauses is fewer than the number of members, the remaining members are value-initialized (numeric types become zero, objects are default-constructed).
  3. Supplying more initializers than members results in a compile-time error.

Starting in C++20, designated initializers are supported, enabling explicit member naming:

Point p = {.x = 10, .y = 20};

Designated initializers must follow the declaration order of the members and cannot be mixed with non-designated initializers for different fields.

What Qualifies as an Aggregate Class?

In modern C++, an aggregate is defined primarily by what it does not have. The rules have evolved across standards to become more flexible while maintaining predictable layout and construction semantics.

As of modern C++ (C++17 and C++20), a class type (class, struct, or union) is an aggregate if it meets the following criteria:

1. No User-Declared Constructors (C++20)

In C++11 through C++17, the rule forbade user-provided constructors, meaning a class with = default or = delete constructors could sometimes still qualify. C++20 tightened this rule: an aggregate must have no user-declared or inherited constructors whatsoever, even if marked = default or = delete.

struct NotAggregate {
    int a;
    NotAggregate() = default; // Disqualifies the class as an aggregate in C++20
};

2. No Private or Protected Non-Static Data Members

All direct non-static data members must be public. Private or protected static members do not disqualify a class because static members are not initialized via aggregate initialization.

struct ValidAggregate {
    int x;
    int y;
private:
    static int shared_count; // Allowed
};

3. No Virtual Functions

An aggregate cannot define or inherit any virtual member functions. The presence of a virtual function introduces a virtual method table pointer (vptr), which disrupts straightforward member initialization.

4. No Virtual, Private, or Protected Base Classes

Since C++17, an aggregate may derive from other classes, provided that:

  • The base classes are public.
  • The base classes are non-virtual.

When initializing an aggregate with base classes, the base classes are initialized first, treated as subaggregates:

struct Base {
    int a;
};

struct Derived : Base {
    int b;
};

Derived d = {{1}, 2}; // C++17 aggregate initialization with base class

5. In-Class Member Initializers are Allowed (C++14 Onward)

In C++11, default member initializers disqualified a type from being an aggregate. C++14 removed this restriction. Default member values are used when an explicit value is omitted from the initializer list:

struct Config {
    int timeout = 3000;
    bool debug = false;
};

Config c = {.debug = true}; // timeout remains 3000 in C++20

How to Verify Aggregate Status at Compile Time

The Standard Library provides a type trait to check whether a type satisfies aggregate criteria:

#include <type_traits>

struct Data {
    int id;
    double value;
};

static_assert(std::is_aggregate_v<Data>, "Data must be an aggregate");

std::is_aggregate evaluates to true if the type meets the language rules for aggregates under the active compiler standard.