C++ Uniform Initialization | Braces `{}` Explained

C++ Uniform Initialization | Braces `{}` Explained

이 글의 핵심

Uniform initialization lets you initialize any type with `{}` for consistent syntax, narrowing checks, and clearer object construction than parentheses alone.

What is uniform initialization?

Uniform initialization (C++11) is the consistent {} syntax for initializing values of any type: scalars, arrays, structs, classes, and containers.

// Before C++03: many styles
int x = 10;
int arr[] = {1, 2, 3};
std::vector<int> vec;
vec.push_back(1);

// C++11: uniform initialization
int x{10};
int arr[]{1, 2, 3};
std::vector<int> vec{1, 2, 3};

Why it matters:

  • Consistency: one syntax for many types
  • Safety: catches narrowing conversions
  • Clarity: avoids the Most Vexing Parse
  • Convenience: simpler container initialization

Comparison of styles:

StyleSyntaxNarrowingMost Vexing Parse
Copy initializationint x = 10;Allowed (often with warning)
Direct initializationint x(10);AllowedCan bite
Brace / uniformint x{10};Usually rejectedAvoided with {}

Basic usage

// Scalars
int x{10};
double y{3.14};
char c{'A'};

// Arrays
int arr[]{1, 2, 3, 4, 5};

// Struct
struct Point {
    int x, y;
};
Point p{10, 20};

// Class types
std::string s{"Hello"};
std::vector<int> vec{1, 2, 3};

Narrowing prevention

// ❌ Narrowing error
int x{3.14};  // compile error: double -> int

// ✅ Explicit conversion
int x{static_cast<int>(3.14)};

// Copy init may allow (with warnings)
int y = 3.14;  // OK (warning)

Narrowing in more detail:

Brace initialization often rejects these lossy conversions:

// ❌ Floating -> integer
double d = 3.14;
int x{d};  // error

// ❌ Large integer -> smaller
long long big = 1000000000000LL;
int y{big};  // error

// ❌ Integer -> float (precision loss)
int large = 16777217;
float f{large};  // error (not exactly representable as float)

// ❌ Signed -> unsigned (negative value)
int negative = -1;
unsigned int u{negative};  // error

// ✅ Explicit casts
int x2{static_cast<int>(d)};
int y2{static_cast<int>(big)};
float f2{static_cast<float>(large)};
unsigned int u2{static_cast<unsigned int>(negative)};

Practical use:

// ✅ Safer API parameters
void setVolume(int volume) {
    // volume: 0~100
}

double userInput = 75.5;
// setVolume({userInput});  // compile error (narrowing)
setVolume(static_cast<int>(userInput));  // explicit intent

// ✅ Parsing config values
int parsePort(const std::string& value) {
    double parsed = std::stod(value);
    // return {parsed};  // error: double -> int
    return static_cast<int>(parsed);
}

Fixing the Most Vexing Parse

class Widget {
public:
    Widget() {}
};

// ❌ Most Vexing Parse
Widget w();  // parsed as a function declaration!

// ✅ Brace initialization
Widget w{};  // object construction

What is the Most Vexing Parse?

Because () can mean both function declaration and construction, the compiler may interpret T x(); as declaring a function.

class Timer {
public:
    Timer() { std::cout << "Timer constructed\n"; }
};

int main() {
    Timer t();  // ❌ function declaration: Timer t();
    // t.start();  // error: t is a function
    
    Timer t2{};  // ✅ object
}

A subtler example:

#include <fstream>
#include <iterator>
#include <algorithm>

int main() {
    std::ifstream file("data.txt");
    
    // ❌ Most Vexing Parse
    std::vector<int> data(
        std::istream_iterator<int>(file),
        std::istream_iterator<int>()
    );
    // `data` is parsed as a function declaration!
    
    // ✅ Fix 1: braces
    std::vector<int> data{
        std::istream_iterator<int>(file),
        std::istream_iterator<int>()
    };
    
    // ✅ Fix 2: extra parentheses
    std::vector<int> data2(
        (std::istream_iterator<int>(file)),
        (std::istream_iterator<int>())
    );
}

