상세 컨텐츠

본문 제목

이동 생성자의 원리 w.우측값(rvalue)

C / C++

by 부레두 2024. 3. 6. 16:45

본문

컴파일러가 자동으로 생성하는 다수의 함수가 있다.

대략 생성자, 소멸자, 복사 대입연산자 등이 있는데 이곳에선 복사 생성자와 이동생성자를 다룬다.

이 둘은 이름 그대로 값을 복사, 이동하는데 사용하곤 한다.

어떻게 작동하는지 원리를 이해하기 위해서 먼저 알고가면 좋은 키워드가 있어 먼저 서술한다.

 

1. 좌측값 (lvalue), 우측값 (rvalue)

C++에서 쓰이는 표현식은 대체로 '구문이 어떤 타입을 가지는가?' 와 '어떠한 종류의 값을 가지는가?' 를 기준으로

좌측값(이하 lvalue), 우측값(이하 rvalue)로 구분이 가능하다.


int test = 6;

 

 

위 표현식에서 'test' 라 이름 지은 변수는 메모리 상에 주소를 가지고 존재하는 이름으로 & 연산자를 통해 주소값을 알수있다. 이렇게 주소값을 얻을 수 있는 값을 좌측값이라 부른다.

좌측값은 표현식의 왼쪽, 오른쪽 모두에 올 수 있다.

 

위 표현식에서 '3'은 'test' 와 다르게 주소값을 얻을 수 없고 연산할 때 잠깐 존재할 뿐 나중에 사라지는 값이다.

'실체가 없는 값'이라 표현할 수 있는데 이렇게 주소값을 얻을 수 없는 값을 우측값 이라 부른다.

 

int value; 		// value 는 좌측값
int& l_value = value;	// l_value는 좌측값 레퍼런스

int& r_value = 3;	// 3 은 우측값, 오류!

 


2. 복사 생략 

#include <iostream>

class A {
  int data_;

 public:
  A(int data) : data_(data) 
  { 
      std::cout << "일반 생성자 호출!" << std::endl; 
  }

  A(const A& a) : data_(a.data_) 
  {
    std::cout << "복사 생성자 호출!" << std::endl;
  }
};

int main() 
{
  A a(1);  // 일반 생성자 호출
  A b(a);  // 복사 생성자 호출

  // 그렇다면 이것은?
  A c(A(2));
}

 

의 결과

c를 만들기 위해 A를 거쳐 일반 생성자를 호출하고, 생성된 임시 객체로 복사 생성자가 호출될꺼라 예상한 결과가 복사 생성자를 호출하지 않은 결과로 나왔다.

 

이유는, 컴파일러가 스스로 복사 생성을 수행하지 않고 임시로 만들어진 A(2) 자체를 c로 만들어 버렸기 때문이다.

이렇게 컴파일러 자체가 복사를 생략해 최적화 하는 작업을 복사 생략 이라 한다.


3. 이동 생성자

임시 생성 객체를 그대로 새로 생성될 객체에 대입하는 과정은 컴파일러가 최적화하기도 하지만 그렇지 않기도 하다.

 

그럴 경우, 복사하려는 대상의 크기가 크다면 과한 자원을 소모하는 복사를 여러번 하게 될 것이다.

이를 명시적으로 해결하기 위해 '이동 생성자'를 사용하는데 원리는 다음과 같다.

 

  1. 생성된 임시 객체의 주소값을 이동 시킬 객체의 주소값으로 변경
  2. 임시 객체에 생성된 포인터를 nullptr로 변경, 소멸자에서 nullptr일 때 소멸되지 않도록 변경
    • 이동 시킨 객체의 메모리도 삭제되기 때문에

'원래 주인이 가지고 있는 물건을 다른 사람에게 소유권을 옮긴다.' 라는 표현이 적절하다고 생각한다.

다만 프로그램 상에서는 과정이 완벽하게 끝나기전에 삭제하게 되면 원본이 사라지게 되므로 주의해야 한다.

 

이동 생성자 작성 시 주의점

복사 생성자는 복사 시 예외나 에러가 발생할경우 새로 할당한 메모리를 소멸 시키는 간단한 해결책이 있지만

이동 생성자는 기존의 메모리에 원소들이 이동되어 사라진 후 예외가 발생하기 때문에 같은 해결책이 통하지 않는다.

 

때문에 c++의 컨테이너들은 noexcept 키워드가 존재하지 않으면 이동생성자를 호출하지 않는다.

관련글 더보기