이동 시맨틱(Move Semantics): 이동 생성자, 이동 대입 연산자
필독
아래에서 주의해서 봐야할 내용이 있습니다. C++17부터 Guarenteed Copy Elision(보장된 복사 생략)이라는 개념이 들어오면서 단순히 함수의 리턴값을 대입하는 경우 임시 객체를 만드는 과정을 생략합니다. 아래 예시에서도 단순히 함수의 리턴 값을 대입하고 있는데, 이 글은 C++11을 기준으로 작성된 내용이므로 실제 컴파일러에서 코드를 돌리는 것과 동일하게 작동하지 않습니다. 이동 시맨틱에 대해서 배운다는 목적으로 아래 내용을 참조하면 되겠습니다.
깊은 복사의 문제점을 해결하기 위해 얕은 복사를 사용
객체를 복사할 때 하나 하나 많은 양의 값을 복사를 해야함.
깊은 복사가 필요 없는 경우도 존재함
// 고의적으로 얕은 복사를 하는 경우
// 깊은 복사 : 하나 하나 많은 양의 값을 다 복사를 해야함.
#include <iostream>
using namespace std;
class String {
public:
String() {
cout << "String() : " << this << '\n';
strData = NULL;
len = 0;
} // 동적할당 하는바에 포인터에 널값을 대입
String(const char *str) {
cout << "String(const char *str) : " << this << '\n';
// strData = str; --> 얕은 복사
len = strlen(str);
alloc(len);
strcpy(strData, str); // 깊은 복사
}
// 복사 !!!생성자!!!의 작동
String(const String &rhs) { // String s2(s1); 사용시 발생하는 일.
cout << "String(String &rhs) : " << this << '\n';
// strData = rhs.strData; // 얕은 복사로 인해 메모리 해제 2번
len = rhs.len;
alloc(len); // 깊은 복사
strcpy(strData, rhs.strData); // 깊은 복사
}
~String() {
cout << "~String() : " << this << endl;
release();
strData = NULL; // delete하고 해제된 메모리에 대한 접근을 막기위한 관습적 방법.
}
String &operator=(const String &rhs) { // 레퍼런스 변수를 사용하지 않으면 rhs(s1)이라는 복사 생성자가 작동해서 복잡해짐.
cout << "String &operator=(const String &rhs) : " << this << endl;
if (this != &rhs) { // 자기 자신을 대입하는 경우를 막기.
// 만약 strData에 어떤 값이 존재한다면, delete해줘야 함.
release();
len = rhs.len;
alloc(len);
strcpy(strData, rhs.strData); // 깊은 복사
} // 깊은 복사
return *this; // this : 함수가 속한 객체의 주솟값. *this --> 객체
}
char *GetStrData() const {
return strData;
}
int GetLen() const {
return len;
}
void SetStrData(const char *str) {
cout << "void SetStrData(const char *str) : " << this << ", " << str << '\n';
len = strlen(str);
alloc(len);
strcpy(strData, str);
}
private:
void alloc(int len) {
strData = new char[len + 1];
cout << "strData allocated : " << (void *)strData << '\n';
}
void release() {
delete[] strData;
if (strData) cout << "strData released : " << (void *)strData << '\n';
}
char *strData;
int len;
};
String getName() {
cout << "===== 2 =====" << '\n';
String res("Doodle");
/* 코드 생략 */
cout << "===== 3 =====" << '\n';
return res;
}
int f() { return 5; }
int main(void) {
String a;
cout << "===== 1 =====" << '\n';
a = getName();
/*
operator=에 rvalue를 매개변수로 갖는 연산자 오버로딩을 하자.
*/
// String &&r = getName(); // 임시객체 반환, &&r은 r-value 참조자이다.
cout << "===== 4 =====" << '\n';
}
// r-value(등호에 우변에만 올 수 있는 값)
/*
int a = 5;
a = a --> a는 l-value(오른쪽, 왼쪽 다 올 수 있음)
5 = a 이런건 안됨 --> 5는 r-value
int x = 2
x + 3 = 5; // x + 3 은 r-value --> 연산의 결과값은 r-value이다.
f() = 1; // 리턴값 자체는 임시객체인데 왜 1을 대입을 못하냐? 리턴값은 수정할 수 없는 그냥 r-value이다.
결국 임시객체는 r-value 이다
*/
3과 4 사이에 복사 생성자가 호출됨
어디서 복사 생성자가 호출될까?
위 과정에서 getName함수는 res를 리턴하는데, res가 임시 객체에 복사되고 리턴하면서 res가 소멸되고, 메인 함수 내의 a로 임시 객체가 복사되고 임시 객체가 소멸된다.
a에 이름을 넣기 위해, res, 임시 객체를 만들어 깊은 복사를 2번이나 하고 2개의 변수가 생성되고 소멸된다.
res -> 임시객체로 -> a 과정에서 복사 생성자, 복사 대입 연산자가 사용되고 있는데, 이를 사용하지 않고 얕은 복사가 일어나도록 다른 종류의 생성자를 사용하고 대입과정에서 얕은 복사를 이용해보자
- 이 과정에서 이동 시멘틱을 이용한다
"임시 객체의 strdata"가 "res의 strdata" 가리키는 곳"을 가리키고,
"a의 strdata"가 "임시 객체의 strdata가 가리키는 곳"을 가리킨다.
--> "a의 strdata"가 "res의 strdata"를 가리키게 된다
즉, strData를 복사하지 않고, res의 strData가 임시객체의 strData로 이동하고, a의 strData로 이동하는 것처럼 구현할 수 있다는 것이다.
어떻게 구현할 건가요?
함수의 리턴에서 등장하는 임시객체는 r-value이다
r-value 참조자를 이용해 getName()에서 반환하는 임시객체를 참조할 수 있다.
복사 생성자에서 l-value 참조자를 썼던 것처럼, r-value 참조자를 이용할 수 있다.
- String &&r = getName(); 이런 식으로 처리해보자.
1. "임시 객체의 strdata"가 "res의 strdata" 가리키는 곳"을 가리키도록 해보자
이 과정에 이동 생성자를 구현해서 사용하자
String(String &&rhs)를 사용하면 된다. // &&rhs는 r-value 참조자
이동 생성자
임시객체가 res로부터 복사를 받아야 함.
- 임시객체(res)
res는 l-value이고 임시 객체는 r-value인데, 어떻게 r-value에 l-value 복사할 수 있는 거죠?
반환되는 동안에는 res가 r-value로 취급되서 임시객체가 res로부터 복사를 받을 수 있다.
만약 String(String &&rhs)를 선언하지 않으면 어떻게 작동하나요?
String(const String &rhs)가 호출된다.
l-value를 참조하고 있지만, const이므로 값을 변경할 수 없다. 즉 r-value 처럼 작동하기 때문에 const 형 레퍼런스 변수가 r-value를 받을 수 있는 것이다.
// 이동 생성자(깊은 복사), 임시객체(res)에 해당
String(String &&rhs) { // 임시객체가 res로부터 복사를 받아야 함. res는 l-value이지만, res가 반환되는 동안에는 r-value로 취급됨.
cout << "String(String&&) : " << this << '\n';
len = rhs.len;
strData = rhs.strData; // 얕은 복사
rhs.strData = NULL; // res.strData와 메모리와 연결을 끊어주는 것
}
- 위 1, 2번 중에서 1번 해결 완료!
2. "a의 strdata"가 "임시 객체의 strdata가 가리키는 곳"을 가리키도록 해보자
- "a = 임시객체" 와 같은 동작이 실행될 것이고, 대입 연산자가 r-value를 참조하는 이동 대입 연산자를 구현해야 한다
// 이동 대입 연산자
String &operator=(String &&rhs) { // a = 임시객체(r-value) 호출
cout << "String &operator=(String &&) : " << this << '\n';
len = rhs.len;
strData = rhs.strData;
rhs.strData = NULL; // 임시객체의 strData와 메모리 간의 연결 끊기
return *this;
}
String의 대입 연산자에서 자신의 객체를 반환하도록 구현한 이유(return *this)
- String a = b = c;와 같은 문법을 위해
2번 해결 완료!
참조) 두들낙서 C/C++ 강좌
'Programming Language > C++' 카테고리의 다른 글
[C++] 상속(Inheritance)과 접근제어(Access Control) (0) | 2024.06.27 |
---|---|
[C++] Implicit Conversion(묵시적 형변환) (0) | 2024.06.27 |
[C++] 대입 연산자 오버로딩 (0) | 2024.03.12 |
[C++] 복사 생성자 오버로딩 (0) | 2024.03.12 |
[C++] 깊은 복사와 얕은 복사(Deep Copy, Shallow Copy) (0) | 2024.03.12 |