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:
- Evaluate
expr, possibly creating a temporary of typeTor a convertible type - Use
T’s copy constructor (or move constructor) to initializex - 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
| Feature | Copy init T x = v | Direct init T x(v) / T x{v} |
|---|---|---|
explicit single-arg constructor from value | Not selected | Can be selected |
Narrowing conversions with {} | Allowed with = | Rejected with {} |
| Typical reading | ”x gets the value of v" | "construct x with v” |
| When to use | Readable for same-type copies | Explicit 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 fromexprtoT explicitconstructors 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:explicitif 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와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [2026] C++ Copy Elision 심화 | RVO·NRVO·필수 생략·예외 안전
- C++ RVO/NRVO | ‘Return Value Optimization’ 가이드
- C++ explicit Keyword | ‘explicit 키워드’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, copy initialization, direct initialization, explicit, RVO 등으로 검색하시면 이 글이 도움이 됩니다.