A simple struct is often just a small bundle of related data.
When you create one, you usually want its members to start with meaningful values immediately. Aggregate initialization lets you do that in one statement, instead of creating the object first and assigning each member afterward.
Three things to take away:
- A simple data-only
structis usually an aggregate. - Aggregate initialization initializes members directly using braces.
- Missing members use their default member initializers or value-initialization.
The weaker alternative: create, then assign
Without aggregate initialization, you might write this:
#include <iostream>
struct SensorReading {
int sensorId {};
double temperatureC {};
double humidityPercent {};
};
int main(){
SensorReading reading {};
reading.sensorId = 12;
reading.temperatureC = 24.6;
reading.humidityPercent = 58.2;
std::cout << "Sensor: " << reading.sensorId << '\n';
std::cout << "Temperature: " << reading.temperatureC << " C\n";
std::cout << "Humidity: " << reading.humidityPercent << "%\n";
}
Terminal output:
Sensor: 12
Temperature: 24.6 C
Humidity: 58.2%
This works however it’s clunky. You initialize the struct, access the members individually then assign them values
Aggregate initialization merges those steps. 2 birds. 1 stone.
What is an aggregate?
An aggregate is usually a simple class or struct that mainly contains public data members and has no user-declared constructors.
For example:
struct SensorReading {
int sensorId {};
double temperatureC {};
double humidityPercent {};
};
This is an aggregate.
- No user-declared constructors
- No private or protected direct non-static data members
- No virtual functions
A plain data-only struct usually qualifies.
That means it can be initialized directly with a braced list.
Aggregate initialization
Aggregate initialization uses braces to initialize the members in order:
#include <iostream>
struct SensorReading {
int sensorId {};
double temperatureC {};
double humidityPercent {};
};
int main(){
SensorReading reading { 12, 24.6, 58.2 };
std::cout << "Sensor: " << reading.sensorId << '\n';
std::cout << "Temperature: " << reading.temperatureC << " C\n";
std::cout << "Humidity: " << reading.humidityPercent << "%\n";
}
Terminal output:
Sensor: 12
Temperature: 24.6 C
Humidity: 58.2%
This line does the important work:
SensorReading reading { 12, 24.6, 58.2 };
The values are assigned to the members in declaration order:
sensorId ← 12
temperatureC ← 24.6
humidityPercent ← 58.2
Missing initializers
You do not have to provide every member.
If a member is omitted, C++ initializes it using either:
- The member’s default member initializer, if it has one.
- Value-initialization, if it does not.
Example:
#include <iostream>
#include <string>
struct DownloadJob {
int id {};
std::string filename { "untitled.bin" };
bool compressed {};
};
int main(){
DownloadJob job { 42 };
std::cout << "ID: " << job.id << '\n';
std::cout << "File: " << job.filename << '\n';
std::cout << "Compressed: " << job.compressed << '\n';
}
Terminal output:
ID: 42
File: untitled.bin
Compressed: 0
Only id was explicitly initialized:
DownloadJob job { 42 };
The remaining members are initialized like this:
filename ← "untitled.bin" // default member initializer
compressed ← false // value-initialization
For a bool, value-initialization produces false, which prints as 0 by default.
Empty braces
Empty braces initialize every member safely:
#include <iostream>
#include <string>
struct DownloadJob {
int id {};
std::string filename { "untitled.bin" };
bool compressed {};
};
int main(){
DownloadJob job {};
std::cout << "ID: " << job.id << '\n';
std::cout << "File: " << job.filename << '\n';
std::cout << "Compressed: " << job.compressed << '\n';
}
Terminal output:
ID: 0
File: untitled.bin
Compressed: 0
This is a useful habit:
DownloadJob job {};
It avoids uninitialized members.
If a member has a default member initializer, that initializer is used. If not, the member is value-initialized.
For numeric types, value-initialization produces zero.
C++20 designated initializers
C++20 introduced designated initializers for aggregates.
They let you name the members being initialized:
#include <iostream>
struct SensorReading {
int sensorId {};
double temperatureC {};
double humidityPercent {};
};
int main(){
SensorReading reading {
.sensorId = 12,
.temperatureC = 24.6,
.humidityPercent = 58.2
};
std::cout << "Sensor: " << reading.sensorId << '\n';
std::cout << "Temperature: " << reading.temperatureC << " C\n";
std::cout << "Humidity: " << reading.humidityPercent << "%\n";
}
Terminal output:
Sensor: 12
Temperature: 24.6 C
Humidity: 58.2%
This is more verbose, but it makes the mapping explicit:
.temperatureC = 24.6
Designated initializers are useful when:
- The struct has several members.
- Multiple members have the same type.
- You want the initialization to be self-documenting.
Takeaway
Aggregate initialization lets you create a simple struct and initialize its members in one statement:
SensorReading reading { 12, 24.6, 58.2 };
This is more elegant than creating the object first and assigning members afterward.