본문으로 건너뛰기 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

Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Value initialization uses empty () or {}. Scalars become zero-like; classes call the default constructor.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, value initialization, zero initialization, default initialization, C++11.