본문으로 건너뛰기
Previous
Next
C++ Value Initialization | Empty {} and ()

C++ Value Initialization | Empty {} and ()

C++ Value Initialization | Empty {} and ()

이 글의 핵심

Value initialization uses empty () or {}. Scalars become zero-like; classes call the default constructor. Differs from default initialization for locals; compares with zero initialization.

The Initialization Problem

C++ has multiple initialization rules, and the wrong one leads to undefined behavior from reading indeterminate values. Understanding when you get a guaranteed zero vs garbage is essential.

int a;    // default initialization — indeterminate value (garbage)!
int b{};  // value initialization — guaranteed 0
int c = 0; // copy initialization — 0

// Reading a is undefined behavior if it was never assigned
std::cout << a;  // could print anything, crash, or worse
std::cout << b;  // always prints 0

Value initialization is the mechanism that gives you a safe, predictable starting value.


When Value Initialization Happens

Value initialization is triggered by empty parentheses or empty braces:

// Variable declaration with empty braces
int x{};          // value init → 0
double d{};       // value init → 0.0
int* ptr{};       // value init → nullptr
bool flag{};      // value init → false

// Temporary (prvalue) with empty parens or braces
int temp = int(); // value init → 0
int temp2 = int{}; // value init → 0 (same result)

// new expression with empty parens
int* heap = new int();   // value init → 0
int* heap2 = new int{};  // value init → 0

// Arrays
int arr[5]{};   // all elements value-initialized → all zero
int* dynArr = new int[5]();  // all zero

// Class member with default member initializer using {}
class Counters {
    int hits{};        // value-initialized when Counters is constructed
    int misses{};      // value-initialized
    double ratio{};    // 0.0
public:
    Counters() = default;  // uses the member initializers above
};

Value Initialization Rules by Type

The behavior depends on the type:

TypeValue initialization result
Scalar (int, double, pointer, bool)Zero (0, 0.0, nullptr, false)
ArrayEach element is value-initialized
Class with user-provided constructorDefault constructor is called
Aggregate (no user constructor)Zero-initialized, then default constructor if any
Class with no user-provided constructorZero-initialized first
// Scalar
int n{};        // 0
double d{};     // 0.0
char* p{};      // nullptr
bool b{};       // false

// Aggregate
struct Point { int x, y; };
Point pt{};     // x=0, y=0 — both zero-initialized

// Class with constructor
class Widget {
    int id_;
    std::string name_;
public:
    Widget() : id_(0), name_("default") {}
};
Widget w{};  // default constructor called: id=0, name="default"

// Array
int arr[4]{};  // {0, 0, 0, 0}

Default Initialization vs Value Initialization

This is the most practically important distinction:

// Local variables — default initialization
int local;            // indeterminate — do NOT read without assigning
double ratio;         // indeterminate
int* ptr;             // indeterminate (not nullptr!)

// Local variables — value initialization
int safeLocal{};      // 0
double safeRatio{};   // 0.0
int* safePtr{};       // nullptr

// Static/global variables — always default-initialized to zero
static int count;     // 0 — static storage is zero-initialized
int globalCount;      // 0 — same

The rule to remember: for local scalar variables, T x; is indeterminate; T x{}; is zero.

void process() {
    int counter;      // WARNING: indeterminate
    counter++;        // undefined behavior — reading uninitialized
    
    int safeCounter{};  // 0
    safeCounter++;      // OK — well-defined: 1
}

{} vs () for Value Initialization

Both T{} and T() trigger value initialization, but they differ in two ways:

Narrowing Conversions

double pi = 3.14159;

int a(pi);   // OK — narrowing truncates: a = 3 (compiles with warning)
int b{pi};   // Error — narrowing conversion rejected at compile time

{} catches narrowing at compile time, which prevents accidental data loss.

Most Vexing Parse

struct Widget { Widget() {} };

Widget w1();   // PROBLEM: this is a function declaration, not a variable!
               // "w1 is a function that takes no args and returns Widget"

Widget w2{};   // Correct: value-initialized Widget object
Widget w3;     // Also OK: default-initialized Widget object (same here, constructor called)

