임시객체, 이동생성자 & 이동 대입 연산자
임시객체란?
프로그램 실행 도중에 필요에 의해 잠깐 생성되었다가 구문이 끝나면 소멸하는 객체이다.
레지스터에 들어갈 수 있는 사이즈라면 레지스터에 저장되고,
아니라면 스택에 생성되었다가 사라진다.
즉, 사용되는 단일 식을 넘어 지속되지 않는 일시적인 값이라고 볼 수 있다.
임시객체 특징
1) 임시객체는 주소연산자로 주소를 구할 수 없다.
Temporary *t1 = &Temporary(100); // 에러 발생.
2) 임시객체는 Ivalue가 될 수 없다. ( =의 왼쪽에 올 수 없다.)
Temporary(100) = 10; // 에러 발생
3) 임시객체는 일반적인 참조가 불가능하다. 그러나, 상수 참조는 가능하다.
Temporary &ref = Temporary(100); // 에러 발생
const Temporary & ref = Temporary(100); // 실행 가능, 즉 ref가 파괴될 때까지 임시객체는 존재한다.
4) 임시객체는 다음 행으로 넘어가면, 바로 소멸되게 된다.
그러나 참조자에 참조되는 임시객체는, 바로 소멸되지 않는다.
즉, 클래스의 외부에서, 객체의 멤버함수를 호출하기 위해 필요한 것은 다음 세가지 중 하나이다.
- 객체에 붙여진 이름
- 객체의 참조 값(객체 참조에 사용되는 정보)
- 객체의 주소 값
과거 C++ 의 문제(C++11 이전)
- 함수 호출 시 임시 개체(rvalue)에 대한 복사가 너무 많이 일어남
복사를 어떻게 막을까?
- rvalue 참조와 이동 문법으로 해결할 수 있다.
rvalue 참조 (&&)
- C++ 11 이후에 새로 나온 연산자
- 기능상 & 연산자와 비슷
- & 연산자는 lvalue 참조에 사용
- && 연산자는 rvalue 참조에 사용
rvalue 참조, stdmove(), 이동생성자
예시 : rvalue참조
float CalculateAverage()
{
float average;
// ...
return average;
}
int main()
{
int number = 10;
int&& rNumber = number; // 에러, number는 lvaule
int&& rNumber1 = 10; // OK, 10은 rvalue
float&& rAverage = CalculateAverage(); // OK, CaculateAverage()는 rvalue
}
std::move()
- rvalue 참조를 반환
- lvalue를 rvalue로 변환
이동생성자
class Test
{
public:
Test(Test&& _test) { ... }
...
}
- 다른 개체 맴버 변수들의 소유권을 가져옴
- 복사 생성자와 달리, 메모리 재할당을 하지 않음
- 복사 생성자보다 빠름
- 약간 얕은 복사와 비슷
MyString::MyString(MyString&& other)
{
// ...
}
예시 : MyString 이동 생성자
MyString.h
class MyString
{
public:
MyString(MyString&& other); // 이동 생성자
MyString(const MyString& other); // 복사 생성자
MyString(const char* str);
// ...
private:
char* mString;
int mSize;
};
MyString.cpp
// 이동 생성자
MyString::MyString(MyString&& other)
: mString(other.mString)
, mSize(other.mSize)
{
other.mString = nullptr;
other.mSize = 0;
}
// 복사 생성자
MyString::MyString(const MyString& other)
: mSize(other.mSize)
{
mString = new char[mSize];
memcpy(mString, other.mString, mSzie);
}
MyString::MyString(const char* str)
: mSize(strlen(str) +1)
{
mString = new char[mSize];
memcpy(mString, str, mSize);
}
Main.cpp
MyString studentName("Lulu");
MyString copiedName(studentName);
MyString copiedName2(std::move(studentName));
- 이동 생성자를 사용함으로써 메모리 재할당을 막음
이동 대입 연산자
이동 대입 연산자
Type& Type::operator=(Type&& )
- 이동 생성자와 같은 개념
- 다른 개체 맴버 변수들의 소유권을 가져옴
- 메모리 재할다응ㄹ 하지 않음
- 얕은 복사
MyString& MyString::Operator=(MyString&& other)
{
// ...
}
예시 : 이동 대입 연산자
MyString.h
class MyString
{
public:
MyString& operator=(MyString&& other);
// ...
private:
char* mString;
int mSize;
};
MyStirng.cpp
MyString& MyString::operator=(MyString&& other)
{
if(this != &other)
{
delete[] mString;
mString = other.mString;
mSize = other.mSize;
other.mString = nullptr;
other.mSize = 0;
}
return *this;
}
Main.cpp
MsString studnetName("Lulu");
MyString anotherStudentName("Teemo");
anotherStudentName = std::move(studnetName);
STL 컨테이너용 이동 문법
- C++ 11 이후로, STL 컨테이너에 이동 생성자와 이동 대입이 생김
- 그래서, 이것들을 따로 구현할 필요가 없음
rvalue 최적화
- 또다른 C++ 프로그래밍 유행어
- 이동 생성자와 이동 대입 연산자
- 아직 유효
- 포인터 대신 개체 자체를 반환하는 함수
- 함수에서 rvalue를 반환하는 것은 실제로 매우 느림
- 반환 값 최적화라고 하는 컴파일러 최적화를 깨뜨림
베스트 프랙티스
- 기본적으로 그냥 개체를 반환
- 더 빨라진다고 입증된 경우에만 함수가 rvalue를 반환하도록 바꾸자
이동 생성자
임시객체가 생성되고 소멸되는 과정이 2번씩이나 쓸데 없이 일어나는 문제점을 방지하기 위해 만들어진 생성자이다.
생성자의 인자로 자신과 같은 타입의 인자를 받으며, &&기호를 사용한다.
예시 코드
class Knight {
public:
Knight(int _id) :id(_id) { //... }
Knight(const Knight& knight){ cout << "Knight(const Knight&) : " << id << endl; } // 복사 생성자
Knight(Knight&& knight) { cout << "Knight(Knight &&) : " << id << endl; } //이동 생성자
~Knight() { cout << "~Knight() : " << id << endl; }
private:
int id=0;
};
int main()
{
Knight k1(10);
Knight k2(k1);
return 0;
}
class Knight {
public:
Knight(int _id) :id(_id) { cout << "Knight(int) : " << id << endl; }
Knight(const Knight& knight){ cout << "Knight(const Knight&) : " << id << endl; } // 복사 생성자
Knight(Knight&& knight) { cout << "Knight(Knight &&) : " << id << endl; } //이동 생성자
~Knight() { cout << "~Knight() : " << id << endl; }
private:
int id=0;
};
void Knight_Copy(Knight knight){}
void Knight_LValueRef(Knight& knight){}
void Knight_ConstLValueRef(const Knight& knight){}
void Knight_RValueRef(Knight&& knight){} // && 기호는 R-Value 참조자를 뜻하는 기호
int main()
{
Knight k1(10);
Knight_Copy(k1);
Knight_LValueRef(k1);
Knight_ConstLValueRef(k1);
Knight_RValueRef(Knight(20));
return 0;
}
위의 코드 실행결과를 보면 다음과 같다.
main 내부의 Knight_RValueRef 함수 호출부에서 매개변수가 뭔가 다르다.
k1이 아닌 이름없는 객체 Knight를 생성한다.
Knight_RValueRef 함수의 시그니처를 보면 매개변수로 Knight && 라는 형식을 가진다.
'&&' 기호는 'R-Value 참조자' 라는 것이다.
그냥 k1 을 매개변수로 넘겨줄 경우 ' an rvalue reference cannot be bound to an lvalue ' 라는 컴파일 에러가 난다.
그 이유는 다음과 같다.
R-value 참조 자리에 전달되는 인자는 L-Value여서는 안된다.
R-Value는 해당 데이터가 R-Value라는 의미이다.
하지만 R-value 참조라는 것은 R-Value를 참조하겠다는 의미이며, 해당 R-Value는 참조되고 나서 소멸한다.
왜냐하면 L-Value는 R-Value가 될 수 있기 때문이다.
'R-Value 참조'라는 것은 임시객체(잠깐 생성되었다 연산이 끝나면 곧장 소멸하는 객체)를 참조하겠다는 뜻이다.
그러므로 Knight_RValueRef 함수에 매개변수로 'k1' 이라는 식별자(이름)가 있는 객체를 전달해주지 못하는 것이다.
k1은 해당 함수가 반환을 해도 소멸하지 않아야 하기 때문이다.