본문으로 건너뛰기 C++ Copy Initialization: The = Form, explicit, and Copy

C++ Copy Initialization: The = Form, explicit, and Copy

C++ Copy Initialization: The = Form, explicit, and Copy

이 글의 핵심

C++ has multiple initialization syntaxes and they are not interchangeable. Copy initialization (T x = expr) differs from direct initialization (T x(expr)) in one critical way: it cannot use explicit constructors. Understanding this prevents confusing compile errors and implicit conversion surprises.

Three Ways to Initialize

C++ has multiple initialization syntaxes, and they are not equivalent:

int a = 10;    // copy initialization
int b(10);     // direct initialization
int c{10};     // list initialization (C++11+)

// For most built-in types, all three produce the same result.
// For class types, they differ in important ways.

This guide focuses on copy initialization — the = expr form — and how it differs from direct initialization.


What Copy Initialization Does

Copy initialization is used when you write:

T x = expr;

The compiler performs these steps:

  1. Evaluate expr, possibly creating a temporary of type T or a convertible type
  2. Use T’s copy constructor (or move constructor) to initialize x
  3. Elide the copy if possible (RVO / mandatory elision in C++17)
std::string s1 = "hello";   // copy init: creates string from const char*, no actual copy
std::string s2 = s1;        // copy init: copies s1 into s2

int x = 3.7;               // copy init: narrowing — converts 3.7 to 3 (compiles with warning)

explicit Constructors — The Key Difference

The critical difference between copy and direct initialization: copy initialization cannot use explicit constructors.

class Degrees {
public:
    explicit Degrees(double value) : value_(value) {}
private:
    double value_;
};

// Direct initialization — can use explicit constructors
Degrees d1(45.0);   // OK — explicitly constructing
Degrees d2{45.0};   // OK — list initialization

// Copy initialization — cannot use explicit constructor
Degrees d3 = 45.0;  // compile error: explicit constructor not usable here
Degrees d4 = Degrees(45.0);  // OK — creates temporary, then copy/move

Why does this matter? explicit constructors are meant to prevent accidental implicit conversions. Copy initialization is considered an implicit conversion context — so explicit blocks it.

Real-World Example

class FileDescriptor {
public:
    explicit FileDescriptor(int fd) : fd_(fd) {}
private:
    int fd_;
};

void processFile(FileDescriptor fd) { /* ... */ }

int rawFd = open("data.txt", O_RDONLY);

processFile(rawFd);             // compile error — no implicit int → FileDescriptor
processFile(FileDescriptor(rawFd));  // OK — explicit construction

// This prevents accidentally passing raw integers where FileDescriptor is expected

Copy Initialization Contexts

Copy initialization applies in more contexts than just variable declarations:

// 1. Variable declaration with =
std::string s = "hello";

// 2. Function argument passing (implicit conversion)
void process(std::string s);
process("hello");  // "hello" → std::string via copy init

// 3. Function return (before C++17 elision rules)
std::string getName() {
    return "Alice";  // copy init of return value
}

// 4. Initializing members in aggregate initialization
struct Config {
    std::string host;
    int port;
};
Config c = {"localhost", 8080};  // each member copy-initialized

// 5. Exception initialization
throw std::runtime_error("failed");  // copy init of exception object

Direct Initialization

Direct initialization uses T x(expr) or T x{expr}:

std::string s1("hello");   // direct init with const char*
std::string s2{s1};        // direct list init from string

class Widget {
public:
    explicit Widget(int size) : size_(size) {}
    explicit Widget(int size, int flags) : size_(size), flags_(flags) {}
private:
    int size_, flags_ = 0;
};

Widget w1(10);             // direct init — explicit OK
Widget w2{10};             // direct list init — explicit OK
Widget w3 = Widget(10);   // copy init of temporary — explicit OK (construction explicit)

// Widget w4 = 10;         // compile error — copy init of Widget from int, explicit blocks

Comparison Table

FeatureCopy init T x = vDirect init T x(v) / T x{v}
explicit single-arg constructor from valueNot selectedCan be selected
Narrowing conversions with {}Allowed with =Rejected with {}
Typical reading”x gets the value of v""construct x with v”
When to useReadable for same-type copiesExplicit construction
int x = 3.7;    // copy init: narrowing allowed, x = 3 (compiles with warning)
int y{3.7};     // list init: narrowing error — compile error

