8.1 이동 생성자와 이동 대입 연산자
8.1-1 lvalue
Def) lvalue
단일 식을 넘어 지속되는 개체
결국 지금까지 봐 온 많은 것들.
Note) lvalue 종류
주소가 있음/이름 있는 변수/const 변수/배열 이름
비트 필드/공용체/클래스 멤버/
좌측 값 참조로 반환하는 함수 호출/문자열 리터럴
<hide/>
// main.cpp
#include <iostream>
using namespace std;
struct Person
{
string mName;
int mAge;
}
int main()
{
// lvalue
int num = 10; // num
const int NAME_MAX = 10; // NAME_MAX
int* numberPtr = &num // numberPtr
map<string, float> scoreMap; // scoreMap
scoreMap["Lulu"] = 7.f; // scoreMap["Lulu"]
Person person;
person.mName = "Park"; // person.mName
const char* name = "Blitz"; // name, "Blitz"
return 0;
}
8.1-2 rvalue
Def) rvalue
단일 식을 넘어서까지 지속되진 않는 일시적인 값.
lvalue가 아닌 개체라고 생각해도 무방함.
Note) rvalue의 종류
주소가 없는 개체/리터럴(문자열 리터럴 제외)
참조로 반환하지 않는 함수 호출/i++와 i--(++i와 --i는 lvalue)
기본으로 지원되는 산술식, 논리식, 비교식/열거형/람다
<hide/>
// main.cpp
#include <iostream>
using namespace std;
int main()
{
// rvalue
int num = 10; // 10
// 10 = num; compile error. 10 is rvalue.
// (num + 1) = 20; compile error. (num + 1) is a rvalue.
int anotherNum = 20;
int res = num + anotherNum; // num + anotherNum is a rvalue.
// &num = 20; compile error. &num is a rvalue.
if (num < anotherNum) { ... } // (num < anotherNum) is a rvalue.
return 0;
}
Ex08010201) 과거 C++11 이전의 문제
<hide/>
// main.cpp
#include <iostream>
#include <vector>
using namespace std;
class Math
{
public:
static vector<float> convertToPercentage(const vector<float>& scores)
{
vector<float> percentages; // 벡터 개체 하나 생성
for (vector<float>::const_iterator it = scores.begin(); it != scores.end(); ++it) { /* ... */ }
return percentages;
}
};
int main()
{
vector<float> scores;
scores = Math::convertToPercentage(scores); // 반환값
/* 임 시 값(rvalue) */
// 임시값은 사라질건데, 임시값 만드느라 1번 대입 하느라 2번 데이터 만듦.
// 근데 또 결국 남는건 scores 하나임. 돌고 돌아 복사 횟수가 다 필요 없엇음.
// 정리하자면 아래와 같음.
// 1. scores의 메모리 n개가 잡혀 있음.
// 2. 함수가 호출되면서 지역 변수 percentages의 메모리가 생김.
// 3. percentages에 계산해서 저장함.
// 4. 함수의 스코프가 끝남. 반환되면서 percentages가 죽게 될 운명.
// 5. 죽기 전에 rvalue인 반환값의 메모리가 생성됨.
// 6. 반환값의 메모리에 percentages가 복사됨.
// 7. scores에 rvalue인 반환값이 대입됨. (새 개체를 만드는게 아니므로, 복사 생성자는 아님.)
return 0;
}
Note) 복사 생성자 문제
rvalue 참조와 이동 문법으로 해결할 수 있음.
vector<int> scores;
vector<int> copiedScores = scores;
// scores가 복사만 하고 사라진다해도, 일단 메모리가 어딘가에 복사되어짐.
// 그리고 나서 그 메모리를 copiedScores가 가리키는 식.
8.1-3 rvalue 참조(&&)
Note) C++11 이후에 새로 나온 연산자.
기능상 & 연산자와 비슷함.
& 연산자는 lvalue 참조에 사용하고,
&& 연산자는 rvalue 참조에 사용함.
Ex08020101)
// <hide/>
// main.cpp
float calculateAverage(void);
int main()
{
int num = 10;
// int&& refRvalue = num; // compile error. num은 lvalue
int&& refRvalue = 10;
float&& refReturnRvalue = calculateAverage();
return 0;
}
float calculateAverage(void)
{
float average = 0.f;
/* 평균을 구함. */
return average;
}
Def) move()
rvalue 참조(&&)를 반환함.
lvalue를 rvalue로 반환함.
즉, 인자로 받은 lvalue를 rvalue로 바꿔서
단일 식을 뛰어넘지 못하는 temporary한 값으로 바꿈.
Note) 노코프 수업 해석 미완성 코드
<hide/>
// main.cpp
#include <iostream>
#include <string>
using namespace std;
class MyString final
{
public:
MyString() = delete;
MyString(const char* str)
: mSize(strlen(str) + 1)
{
cout << "calling MyString(const char* str)." << endl;
mString = new char[mSize];
memcpy(mString, str, mSize);
}
MyString(const MyString& other)
: mSize(other.mSize)
{
cout << "calling MyString(const MyString& other)." << endl;
mString = new char[mSize];
memcpy(mString, other.mString, mSize);
}
~MyString()
{
delete[] mString;
mString = nullptr;
mSize = 0;
}
MyString& operator=(const MyString& other)
{
cout << "calling operator=(const MyString& other)." << endl;
mSize = other.mSize;
mString = new char[mSize];
memcpy(mString, other.mString, mSize);
}
private:
char* mString;
size_t mSize;
};
void storeByValue(MyString b)
{
// storeByValue() 함수의 스택 프레임에는 b와 c의 메모리 공간이 생성됨.
// main() 함수에서 storeByValue() 함수가 pass by value로 호출되고, b에는 a의 값이 복사됨.
// 이때 "hello" 또한 다시 정의가 되면서 b는 메모리의 시작 주소를 저장함.
// 다시 한 번 c에 b가 대입됨.
// 그럼 "hello"가 한 번 더
MyString c = b;
}
void storeByLRef(MyString& b)
{
// storeByLRef() 함수의 스택 프레임에는 b와 c의 메모리 공간이 생성됨.
// main() 함수에서 storeByLRef() 함수가 pass by reference로 호출되고, b에는 a가 가지고 있던 주소가 복사됨. 값복사 아님.
// 그리고 "hello"가 힙 공간에 복사되어지고, 지역변수 c는 이를 가리킴.
MyString c = b;
}
void storeByRRef(MyString&& b)
{
MyString c = move(b);
}
int main()
{
MyString a("hello");
// main() 함수의 스택 프레임에 a의 메모리 공간 확보.
// data segment(rodata) 영역에 "hello"가 저장되고, a에는 이 메모리의 시작주소가 저장됨.
cout << "-----------------" << endl;
storeByValue(a);
cout << "-----------------" << endl;
storeByLRef(a);
cout << "-----------------" << endl;
// storeByRRef(a); compile error.
storeByRRef(move(a));
cout << "-----------------" << endl;
storeByRRef("hello");
return 0;
}
8.1-4 이동 생성자
Def) 이동 생성자
클래스명(클래스명&& 매개변수명)
다른 개체 멤버 변수들의 소유권을 가져옴.
원본을 다 털어오는 것.
복사 생성자와 달리, 메모리 재할당을 하지 않음.
복사 생성자보다 빠름. 약간 얕은 복사와 비슷함.
MyString(MyString&& other) { ... } // 원본을 다 털어오기 위해서, 매개변수 자료형에 const 키워드를 안씀.
Ex08020102)
<hide/>
// main.cpp
#include <iostream>
#include <vector>
using namespace std;
class MyString final
{
public:
MyString() = default;
MyString(const char* string)
: mSize(strlen(string) + 1)
{
mString = new char[mSize];
memcpy(mString, string, mSize);
}
MyString(MyString&& other) // 이동 생성자. 바로 뒤에서 배울 예정.
: mSize(other.mSize)
{
mString = new char[mSize];
memcpy(mString, other.mString, mSize);
}
MyString(const MyString& other) = delete;
~MyString() {}
private:
char* mString;
size_t mSize;
};
int main()
{
MyString studentName("Park");
MyString clonedStudentName(move(studentName));
return 0;
}
8.1-5 이동 대입 연산자
Def) 이동 대입 연산자
클래스명& operator=(클래스명&& 매개변수명)
이동생성자와 같은 개념임.
원본과 같으면 아무것도 안하겠다는 것과
원본이 갓 생성된 개체가 아니므로, 원본을 소멸시켜줌.
다른 개체 멤버 변수들의 소유권을 가져옴.
이것도 메모리 재할당을 하지 않음. 얕은 복사.
MyString& operator=(MyString&& other) { ... }
Ex08020103)
<hide/>
// main.cpp
#include <iostream>
using namespace std;
class MyString final
{
public:
MyString() = default;
MyString(const char* name)
: mSize(strlen(name) + 1)
{
mString = new char[mSize];
memcpy(mString, name, mSize);
}
MyString(MyString&& other) noexcept
: mSize(other.mSize)
{
mString = new char[mSize];
memcpy(mString, other.mString, mSize);
}
MyString(const MyString& other) = delete;
MyString& operator=(MyString&& other) noexcept
{
mSize = other.mSize;
mString = new char[mSize];
memcpy(mString, other.mString, mSize);
}
private:
char* mString;
size_t mSize;
};
int main()
{
MyString studentName("Park");
MyString copiedStudentName1(move(studentName));
MyString copiedStudentName2;
copiedStudentName2 = move(studentName);
return 0;
}
Note) STL 컨테이너용 이동 문법
C++11 이후로 STL 컨테이너에 이동 생성자와 이동 대입연산자가 생김.
그래서 그것들을 따로 구현할 필요가 없음.
사실 C++11 이후 이동 대입 연산자의 구현은 std::swap()을 통해 구현함.
즉, 원본은 알아서 나중에 지워지게끔 하라고 최적화 해두었음.
Note) 이동 생성자와 이동 대입 연산자는 아직도 좋은 기능.
이거 안하면 실제 힙 메모리 할당이 되버리니까 성능 이득이 맞음.
Note) 포인터 대신 개체 자체를 반환하는 함수의 경우
함수에서 rvalue를 반환하는 것은 실제 매우 느림.
반환값 최적화(Return Value Optimization)라고 하는
컴파일러 최적화가 대부분의 컴파일러에서 지원됨.
Note) 반환값 최적화
이게 뭐냐면, 아까 보았던 지역변수 만들어서 처리하고
반환하는 경우에 컴파일러가 '아 임시개체 반환하는구나'하고
함수 스택 프레임에 지역 변수를 만들어서 나중에 반환값 복사하지 않고
처음부터 함수 바깥에 만들어두고 함수는 그거에 대입할 수 있게 하는 것.
Note) rvalue 최적화는 컴파일러의 반환값 최적화를 깨뜨림.
나중에 CPP 컨퍼런스에서 실수가 밝혀짐.
근데 특정 조건에선 컴파일러의 반환값 최적화가 안될때가 있긴함.
MSDN에 문서로 잘 정리되어 있으니 반환값 최적화를 검색해보자.
검색 키워드는 Named Rvalue Optimization.
Note) 베스트 프렉티스
기본적으로 그냥 개체를 반환하자.
더 빨리진다고 입증된 경우에만 함수가 rvalue를 반환하도록 바꾸자.
즉, 프로파일(성능측정) 했는데 어떤 함수 부분이 느려서
rvalue 최적화 해봤더니 더 빠르다면 그때 쓰도록 하자.
Ex08020104)
<hide/>
// main.cpp
#include <iostream>
using namespace std;
class MyString final
{
public:
MyString() = delete;
MyString(const char* str)
: mSize(strlen(str) + 1)
{
cout << "Calling Constructor!" << endl;
mString = new char[mSize];
memcpy(mString, str, mSize);
}
MyString(const MyString& other)
: mSize(other.mSize)
{
cout << "Calling Copy constructor!" << endl;
mString = new char[mSize];
memcpy(mString, other.mString, mSize);
}
MyString(MyString&& other) noexcept
: mString(other.mString)
, mSize(other.mSize)
{
cout << "Calling Move constructor!" << endl;
other.mString = nullptr;
other.mSize = 0;
// std::swap()으로 구현할 수도 있지만, 별로 선호되진 않음.
}
~MyString()
{
cout << "Calling Destructor!" << endl;
delete[] mString;
mString = nullptr;
}
MyString& operator=(MyString&& other) noexcept
{
cout << "Calling Move assignment operator!" << endl;
if (this != &other) // 같으면 그냥 넘어감. 같은데, 굳이 가져와가지고 nullptr 넣으면 메모리 누수.. 갖고 있던거 잃어버리니까.
{
delete[] mString;
mString = other.mString;
mSize = other.mSize;
other.mString = nullptr;
other.mSize = 0;
}
return *this; // this를 역참조해서 실제 개체를 참조로 반환해버림.
}
const char* GetString() const { return mString; }
size_t GetSize() const { return mSize; }
MyString MakeString()
{
MyString tempString("Temporary");
return tempString;
}
friend ostream& operator<<(ostream& os, const MyString& rhs)
{
for (size_t i = 0; i < rhs.mSize; ++i) { os << rhs.mString[i]; }
return os;
}
private:
char* mString;
size_t mSize;
};
int main()
{
MyString originalString("Hello, move!");
MyString copyString(originalString);
cout << originalString << endl;
cout << copyString << endl;
MyString moveString(originalString.MakeString()); // 여기서 다들 복사생성자가 호출될거라 착각함. 전혀아님. 함수의 반환값은 뭐다? 임시적인 개체. rvalue이기 때문에 결국 이동 생성자가 호출되는 것. 탈탈 털어서 nullptr로 바꿔버림.
cout << moveString << endl;
moveString = move(originalString);
// 일반적인 대입이 아님에 주의. 일반적인 대입이라면, originalString은 lvalue로 판정.
// 그러나 std::move() 함수를 통해서 rvalue 처리.
if (nullptr == originalString.GetString()) { cout << "Successfully moved." << endl; }
if (0 == originalString.GetSize()) { cout << "Successfully moved." << endl; }
cout << moveString << endl;
return 0;
}
'C++ > 문법 정리' 카테고리의 다른 글
Chapter 07. 스마트 포인터 (0) | 2022.05.08 |
---|---|
Chapter 06. C++11/14/17 (0) | 2022.05.08 |
Chapter 05. 템플릿과 파일 시스템 (0) | 2022.05.07 |
Chapter 04. 캐스팅과 인라인 (0) | 2022.05.06 |
Chapter 03. 상속과 다형성 (0) | 2022.05.05 |
댓글