본문 바로가기
cpp

C++ 이동 의미론 (move semantics) - #1

by kanlee2010 2023. 8. 23.

배워야 할 것들 : 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);
}

 

댓글