복사 생략
복사 생략 (Copy elision)
같은 타입(단 cv 한정 무시)의 원본 객체로부터 클래스 객체의 생성이 생략될 수 있어요. 선택된 생성자나 소멸자가 부수 효과를 갖더라도요. 이 객체 생성의 생략을 복사 생략(copy elision)이라고 불러요.
출처: cppreference
본문
특정 기준을 충족하면, 선택된 생성자 및/또는 소멸자가 부수 효과를 갖더라도 같은 타입(단 cv 한정 무시)의 원본 객체로부터 클래스 객체의 생성을 생략할 수 있어요. 이 객체 생성의 생략을 복사 생략이라고 해요.
설명 (Explanation)
복사 생략은 다음 상황에서 허용돼요 (여러 사본을 제거하기 위해 결합될 수 있음):
- 클래스 반환 타입을 갖는 함수의
return문에서, 피연산자가 자동 저장 기간을 갖는 비-volatile객체obj의 이름일 때(함수 매개변수나 처리기 매개변수 제외), 결과 객체의 복사-초기화를 생략하고obj를 함수 호출의 결과 객체에 직접 생성할 수 있어요. 이 복사 생략 변형을 명명된 반환 값 최적화(NRVO, named return value optimization)라고 불러요. - 같은 타입(단 cv 한정 무시)의 클래스 객체 대상이 참조에 바인딩되지 않은 임시 클래스 객체
obj로 초기화될 때,obj를 대상에 직접 생성함으로써 초기화를 생략할 수 있어요. 이 복사 생략 변형을 무명 반환 값 최적화(URVO, unnamed return value optimization)라고 불러요. C++17부터 URVO는 필수이며 더 이상 복사 생략의 한 형태로 간주되지 않아요. 아래를 참고하세요. | (until C++17) throw표현식에서, 피연산자가 (존재한다면) 가장 안쪽의 둘러싸는try블록을 포함하지 않는 스코프에 속하는 자동 저장 기간을 갖는 비-volatile객체obj의 이름일 때, 예외 객체의 복사-초기화를 생략하고obj를 예외 객체에 직접 생성할 수 있어요. 처리기에서, 처리기 인자를 위한 생성자와 소멸자의 실행 외에는 프로그램의 의미가 바뀌지 않는다면, 처리기 인자의 복사-초기화를 생략하고 처리기 매개변수를 예외 객체의 별칭으로 취급할 수 있어요. | (since C++11)- 코루틴에서, 코루틴 매개변수의 사본을 생략할 수 있어요. 이 경우, 매개변수 사본 객체의 생성자와 소멸자의 실행 외에는 프로그램의 의미가 바뀌지 않는다면, 그 사본에 대한 참조를 해당 매개변수에 대한 참조로 대체해요. | (since C++20)
복사 생략이 발생하면, 구현은 생략된 초기화의 원본과 대상을 단순히 같은 객체를 가리키는 두 가지 다른 방식으로 취급해요.
| 파괴 시점 | (since) |
|---|---|
| 파괴는 최적화 없이 두 객체가 파괴되었을 시점 중 더 늦은 시점에 일어난다. | (until C++11) |
| 선택된 생성자의 첫 번째 매개변수가 객체 타입에 대한 rvalue 참조이면, 그 객체의 파괴는 대상이 파괴되었을 시점에 일어난다. 그렇지 않으면 파괴는 최적화 없이 두 객체가 파괴되었을 시점 중 더 늦은 시점에 일어난다. | (since C++11) |
Prvalue 의미론 ("보장된 복사 생략") — C++17부터 prvalue는 필요할 때까지 실체화(materialize)되지 않고, 필요해지면 최종 목적지의 저장 공간에 직접 생성돼요. 이는 때때로 언어 구문이 복사/이동을 시각적으로 암시하더라도(예: 복사 초기화) 실제로 복사/이동이 수행되지 않는다는 뜻이에요. 즉 타입에 접근 가능한 복사/이동 생성자가 전혀 필요하지 않을 수도 있죠. 예를 들어:
- 함수 반환 타입과 같은 클래스 타입(단 cv 한정 무시)의 prvalue가 피연산자인
return문에서 반환 객체를 초기화할 때:T f() { return U(); }은U타입 임시를 생성한 다음 그 임시로부터 반환된T를 초기화해요.T g() { return T(); }은 반환된T를 직접 생성하고 이동이 없어요. 반환된 타입의 소멸자는T객체가 파괴되지 않는데도return문 지점에서 접근 가능하고 비-deleted여야 해요. - 초기화 표현식이 변수 타입과 같은 클래스 타입(단 cv 한정 무시)의 prvalue일 때 객체 초기화에서:
T x = T(T(f()));은x가f()의 결과로 직접 초기화되고 이동이 없어요. 이는 초기화되는 객체가 잠재적으로 겹치는 부분 객체가 아님이 알려진 경우에만 적용돼요: 기반 클래스 부분 객체를 초기화할 때나 다른 클래스의 기반 클래스 부분 객체가 될 수 있는 객체를 초기화할 때는 생략이 없어요.
참고: 이 규칙은 최적화를 지정하지 않고, 표준은 이를 공식적으로 "복사 생략"이라고 설명하지 않아요(생략되는 대상이 없기 때문). 대신 C++17의 prvalue와 임시에 대한 코어 언어 명세는 이전 C++ 개정판과 근본적으로 달라요: 더 이상 복사/이동할 임시가 없죠. C++17의 메커니즘을 다른 방식으로 설명하자면 "실체화되지 않은 값 전달(unmaterialized value passing)" 또는 "지연된 임시 실체화(deferred temporary materialization)"라고 할 수 있어요.
참고 (Notes)
복사 생략은 부수 효과를 바꿀 수 있는 유일하게 허용된 최적화 형태(until C++14) 할당 생략·확장과 함께 허용된 두 최적화 형태 중 하나(since C++14)예요. 일부 컴파일러는 허용된 모든 상황에서 복사 생략을 수행하지 않기 때문에(예: 디버그 모드), 복사/이동 생성자와 소멸자의 부수 효과에 의존하는 프로그램은 이식 가능하지 않아요.
| 참고 | (since) |
|---|---|
return 문이나 throw 표현식에서 컴파일러가 복사 생략을 수행할 수 없지만 복사 생략 조건이 충족되거나(원본이 함수 매개변수인 경우를 제외하면) 충족될 것인 경우, 컴파일러는 원본 피연산자가 lvalue로 지정되더라도 이동 생성자를 사용하려 시도한다(until C++23)원본 피연산자를 rvalue로 취급한다(since C++23). 상수 표현식과 상수 초기화에서는 복사 생략이 절대 수행되지 않는다. |
(since C++11) |
| Feature-test 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_guaranteed_copy_elision |
201606L | (C++17) | Guaranteed copy elision through simplified value categories |
예제 (Example)
#include <iostream>
struct Noisy
{
Noisy() { std::cout << "constructed at " << this << '\n'; }
Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
Noisy f()
{
Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary;
// the move constructor may be called
// (since C++17) "guaranteed copy elision"
return v; // copy elision ("NRVO") from v to the result object;
// the move constructor may be called
}
void g(Noisy arg)
{
std::cout << "&arg = " << &arg << '\n';
}
int main()
{
Noisy v = f(); // (until C++17) copy elision initializing v from the result of f()
// (since C++17) "guaranteed copy elision"
std::cout << "&v = " << &v << '\n';
g(f()); // (until C++17) copy elision initializing arg from the result of f()
// (since C++17) "guaranteed copy elision"
}
가능한 출력 (Possible output):
constructed at 0x7fffd635fd4e
&v = 0x7fffd635fd4e
constructed at 0x7fffd635fd4f
&arg = 0x7fffd635fd4f
destructed at 0x7fffd635fd4f
destructed at 0x7fffd635fd4e
Noisy는 생성·복사·이동·소멸마다 메시지를 찍어요. 그런데 출력을 보면 v는 한 번만 생성되고, 복사나 이동 생성 메시지가 전혀 없죠. C++17 이후에는 이게 최적화가 아니라 보장된 동작이에요—prvalue가 곧바로 최종 목적지에 만들어지니까요.
더 알아보기 (Learn more)
- 복사 초기화 — 개체를 다른 객체로부터 초기화하는 방식.
- 복사 생성자 — 같은 타입 객체로부터 초기화하는 생성자.
- 이동 생성자 — 임시 객체로부터 자원을 옮겨 초기화하는 생성자.