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 initializationWhen aggregate initialization takes place:
- Members are initialized in declaration order.
- 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).
- 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 class5. 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++20How 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.