복사 초기화

복사 초기화 (Copy initialization)

객체를 다른 객체로부터 초기화하는 가장 자연스러운 방식 중 하나가 복사 초기화예요. 초기화식이 같은 타입이거나 그 타입으로 변환될 수 있을 때, =를 써서 객체를 만들어요.

출처: cppreference

본문

다른 객체로부터 객체를 초기화해요.

구문 (Syntax)

구문 번호
T object = other; (1)
T object = {other}; (2) (until C++11)
f(other) (3)
return other; (4)
throw object; catch (T object) (5)
T array[N] = {other-sequence}; (6)

설명 (Explanation)

복사 초기화는 다음 상황에서 수행돼요:

  1. 비참조 타입 T의 이름 있는 변수(자동, 정적, 또는 스레드 로컬)가 등호 다음에 표현식이 오는 초기화식으로 선언될 때.
  2. (until C++11) 스칼라 타입 T의 이름 있는 변수가 등호 다음에 중괄호로 둘러싸인 표현식이 오는 초기화식으로 선언될 때 (참고: C++11부터는 이것이 목록 초기화로 분류되고, 축소 변환은 허용되지 않아요).
  3. 인자를 함수에 값으로 전달할 때.
  4. 값을 반환하는 함수에서 반환할 때.
  5. 예외를 값으로 던지거나 잡을 때.
  6. 집합체 초기화의 일부로, 초기화식이 제공된 각 요소를 초기화할 때.

복사 초기화의 효과는 다음과 같아요:

참고 (since)
먼저, T가 클래스 타입이고 초기화식이 cv-unqualified 타입이 T와 같은 클래스인 prvalue 표현식이면, 그 초기화식 자체를(실체화된 임시가 아니라) 목적지 객체 초기화에 사용한다: 복사 생략을 참고. (since C++17)
  • 그 외에는, (since C++17)T가 클래스 타입이고 other 타입의 cv-unqualified 버전이 T이거나 T에서 파생된 클래스이면, T의 비-explicit 생성자를 조사하고 오버로드 해석으로 최적 일치를 선택해요. 그 생성자를 호출해 객체를 초기화해요.
  • 그 외에는, T가 클래스 타입이고 other 타입의 cv-unqualified 버전이 T 또는 T에서 파생된 것이 아니거나, T가 비클래스 타입이지만 other 타입이 클래스 타입이면, other 타입에서 T로 변환할 수 있는(또는 T가 클래스 타입이고 변환 함수를 사용할 수 있으면 T에서 파생된 타입으로) 사용자 정의 변환을 조사하고 오버로드 해석으로 최적을 선택해요. 변환 생성자가 사용되었으면 변환 결과(변환 생성자를 썼다면 T의 cv-unqualified 버전의 rvalue 임시(until C++11)prvalue 임시(since C++11)(until C++17)prvalue 표현식(since C++17))로 객체를 직접 초기화해요. 마지막 단계는 보통 최적화로 제거되고 변환 결과가 대상 객체에 할당된 메모리에 직접 생성되지만, 적절한 생성자(이동 또는 복사)는 사용되지 않더라도 접근 가능해야 해요.(until C++17)
  • 그 외에는 (Tother 타입도 클래스 타입이 아니면), 필요하다면 표준 변환을 사용해 other의 값을 T의 cv-unqualified 버전으로 변환해요.

참고 (Notes)

복사 초기화는 직접 초기화보다 덜 관대해요: explicit 생성자는 변환 생성자가 아니므로 복사 초기화에서 고려되지 않아요.

struct Exp { explicit Exp(const char*) {} }; // not convertible from const char*
Exp e1("abc");  // OK
Exp e2 = "abc"; // Error, copy-initialization does not consider explicit constructor

struct Imp { Imp(const char*) {} }; // convertible from const char*
Imp i1("abc");  // OK
Imp i2 = "abc"; // OK

또한 복사 초기화에서의 암시적 변환은 초기화식에서 T를 직접 만들어야 해요. 반면 직접 초기화는 초기화식에서 T 생성자의 인자로의 암시적 변환을 기대해요.

struct S { S(std::string) {} }; // implicitly convertible from std::string
S s("abc");   // OK: conversion from const char[4] to std::string
S s = "abc";  // Error: no conversion from const char[4] to S
S s = "abc"s; // OK: conversion from std::string to S

other가 rvalue 표현식이면, 오버로드 해석이 이동 생성자를 선택해 복사 초기화 중에 호출해요. 이것도 여전히 복사 초기화로 간주돼요. 이 경우를 위한 특별한 용어(예: 이동 초기화)는 없어요.

암시적 변환은 복사 초기화 관점에서 정의돼요: T 타입의 객체가 표현식 E로 복사 초기화될 수 있으면, ET로 암시적으로 변환 가능해요.

이름 있는 변수의 복사 초기화에서의 등호 =는 대입 연산자와 관련이 없어요. 대입 연산자 오버로드는 복사 초기화에 아무 영향을 주지 않아요.

예제 (Example)

#include <memory>
#include <string>
#include <utility>
 
struct A
{
    operator int() { return 12;}
};
 
struct B
{
    B(int) {}
};
 
int main()
{
    std::string s = "test";        // OK: constructor is non-explicit
    std::string s2 = std::move(s); // this copy-initialization performs a move
 
//  std::unique_ptr<int> p = new int(1); // error: constructor is explicit
    std::unique_ptr<int> p(new int(1));  // OK: direct-initialization
 
    int n = 3.14;    // floating-integral conversion
    const int b = n; // const doesn't matter
    int c = b;       // ...either way
 
    A a;
    B b0 = 12;
//  B b1 = a;       // < error: conversion from 'A' to non-scalar type 'B' requested
    B b2{a};        // < identical, calling A::operator int(), then B::B(int)
    B b3 = {a};     // <
    auto b4 = B{a}; // <
    
//  b0 = a;         // < error, assignment operator overload needed

    [](...){}(c, b0, b3, b4); // pretend these variables are used
}

std::unique_ptrexplicit 생성자라서 = new int(1) 같은 복사 초기화가 안 되지만, 직접 초기화 p(new int(1))은 돼요. A aoperator int() 변환을 거쳐 B b2{a};처럼 목록 초기화에서는 가능하죠.

더 알아보기 (Learn more)

  • 복사 생략 — 복사·이동 생성이 생략될 수 있는 상황.
  • 변환 생성자 — 암시적 변환을 일으키는 생성자.
  • 복사 대입, 복사 생성자, 기본 생성자, 소멸자, explicit.
  • 각 초기화 방식 (직접/목록/값/기본/제로 초기화 등).
  • 이동 대입, 이동 생성자.