1. 객체의 이동/복사 발생 경우
객체가 복사되거나 이동되는 경우는 크게 다섯 가지 경우이다.
객체의 이동과 복사는 다음과 같은 상황에서 주로 발생된다
- 할당의 소스로 사용될 때
- 객체의 초기자로 사용될 때
- 함수의 인수로 사용될 때
- 함수에서 값을 반환할 때
- 예외를 던지고(throw) 받을(catch) 때
1-1. 할당의 소스로 사용될 때
할당 시, 이동/복사 할당 연산자를 호출하며 객체의 이동/복사가 발생한다
기존 객체를 할당 연산의 소스로 사용할 시, 객체의 이동/복사가 발생한다.
MyClass x{};
MyClass y{};
y=x; //copy assignment operator
y=move(x); //move assignment operator

다음과 같은 상황에서 x는 복사와 이동이 이루어지며, 할당 연산자가 사용된다.
1-2. 객체의 초기자로 사용될 때
이동/복사 생성자 호출 시, 기존 객체의 이동/복사가 발생한다.
새 객체의 초기화에서 기존 객체를 초기자로 사용할 때 이동/복사가 발생한다.
MyClass newObj01{x}; //copy constructor
MyClass newObj02{move(x)}; //move constructor

다음과 같은 상황에서 x는 객체 초기자로서 사용된다. 이때 x는 복사와 이동이 이루어지며, 이동/복사 생성자가 사용된다
1-3. 함수의 인수로 사용될 시
함수의 인수로 사용될 때, 객체의 이동과 복사가 발생한다
void ParamTestFunc01(MyClass a){ return; }
/* ... */
ParamTestFunc01(x); ////copy constructor
ParamTestFunc01(move(x)); //move constructor

다음과 같은 상황에서 객체 x는 인수로 사용된다. 이 x는 매개변수 a의 생성자가 호출될 때 이동/복사를 통해 초기자로 사용된다.
함수 인자는 단순히 값을 넘기는 것이 아닌, 타입에 맞는 값으로 변환하는 과정등을 거친다.
다음과 같은 함수를 생각해보자 10은 int이지만 float로 암묵적 형변환이 발생한다. 이처럼 인수로 받은 값의 타입이 불일치할 때, 형변환 같은 추가 작업을 시도한다.void f(float x); /* ... */ f(10); // float로 암묵적 형변환
다음 코드도 이전 예시와 비슷한 맥락이다. 인자와 매개변수의 타입은 불일치하지만 T&&는 T의 rvalue이다. 이를 인수로 전달하면 이동 생성자를 호출해, 타입 T인 매개변수를 생성한다.void ParamTestFunc01(MyClass a); /*...*/ ParamTestFunc01(move(x));
1-4. 함수의 값을 반환할 때
함수에서 객체를 반환 시, 이동/복사가 발생한다.
MyClass ReturnTestFunc01()
{
volatile MyClass returnObj{}; //default constructor
/*...*/
cout<<"ReturnTestFunc01 >> "; return returnObj; //copy constructor(volatile obj)
}
MyClass ReturnTestFunc02()
{
MyClass returnObj{}; //default constructor
/*...*/
cout<<"ReturnTestFunc02 >> "; return move(returnObj); //copy constructor(volatile obj)
}
/*...*/
ReturnTestFunc01(); //copy constructor
MyClass w{ReturnTestFunc01()}; //copy constructor
ReturnTestFunc02(); //move constructor
MyClass z{ReturnTestFunc02()}; //move constructor

