The Complete Guide to C++20 Designated Initializers | Clear Struct Initialization
이 글의 핵심
C++20 designated initializers: .field = value syntax for aggregates—ordering rules vs C and mixing with positional init.
What Are Designated Initializers? Why Do We Need Them?
Problem Scenario: Confusion in Struct Initialization
The Problem: When a struct has many members, listing values in order can make it unclear which value corresponds to which member.
struct Config {
std::string host;
int port;
bool ssl;
int timeout;
int max_connections;
};
int main() {
// It's unclear which value corresponds to which member
Config cfg = {"localhost", 8080, true, 30, 100};
// Is port 8080? Is timeout 30?
}
The Solution: Designated Initializers (introduced in C++20) allow you to initialize struct members by explicitly specifying their names. This makes the code more readable and safe, even if the order of members changes.
int main() {
Config cfg = {
.host = "localhost",
.port = 8080,
.ssl = true,
.timeout = 30,
.max_connections = 100
};
// Clear and easy to read!
}
flowchart LR
subgraph before["Traditional Initialization"]
b1["Config cfg = {val1, val2, val3, val4, val5}"]
b2["Requires remembering the order"]
b3["Confusing"]
end
subgraph after["Designated Initializers"]
a1["Config cfg = {.host=..., .port=...}"]
a2["Explicit member names"]
a3["Clear"]
end
b1 --> b2 --> b3
a1 --> a2 --> a3
Table of Contents
- Basic Syntax
- Order Rules
- Nested Structs
- Default Values and Partial Initialization
- Common Errors and Solutions
- Production Patterns
- Complete Example: HTTP Request Configuration
1. Basic Syntax
Basic Usage
struct Point {
int x;
int y;
};
int main() {
// C++20 Designated Initializers
Point p1 = {.x = 10, .y = 20};
// Curly braces can be omitted
Point p2{.x = 30, .y = 40};
std::cout << p1.x << ", " << p1.y << '\n'; // 10, 20
}
Comparison with Traditional Initialization
struct Person {
std::string name;
int age;
std::string city;
};
int main() {
// Traditional initialization (order matters)
Person p1 = {"Alice", 30, "Seoul"};
// Designated Initializers (explicit member names)
Person p2 = {
.name = "Bob",
.age = 25,
.city = "Busan"
};
}
2. Order Rules
Must Follow Declaration Order
In C++20, Designated Initializers must follow the declaration order of struct members (unlike in C).
struct Data {
int a;
int b;
int c;
};
int main() {
// ✅ In declaration order
Data d1 = {.a = 1, .b = 2, .c = 3}; // OK
// ❌ Out of order
// Data d2 = {.b = 2, .a = 1, .c = 3}; // Error in C++20
// ✅ Skipping members is OK
Data d3 = {.a = 1, .c = 3}; // b = 0 (default value)
}
Differences Between C and C++20
| Feature | C | C++20 |
|---|---|---|
| Order | Flexible | Must follow declaration order |
| Mixing | Allowed | Not allowed |
| Arrays | Free index | Sequential only |
// Allowed in C
struct Data { int a, b, c; };
Data d = {.c = 3, .a = 1, .b = 2}; // C: OK, C++20: Error
3. Nested Structs
Nested Initialization
struct Address {
std::string city;
std::string street;
int zipcode;
};
struct Employee {
std::string name;
int id;
Address address;
};
int main() {
Employee emp = {
.name = "Alice",
.id = 100,
.address = {
.city = "Seoul",
.street = "Gangnam",
.zipcode = 12345
}
};
std::cout << emp.address.city << '\n'; // Seoul
}
Deep Nesting
struct Location {
double lat;
double lon;
};
struct Address {
std::string street;
Location location;
};
struct Person {
std::string name;
Address address;
};
int main() {
Person p = {
.name = "Bob",
.address = {
.street = "Main St",
.location = {
.lat = 37.5,
.lon = 127.0
}
}
};
}
4. Default Values and Partial Initialization
Providing Default Values
struct Settings {
int width = 800;
int height = 600;
bool fullscreen = false;
int fps = 60;
};
int main() {
// Specify some values, others use defaults
Settings s1 = {
.width = 1920,
.height = 1080
};
// fullscreen = false, fps = 60
Settings s2 = {
.fullscreen = true
};
// width = 800, height = 600, fps = 60
}
Empty Initialization
struct Point {
int x;
int y;
};
int main() {
Point p = {}; // x = 0, y = 0
}
5. Common Errors and Solutions
Error 1: Out of Order
Symptom: error: designator order for field does not match declaration order.
struct Data {
int a;
int b;
int c;
};
int main() {
// ❌ Out of order
// Data d = {.b = 2, .a = 1}; // Error
// ✅ Correct order
Data d = {.a = 1, .b = 2}; // OK
}
Error 2: Mixing Initializations
Symptom: error: cannot mix designated and non-designated initializers.
struct Point {
int x;
int y;
int z;
};
int main() {
// ❌ Mixing not allowed
// Point p = {10, .y = 20, .z = 30}; // Error
// ✅ Only designated
Point p1 = {.x = 10, .y = 20, .z = 30}; // OK
// ✅ Only traditional
Point p2 = {10, 20, 30}; // OK
}
Error 3: Non-Aggregate Types
Symptom: error: designated initializers cannot be used with a non-aggregate type.
Cause: Classes with constructors or private members are not aggregates.
// ❌ Has a constructor
class MyClass {
public:
MyClass(int x) : value(x) {}
int value;
};
// MyClass obj = {.value = 10}; // Error
// ✅ Aggregate (no constructor)
struct MyStruct {
int value;
};
MyStruct obj = {.value = 10}; // OK
6. Production Patterns
Pattern 1: Configuration Structs
struct ServerConfig {
std::string host = "0.0.0.0";
int port = 8080;
bool ssl = false;
int timeout_ms = 30000;
int max_connections = 1000;
std::string log_level = "info";
};
ServerConfig load_config() {
// Use defaults and override some
return ServerConfig{
.host = "localhost",
.port = 9000,
.ssl = true
// Others use defaults
};
}
Pattern 2: Replacing the Builder Pattern
// Before: Builder Pattern
class ConfigBuilder {
public:
ConfigBuilder& setHost(std::string h) { host = h; return *this; }
ConfigBuilder& setPort(int p) { port = p; return *this; }
Config build() { return Config{host, port, ...}; }
private:
std::string host;
int port;
};
// After: Designated Initializers (simpler)
struct Config {
std::string host = "localhost";
int port = 8080;
bool ssl = false;
};
Config cfg = {
.host = "example.com",
.port = 443,
.ssl = true
};
Pattern 3: Test Data
struct TestCase {
std::string name;
int input;
int expected;
bool should_throw = false;
};
std::vector<TestCase> tests = {
{.name = "zero", .input = 0, .expected = 0},
{.name = "positive", .input = 5, .expected = 25},
{.name = "negative", .input = -3, .expected = 9},
{.name = "overflow", .input = 999999, .expected = 0, .should_throw = true}
};
void run_tests() {
for (const auto& test : tests) {
std::cout << "Running test: " << test.name << '\n';
// ... run test ...
}
}
Pattern 4: Optional Parameters
struct RenderOptions {
int width = 800;
int height = 600;
bool antialiasing = true;
int samples = 4;
std::string output_format = "png";
};
void render(const std::string& scene, const RenderOptions& options = {}) {
std::cout << "Rendering " << scene
<< " at " << options.width << "x" << options.height << '\n';
}
int main() {
// Use all defaults
render("scene1.obj");
// Override some options
render("scene2.obj", {.width = 1920, .height = 1080});
// Override more options
render("scene3.obj", {
.width = 3840,
.height = 2160,
.samples = 8,
.output_format = "exr"
});
}
Pattern 5: Factory Functions
struct DatabaseConfig {
std::string host = "localhost";
int port = 5432;
std::string database;
std::string user;
std::string password;
int pool_size = 10;
bool ssl = false;
};
// Factory with sensible defaults
DatabaseConfig create_dev_config() {
return {
.database = "dev_db",
.user = "dev_user",
.password = "dev_pass"
// Others use defaults
};
}
DatabaseConfig create_prod_config() {
return {
.host = "prod.example.com",
.database = "prod_db",
.user = "prod_user",
.password = "secure_pass",
.pool_size = 50,
.ssl = true
};
}
7. Complete Example: HTTP Request Configuration
#include <string>
#include <map>
#include <chrono>
#include <iostream>
struct HttpHeaders {
std::string content_type = "application/json";
std::string authorization;
std::string user_agent = "MyApp/1.0";
};
struct HttpRequest {
std::string method = "GET";
std::string url;
HttpHeaders headers;
std::string body;
std::chrono::seconds timeout = std::chrono::seconds(30);
bool follow_redirects = true;
int max_redirects = 5;
};
void send_request(const HttpRequest& req) {
std::cout << req.method << " " << req.url << '\n';
std::cout << "Content-Type: " << req.headers.content_type << '\n';
std::cout << "Timeout: " << req.timeout.count() << "s\n";
}
int main() {
// GET request (using defaults)
HttpRequest get_req = {
.url = "https://api.example.com/users"
};
send_request(get_req);
// POST request (overriding some values)
HttpRequest post_req = {
.method = "POST",
.url = "https://api.example.com/users",
.headers = {
.content_type = "application/json",
.authorization = "Bearer token123"
},
.body = R"({"name":"Alice","age":30})",
.timeout = std::chrono::seconds(60)
};
send_request(post_req);
}
Performance Considerations
Zero Overhead
Designated initializers have zero runtime overhead compared to traditional initialization. They are purely a compile-time feature.
// Traditional
Config c1 = {"localhost", 8080, true};
// Designated
Config c2 = {.host = "localhost", .port = 8080, .ssl = true};
// Both compile to identical assembly code
Compile-Time Verification
Designated initializers provide compile-time safety:
struct Point {
int x;
int y;
};
// ❌ Typo in member name
// Point p = {.x = 10, .z = 20}; // Error: no member named 'z'
// ✅ Compiler catches the error
Point p = {.x = 10, .y = 20}; // OK
When to Use
| Scenario | Traditional | Designated |
|---|---|---|
| Few members (2-3) | OK | Optional |
| Many members (4+) | Confusing | Recommended |
| Default values | Verbose | Clean |
| Partial initialization | Error-prone | Safe |
| Readability | Low | High |
Recommendation: Use designated initializers for:
- Configuration structs
- API parameters
- Test data
- Any struct with 4+ members
Memory Layout
Designated initializers do not affect memory layout or size:
struct Data {
int a;
int b;
int c;
};
Data d1 = {1, 2, 3};
Data d2 = {.a = 1, .b = 2, .c = 3};
// sizeof(d1) == sizeof(d2)
// Memory layout is identical
Summary
| Concept | Description |
|---|---|
| Designated Initializers | Initialize struct members by name |
| Syntax | {.member = value} |
| Order | Must follow declaration order (C++20) |
| Mixing | Cannot mix designated and traditional initializations |
| Use Cases | Configurations, test data, API parameters |
Designated Initializers significantly improve code readability and make struct initialization clearer.
FAQ
Q1: What are the differences between C and C++20?
A: In C, the order of designated initializers is flexible, and skipping members is allowed. In C++20, you must follow the declaration order, and mixing designated and traditional initializations is not allowed.
Q2: What happens if I don’t follow the order?
A: A compile-time error will occur. You must specify members in the same order they are declared.
Q3: What if I only initialize some members?
A: The remaining members will be initialized to their default values (if provided) or zero.
Q4: Can I use Designated Initializers with classes?
A: Only Aggregate types support Designated Initializers. Classes with constructors or private members are not aggregates.
Q5: What about arrays?
A: In C++20, Designated Initializers for arrays are limited. You can only initialize elements sequentially, and skipping indices is not allowed.
Q6: Can I use designated initializers with inheritance?
A: No, designated initializers only work with aggregate types. Classes with base classes are not aggregates.
struct Base { int x; };
struct Derived : Base { int y; };
// ❌ Error: Derived is not an aggregate
// Derived d = {.x = 10, .y = 20};
// ✅ Use composition instead
struct Derived {
Base base;
int y;
};
Derived d = {.base = {.x = 10}, .y = 20};
Q7: What about performance?
A: Designated initializers have zero runtime overhead. They are a compile-time feature that improves readability without affecting performance.
Q8: Can I use designated initializers with std::array or std::vector?
A: No, designated initializers only work with aggregate types (plain structs/arrays). Use traditional initialization for STL containers.
// ❌ Error: std::array is not a plain array
// std::array<int, 3> arr = {.[0] = 1, .[1] = 2};
// ✅ Traditional initialization
std::array<int, 3> arr = {1, 2, 3};
Q9: How do I migrate existing code?
A: Gradually replace traditional initialization with designated initializers:
- Start with configuration structs
- Add default values to members
- Update initialization sites one by one
- Enable
-Wc++20-designatorwarning to catch issues
Q10: Where can I learn more about Designated Initializers?
A:
- cppreference - Aggregate initialization
- “C++20: The Complete Guide” by Nicolai Josuttis
- C++20 Features
Related posts: Aggregate Initialization, Uniform Initialization, List Initialization.
One-line summary: C++20 Designated Initializers make struct initialization clear and safe by explicitly naming members, with zero runtime overhead.
## 같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ Aggregate Initialization 완벽 가이드 | 집합 초기화](/blog/cpp-aggregate/)
- [C++ 균일 초기화 | "Uniform Initialization" 가이드](/blog/cpp-uniform-initialization/)
- [C++ List Initialization | "리스트 초기화" 가이드](/blog/cpp-list-initialization/)
---
---
## 이 글에서 다루는 키워드 (관련 검색어)
C++, initializer, cpp20, struct, syntax, aggregate 등으로 검색하시면 이 글이 도움이 됩니다.