double d = 3;   // copy init: implicit int → double, fine
double e{3};    // list init: int → double, fine (no narrowing, just widening)

RVO and Copy Elision

Modern C++ eliminates most copies you might worry about.

Named Return Value Optimization (NRVO)

std::vector<int> buildRange(int n) {
    std::vector<int> result;
    for (int i = 0; i < n; i++) result.push_back(i);
    return result;  // compiler constructs result directly in caller's space — no copy
}

auto v = buildRange(1000);  // effectively zero copies

C++17 Mandatory Elision (Prvalues)

C++17 guarantees that prvalue temporaries are never copied or moved — they are constructed directly in their final location:

// In C++17, this is guaranteed to not copy or move
std::string s = std::string("hello") + " world";

// Even for types with deleted copy/move constructors:
class Uncopyable {
public:
    Uncopyable() = default;
    Uncopyable(const Uncopyable&) = delete;
    Uncopyable(Uncopyable&&) = delete;
};

Uncopyable u = Uncopyable();  // C++17: OK — mandatory elision, no copy needed
                               // C++14: compile error — copy constructor deleted

Non-Copyable Types

Types with deleted copy constructors cannot use copy initialization syntax:

#include <memory>

// unique_ptr is move-only — copy constructor is deleted
std::unique_ptr<int> p1 = std::make_unique<int>(42);  // OK in C++17 (elision)
std::unique_ptr<int> p2 = p1;    // compile error — cannot copy unique_ptr

// Move ownership explicitly
std::unique_ptr<int> p3 = std::move(p1);  // OK — move init

When to Use Each Form

Use copy initialization (=) for:

  • Copying from the same type: std::string s2 = s1
  • Readable scalar initialization: int count = 0
  • Return values (the compiler elides anyway): return value

Use direct list initialization ({}) for:

  • Class types where you want to prevent narrowing: int x{someDouble} will catch narrowing at compile time
  • Constructing with multiple arguments: std::vector<int> v{1, 2, 3, 4, 5}
  • When you want to be explicit about construction

Avoid mixing for consistency — choose one style for your codebase and stick to it. Many modern C++ codebases default to {} for local variables and = for copies from the same type.


Common Pitfalls

Unexpected Implicit Conversion

class Length {
public:
    Length(double meters) : meters_(meters) {}  // NOT explicit
private:
    double meters_;
};

void setDistance(Length d) { /* ... */ }

setDistance(5.0);    // OK — implicit conversion double → Length (maybe intentional)
setDistance(5);      // OK — implicit int → double → Length (maybe surprising)

// Fix: make the constructor explicit if implicit conversions are undesired
class LengthSafe {
public:
    explicit LengthSafe(double meters) : meters_(meters) {}
private:
    double meters_;
};

setDistance(5.0);           // now requires explicit construction
setDistance(LengthSafe(5.0));  // OK — caller makes conversion explicit

auto and Copy Initialization

auto x = 3.14;   // x is double — straightforward
auto y = {1, 2, 3};  // y is std::initializer_list<int> — surprising!

// Be explicit with auto
auto v = std::vector<int>{1, 2, 3};  // v is vector<int>
auto s = std::string{"hello"};       // s is string

Key Takeaways

  • Copy initialization is T x = expr — it invokes an implicit conversion sequence from expr to T
  • explicit constructors are excluded from copy initialization — use direct init or construct explicitly
  • Copy elision (RVO) and C++17 mandatory elision for prvalues mean most copy-init forms involve zero actual copies at runtime
  • List initialization ({}) prevents narrowing conversions at compile time — prefer it for class types
  • Non-copyable types (like std::unique_ptr) can use copy-init syntax in C++17 when the right side is a prvalue (mandatory elision)
  • Match constructor explicitness to intent: explicit if implicit conversion is surprising or dangerous

자주 묻는 질문 (FAQ)

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

A. C++ copy initialization explained: how T x = expr differs from direct initialization, why explicit blocks it, how RVO.

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

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

Q. 더 깊이 공부하려면?

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


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

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


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

C++, copy initialization, direct initialization, explicit, RVO 등으로 검색하시면 이 글이 도움이 됩니다.