함수에서 값을 반환할 때, 그 호출 지점에서 반환값을 초기자로서 이동/복사하여 생성자를 호출한다.
volatile 변수는 최적화에서 제외된다. 이는 이후 설명할 최적화인 “복사 생략(copy elision)”를 방지해 예제가 복사 생성자를 호출하도록 유도한다. 자세한 내용은 후술
1-3의 추가설명과 비슷한 맥락이다. 리턴은 그저 값을 그래로 전달하는 것이 아닌, 반환 타입에 맞게 변환하는 작업등이 포함된다.
float f() {return 10;};다음과 같은 함수는 float를 리턴하지만 int값을 반환한다. 이 경우 int에서 float으로의 암묵전 변환이 발생한다. 즉 반환 타입과 실제 반환값의 타입 불일치 발생 시 형변환과 같은 작업을 수행한다.
MyClass ReturnTestFunc02() { MyClass returnObj{}; //default constructor /*...*/ return move(returnObj); //copy constructor(volatile obj) }위에서 설명한 코드도 마찬가지다. move(obj);의 타입은 T&&로 반환 타입과 다르다. 하지만 이는 T의 rvalue이며, 이 값을 이용해 이동생성자를 호출, 생성자를 통해 나온 T타입의 객체를 반환하게 된다.
1-5. 예외를 던지고(throw) 받을(catch) 때
throw와 catch시 예외 객체의 복사/이동이 발생한다
void ErrorFunc(){
volatile MyClass tmp{};
cout<<"throw >> "; throw tmp; //copy constructor(volatile obj)
}
/*...*/
try{
ErrorFunc();
}
catch(MyClass e) // copy constructor
{ /*...*/ }

예외를 발생시킬 시, throw에선 return과 비슷하게 객체를 전달하면서 이동/복사가 발생한다.
그리고 catch에서 객체를 전달받을 때도 이동/복사가 발생한다.
2. 최적화
복사/이동 생성자를 통해 객체를 전달하는 것은, 객체의 크기가 커지거나 자주 호출될 경우 성능문제를 겪을 수 있다. 이를 최적화하는 방법에 대해 간단히 설명한다.
2-1. 함수의 인수에서의 참조 사용
인수에서 참조 형식을 사용하는 것으로 이동과 복사를 생략할 수 있다.
함수의 인수로 객체를 그대로(값 형식)으로 전달 시, 전달받은 객체를 통해 이동/복사 생성자를 호출하게 된다(1-3). 이는 인수를 참조 형식으로 전달하는 것으로 해결할 수 있다.
void ParamTestFunc02(const MyClass& a);
참조의 의한 호출 시, 이동/복사 생성자가 발생하지 않는다. 하지만 인수로 쓴 객체의 변경이 발생할 수 있으므로 이를 방지하고 싶다면 const 키워드를 사용한다.
2-2. 복사 생략(copy elision)
컴파일러가 이동/복사 생성자 대신, 목적지에 바로 객체를 생성하는 것을 복사 생략이라 한다.
return등으로 객체를 전달하는 하는 상황에서, 컴파일러가 복사/이동 생성자를 사용하지 않고 직접 목적지에서 객체를 생성하는 최적화를 진행한다. 이를 복사 생략이라고 한다.
MyClass a = MyClass{};//기본 생성자 후 복사 생성자 호출이 아닌 a에 바로 객체를 생성
위 코드는 “이론적으론” 우측에서 임시 객체를 생성한 후, a에서 복사 생성자를 호출하여 a를 초기화 한다.
하지만 복사 생략을 통해, 임시 객체를 생성하는 것이 아닌 목적지인 a(타깃 객체)에서 바로 객체를 생성한다.
MyClass CopyElisionFunc01(){
return MyClass{};
}
MyClass CopyElisionFunc02(){
MyClass returnObj{};
/*...*/
return returnObj;
}
/*...*/
MyClass x = CopyElisionFunc01();
MyClass y = CopyElisionFunc02();

함수가 값을 반환할 때도 마찬가지다.
CopyElisionFunc01의 경우 return시 임시 객체를 생성한 후 복사하는 것이 아닌, 목적지인 x에 객체를 바로 생성한다.
CopyElisionFunc02의 경우 “returnObj”를 생성할 때부터 목적지인 y에 객체를 생성한다.
이를 RVO(Return Value Optimization)라고 하며, 리턴 값의 이름 여부에 따라 02의 경우는 NRVO(Named Return Value Optimization)라고도 불린다. (NRVO는 RVO의 한 종류로, RVO의 한 경우이다)
이러한 복사 생략은 C++17부터 강제된다.
하지만 NRVO와 같은 복사 생략이 작동하지 못하는 경우도 있다. 다음과 같은 경우를 보자
MyClass CopyElisionFunc03(){
random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 100);
cout<< "a: "; MyClass a{1};
cout<< "b: ";MyClass b{2};
cout<<"return: ";
if(dis(gen)%2 == 0) return a;
else return b;
}

다음 코드는 객체 a, b중 하나를 랜덤하게 반환한다. 함수가 어느 객체를 반환하는 지를 알지 못한다면(로직에 의한 리턴 값 선택 등) NRVO와 같은 복사 생략이 작동하지 않는다.
이 경우 목적지에 바로 객체를 생성하지는 못한다. 하지만 이러한 경우 비교적 비용이 큰 복사 대신, 컴파일러가 자체적으로 이동 생성자를 호출하는 최적화가 진행된다.
TMI) 물론 다음 코드에서 “return MyClass{1}”꼴로 CopyElisionFunc01처럼 return과 동시에 객체를 생성하면 복사 생략이 가능하다. 위의 예제는 미리 객체를 만들고 나중에 return하는 NRVO여서 문제가 발생하는 케이스이다.
MyClass CopyElisionThrow01(){
cout<<"throw >> "; throw MyClass{};
}
MyClass CopyElisionThrow02(){
MyClass tmp{};
cout<<"throw >> "; throw tmp;
}
오류의 경우도 return과 비슷하다. CopyElisionThrow01의 경우 throw에서 바로 오류 객체를 목적지에 생성한다.
하지만 이미 선언된 객체를 throw하는, NRVO와 비슷한 형식의 CopyElisionThrow02는 복사 생략을 하지 않고 이동 생성자로 객체를 전달한다. 이는 예외 객체가 지역 객체 tmp가 생성된 이후 생성되기 때문에 복사 생략을 위해선 이 순서를 재배치하며, 이때 생기는 문제점을 의식해서 방지한 것으로 보인다. (http://quuxplusone.github.io/blog/2018/04/09/elision-in-throw-statements/)
이렇게 전달된 객체를 catch할 경우, 복사 혹은 이동 생성자를 통해 오류 객체를 전달하게 된다.
잘못된 정보 등 지적 환영
참고 자료
'C++' 카테고리의 다른 글
| C++ - 이동/복사 특수 맴버 함수 (0) | 2025.05.18 |
|---|---|
| C++ - 클래스 소개(구체/추상/파생 클래스) (0) | 2025.04.24 |
| C++ - 함수 객체와 람다 (0) | 2025.04.21 |
| C++ - 내장 타입과 사용자 정의 타입 (0) | 2025.04.21 |
| C++ - 초기화 (0) | 2025.04.21 |
댓글