배워야 할 것들 : std::move, type&&, 이동 생성자, 이동 대입 연산자
이유 : 임시 객체를 만들 때 복사 동작 생략해서 성능 향상하기 위해
int a = (4 + 2);
위 코드를 조금 상세하게 보면 (4 + 2) 의 결과가 임시값으로 생성되고 'a'라는 변수에 복사 됩니다.
여기서 'a'는 이름이 있는 변수로써 좌측값에 해당하고, (4 + 2)는 임시 객체로 우측값에 해당 합니다.
void display(string& message);
string a = "Hello";
string b = "World";
display(a + b);
그렇다면 위에서 함수 인자로 전달되는 두 string 변수의 합은 어떨까요?
아래와 같은 컴파일 에러가 발생합니다.
error: cannot bind non-const lvalue reference of type ‘std::string&’ {aka ‘std::__cxx11::basic_string<char>’} to an rvalue of type ‘std::__cxx11::basic_string<char>’
15 | display(a + b);
| ~^~
note: initializing argument 1 of ‘void display(std::string&)’
5 | void display(string& message)
(string + string)와 같이 연산의 결과는 임시 객체로 우측값에 해당하기 때문에
display(string& message);
이 함수로는 지정할 수 없습니다. 이를 표현하기 위해서는 string&& message가 필요 합니다.
void display(string&& message);
string a = "Hello";
string b = "World";
display(a + b);
마찬가지로 아래 라인도 동일한 컴파일 에러를 발생 시킵니다.
void display(string& message);
display("Hello World");
"Hello World"라는 const 임시 객체가 우측값에 해당하기 때문입니다. 이를 고치려면 마찬가지로 string&& message가 필요 합니다.
그렇다면 아래 코드는 어떨까요?
void display(string&& message);
string a = "Hello";
display(a);
반대로 아래와 같은 컴파일 에러가 발생합니다.
error: cannot bind rvalue reference of type ‘std::string&&’ {aka ‘std::__cxx11::basic_string<char>&&’} to lvalue of type ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’}
19 | display(a);
| ^
note: initializing argument 1 of ‘void display(std::string&&)’
5 | void display(string&& message)
이럴 때 사용할 수 있는 즉 좌측값을 우측값으로 바꾸어 주는 std::move()를 사용할 수 있습니다.
void display(string&& message);
string a = "Hello";
display(std::move(a));
이 임시 우측값을 인자로 받는 내용을 class에 반영해서 class를 생성하거나 대입할 때 임시 또는 익명 객체를 다룰 수 있습니다.
이동 생성자
생성자와 대입 연산자를 정의하여 간단히 class Person을 정의 하고 사용해 보겠습니다.
using namespace std;
class Person {
public:
Person(string name);
Person(const Person& rhs) = default; // default 복제 생성자
Person& operator=(const Person& rhs) = default; // default 복제 대입 연산자
private:
string name_;
};
Person createPerson()
{
return Person("Jill");
}
int main(void)
{
vector<Person> people;
for(int i=0; i<2; i++) {
people.push_back(Person("John");
}
Person p("Jack");
p = createPerson(); //<-- 임시객체 -> 이동 대입 연산
Person p2("Bob");
p2 = p; //<-- 복제 대입 연산
cout << "Who am I? " << p << endl;
return 0;
}
- people.push_back(Person("John")) 의 경우 Person("John")을 생성하고 그 객체를 다시 복제해서 vector people에 추가 합니다. 더욱이 push_back()을 수행할 때 vector의 크기가 변경되면 모든 원소들을 크기가 변경된 vector로 복사하기 때문에 비효율이 발생합니다. (빈번한 접근을 하지 않고 삽입/삭제가 더 주요하게 사용되는 경우는 std::list를 사용하는 것이 더 좋겠습니다.)
이 때 중간 복제를 방지하려면 이동 생성자를 구현하여 복제 대신 이동을 수행하게 할 수 있습니다. - 마찬가지로 createPerson()이 생성한 Person("Jill")은 임시 객체로 리턴되어 p 변수에 대입될 때 복제를 하지만 이동 대입 연산자를 구현하면 복제 대신 이동을 수행하게 할 수 있습니다.
- p2 = p 의 경우도 이동 대입 연산자가 없으면 복제 대입 연산자가 호출되는데 이 때 불필요한 복사와 연산이 이루어 지므로 이동 대입 연산자를 구현하는 것이 좋겠습니다.
class Person {
public:
Person(string name);
Person(Person&& rhs) noexcept; // 이동 생성자
Person& operator=(Person&& rhs) noexcept; // 이동 대입 연산자
private:
string name_;
};
Person::Person(Person&& rhs) noexcept
{
name_ = std::move(rhs.name_); // 내부 정보 이관
}
Person& Person::operator=(Person&& rhs) noexcept
{
if (this == &rhs) { // 자기 자신을 대입한 경우
return *this;
}
name_ = std::move(rhs.name_);
return *this;
}
깊은 복제가 필요한 경우 이동 생성자와 이동 대입 연산자에 중복 코드가 크게 발생할 수 있으므로
swap(*this, rhs)로 맏바꾸는 함수로 일원화 합니다.
class Person {
public:
Person(string name);
Person(Person&& rhs) noexcept; // 이동 생성자
Person& operator=(Person&& rhs) noexcept; // 이동 대입 연산자
friend void swap(Person& dst, Person& src) noexcept;
private:
string name_;
Person() = default; // 이동 생성자에서 *this를 생성하기 위해 default 생성자 지정
};
Person::Person(Person&& rhs) noexcept
: Person()
{
swap(*this, rhs); // default 생성자에 의해서 *this가 생성되었으므로 swap() 호출 가능
}
Person& Person::operator=(Person&& rhs) noexcept
{
Person p(std::move(rhs)); // Person&& 우측값으로 지정하여 이동 생성자 호출
swap(*this, p);
return *this;
}
void swap(Person& dst, Person& src) noexcept
{
Person temp(std::move(dst));
dst = std::move(src);
src = std::move(temp);
}
댓글