Real-world examples

Example 1: containers

#include <vector>
#include <map>
#include <set>

int main() {
    std::vector<int> numbers{1, 2, 3, 4, 5};
    
    std::map<std::string, int> ages{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
    
    std::set<int> uniqueNumbers{5, 2, 8, 1, 9};
    
    std::vector<std::vector<int>> matrix{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
}

Example 2: structs and classes

struct Person {
    std::string name;
    int age;
    double height;
};

class Rectangle {
private:
    int width, height;
    
public:
    Rectangle(int w, int h) : width{w}, height{h} {}
    
    int area() const {
        return width * height;
    }
};

int main() {
    Person p{"Alice", 30, 165.5};
    Rectangle rect{10, 20};
    std::cout << rect.area() << std::endl;  // 200
}

Example 3: dynamic allocation

#include <memory>

int main() {
    int* ptr = new int{42};
    delete ptr;
    
    int* arr = new int[5]{1, 2, 3, 4, 5};
    delete[] arr;
    
    auto unique = std::make_unique<int>(42);
    auto shared = std::make_shared<std::string>("Hello");
    
    auto vec = std::make_unique<std::vector<int>>(
        std::initializer_list<int>{1, 2, 3, 4, 5}
    );
}

Example 4: return values

#include <vector>

std::vector<int> getNumbers() {
    return {1, 2, 3, 4, 5};
}

struct Point {
    int x, y;
};

Point getOrigin() {
    return {0, 0};
}

int main() {
    auto numbers = getNumbers();
    auto origin = getOrigin();
}

= vs {}

// Copy-style init
int x = 10;
int y = 3.14;  // narrowing allowed (warning)

// Braces
int a{10};
// int b{3.14};  // error: narrowing

std::string s1 = "Hello";  // OK
std::string s2{"Hello"};   // OK

// Direct vs brace for containers
std::vector<int> v1(10);    // 10 elements (default value)
std::vector<int> v2{10};    // 1 element with value 10

initializer_list precedence

class MyClass {
public:
    MyClass(int x) {
        std::cout << "int ctor\n";
    }
    
    MyClass(std::initializer_list<int> list) {
        std::cout << "initializer_list ctor\n";
    }
};

int main() {
    MyClass obj1(10);   // int ctor
    MyClass obj2{10};   // initializer_list ctor wins!
}

Common pitfalls

Pitfall 1: vector size vs element list

// ❌ Not what people sometimes expect
std::vector<int> v1(10);    // 10 elements (zero-initialized)
std::vector<int> v2{10};    // 1 element: 10

// ✅ Be explicit
std::vector<int> v3(10, 0);      // 10 elements, all 0
std::vector<int> v4{1, 2, 3};    // 3 elements

Pitfall 2: with auto

// ❌ Surprising type
auto x = {1, 2, 3};  // std::initializer_list<int>

// ✅ Name the container
std::vector<int> y = {1, 2, 3};

Pitfall 3: narrowing

int x{3.14};       // error
char c{1000};      // error (typically)

int x{static_cast<int>(3.14)};
char c{static_cast<char>(1000)};

Pitfall 4: empty braces

class MyClass {
public:
    MyClass() {
        std::cout << "default ctor\n";
    }
    
    MyClass(std::initializer_list<int> list) {
        std::cout << "initializer_list ctor\n";
    }
};

int main() {
    MyClass obj1;      // default ctor
    MyClass obj2{};    // default ctor
    MyClass obj3{{}};  // initializer_list ctor (empty list)
}

Member initialization

class MyClass {
private:
    int x{10};
    std::string s{"Hello"};
    std::vector<int> vec{1, 2, 3};
    
public:
    MyClass() = default;
    
    MyClass(int value) : x{value} {}
};

Aggregate-style initialization

struct Point {
    int x, y, z;
};

Point p1{1, 2, 3};
Point p2{1, 2};     // z value-initialized
Point p3{};         // all zero / value-init

struct Line {
    Point start, end;
};

Line line{{0, 0, 0}, {10, 10, 10}};

Pros and cons

// ✅ Pros
int x{10};
std::vector<int> vec{1, 2, 3};
// int y{3.14};  // error

Widget w{};  // object, not function

// ❌ Cons
std::vector<int> v{10};  // one element, not size 10
auto x = {1, 2, 3};      // initializer_list

Pros summary:

ProNotes
ConsistencySame {} for scalars and containers
SafetyNarrowing often rejected
ClarityWidget w{} is an object
Conveniencemap/vector init in one place

Cons summary:

ConMitigation
initializer_list wins for {}Sometimes use ()
vector size confusionvector<int>(n) for size
auto + = with {...}Prefer explicit types

Recommendations

// ✅ Good fits
std::vector<int> vec{1, 2, 3};
Point p{10, 20};
int x{value};  // when you want narrowing checks

// ⚠️ Use parentheses when clearer
std::vector<int> v(100);   // 100 elements
std::string s(10, 'x');    // ten 'x' characters

Patterns

Settings object

struct ServerConfig {
    std::string host = "localhost";
    int port = 8080;
    int maxConnections = 100;
    bool enableSSL = false;
};

ServerConfig cfg{
    "0.0.0.0",
    3000,
    500,
    true
};

Return struct

struct Result {
    bool success;
    std::string message;
    int code;
};

Result processRequest() {
    if (error) {
        return {false, "Error occurred", 500};
    }
    return {true, "Success", 200};
}

Member defaults

class Connection {
    std::string host_{"localhost"};
    int port_{8080};
    std::vector<std::string> options_{
        "keepalive=true",
        "timeout=30"
    };
    
public:
    Connection() = default;
    Connection(std::string host, int port) 
        : host_{std::move(host)}, port_{port} {}
};

FAQ

Q1: When should I use uniform initialization?

A: For listing elements in containers, when narrowing checks matter, and when you want a consistent style—while remembering vector and initializer_list rules.

Q2: () vs {}?

A: {}: narrowing checks, initializer_list ctor preference, MVP fix. (): usual ctor calls, e.g. vector size.

Q3: What is narrowing?

A: Conversions that lose information (e.g. doubleint, long longint).

Q4: What is the Most Vexing Parse?

A: Widget w(); looks like a function declaration. Use Widget w{};.

Q5: Does initializer_list always win?

A: For {}, the initializer_list constructor is strongly preferred when it exists.

std::vector<int> v1{10};  // one element 10
std::vector<int> v2(10);  // size 10

Q6: What does empty {} mean?

A: Value initialization: scalars to zero-like values; class types call the default constructor if available.

Q7: Performance?

A: Same as other initialization forms at runtime; no inherent overhead.

Q8: Further reading?

A: Scott Meyers, Effective Modern C++ (Item 7); cppreference — List initialization.

Related: List initialization, initializer_list, Value initialization.

In one sentence: Uniform initialization uses braces for consistent, often safer initialization and catches many narrowing mistakes.


  • C++ initializer_list
  • C++ initializer_list constructor
  • C++ call_once

Practical tips

  • Check compiler warnings first when something looks wrong.
  • Reproduce with a minimal example.

Performance

  • Do not optimize without profiling.
  • Define measurable goals first.

Code review

  • Match team conventions for () vs {}.

Checklist

Before coding

  • Is this the right tool for the problem?
  • Will teammates understand and maintain it?
  • Does it meet performance needs?

While coding

  • Warnings addressed?
  • Edge cases considered?
  • Error handling appropriate?

At review

  • Intent clear?
  • Tests sufficient?
  • Documented where needed?

Keywords

C++, uniform initialization, list initialization, narrowing, C++11.


  • C++ call_once
  • C++ Aggregate initialization
  • C++ Aggregate types
  • C++ async & launch