The Most Vexing Parse is a notorious C++ ambiguity. Using {} eliminates it.

initializer_list Ambiguity

std::vector<int> v1(5, 0);   // 5 elements, all zero: {0,0,0,0,0}
std::vector<int> v2{5, 0};   // 2 elements: {5, 0}

When a class has an initializer_list constructor, {} prefers it. For std::vector, {5, 0} means a vector with elements 5 and 0, not 5 zeros. Use () when you want the non-initializer_list constructor.


new T() vs new T

For heap allocations, the distinction between value and default initialization matters:

// Default initialization — value is indeterminate for scalars
int* p1 = new int;    // *p1 is indeterminate
int* p2 = new int[5]; // all 5 elements indeterminate

// Value initialization — scalars become zero
int* p3 = new int();     // *p3 == 0
int* p4 = new int{};     // *p4 == 0 (same)
int* p5 = new int[5]();  // all zero
int* p6 = new int[5]{};  // all zero

// For class types with user constructors — same either way
Widget* w1 = new Widget;   // default constructor called
Widget* w2 = new Widget(); // default constructor called

Use new T() or new T{} consistently when you want zero-initialized memory, even if you’ll overwrite it immediately — it avoids accidentally reading indeterminate values.

In modern C++, prefer smart pointers:

auto p = std::make_unique<int>();    // *p == 0
auto arr = std::make_unique<int[]>(5); // all 5 elements zero

Value Initialization in Containers

When standard containers create new elements, they value-initialize them:

#include <vector>
#include <iostream>

int main() {
    // resize adds value-initialized elements
    std::vector<int> v;
    v.resize(5);
    for (int x : v) std::cout << x << ' ';  // 0 0 0 0 0
    std::cout << '\n';

    // Constructor with count — also value-initializes
    std::vector<double> d(3);  // {0.0, 0.0, 0.0}

    // insert/emplace don't value-initialize — you provide the value
    v.push_back(42);  // adds 42
}

Member Default Member Initializers

C++11 lets you provide default values for members at the declaration site. These are used during value initialization:

class Connection {
    int fd_ = -1;            // default member initializer
    bool connected_ = false;
    std::string host_;       // default-constructed (empty string)
    int retries_ = 3;

public:
    Connection() = default;  // uses all default member initializers
    explicit Connection(std::string host, int fd)
        : fd_(fd), host_(std::move(host)), connected_(true) {}
};

Connection c1;           // fd=-1, connected=false, host="", retries=3
Connection c2("db", 5);  // fd=5, connected=true, host="db", retries=3

Default member initializers with {} (or = value) are the modern way to ensure members are always initialized to a known state.


Comparison Table

FormLocal intLocal classComment
int x;IndeterminateDefault ctorUnsafe for scalars
int x{};0Default ctorSafe, preferred
int x = 0;0Copy initClear intent for scalars
int x(0);0Matching ctorOK but avoid MVP with no args
new int;IndeterminateDefault ctorUnsafe for scalars
new int();0Default ctorSafe

Key Takeaways

  • T x{}; triggers value initialization — scalars become zero, classes call default constructor
  • T x; (no initializer) for a local scalar is default initialization — the value is indeterminate (undefined behavior if read)
  • Static and global variables are always zero-initialized regardless of how they’re declared
  • {} prevents narrowing conversions at compile time and avoids the Most Vexing Parse — prefer it over () for local variables
  • vector::resize, new T(), and container constructors with a count all value-initialize elements
  • Default member initializers (int x = 0; or int x{}; in class body) ensure members are always initialized even with the default constructor

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Value initialization uses empty () or {}. Scalars become zero-like; classes call the default constructor. Differs from d… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [C++ Copy Initialization: The = Form, explicit, and Copy](/en/blog/cpp-copy-initialization/
  • [C++ Designated Initializers (C++20)](/en/blog/cpp-designated-initializers/
  • [C++ Functions: Parameters, Return Values, Overloading, and](/en/blog/cpp-function-basics/

이 글에서 다루는 키워드 (관련 검색어)

C++, value initialization, zero initialization, default initialization, C++11 등으로 검색하시면 이 글이 도움이 됩니다.