우측값 레퍼런스와 이동생성자

강한친구·2022년 3월 8일

C / CPP

목록 보기
16/19

복사 생략 (copy ellision)

# include <iostream>

class A {
    int data;
    
    public:
    A(int data) : data(data) { std::cout << " Constructor " << "\n"; } 
    A(const A& a) :data(a.data) {
        std::cout << " Copy_Constructor " << "\n";
    }
};

int main() {
    A a(1);
    A b(a);

    A c(A(2));
}

다음과 같은 코드가 있다고 할 때, 원래대로 생각해보면
A(2)를 통해 일반생성자 객체가 생성되고, 그 객체를 A c()가 받아서 복사생성자가 생성될것만 같다.

하지만 실제로 컴파일해보면

 Constructor      
 Copy_Constructor 
 Constructor   
 

같은 결과가 나온다. 이는 컴파일러가 스스로 판단하여 '어차피 A(2)로 c를 만들거면 그냥 c를 A(2)로 처리하자' 라는 프로세스를 거쳐서 일반생성자 호출이 된 것이다.

이처럼 복사를 생략하는 작업을 복사생략이라 부른다.
반대로 복사생성자가 필요한 경우에서 안쓰이는 경우도 생긴다.

예전에 만든 Mystring에서 두 문자열을 합쳐서 출력하는
str3 = str1 + str2을 본적이 있을것이다.

이때, str3 은 str1과 str2로 만들것이기 때문에 굳이 복사생성자를 호출 할 필요가 없다. 하지만 컴파일러는 이를 실행하지 않았다.

이미지 출처

lvalue, rvalue

이러한 문제를 해결하기 위해 나온것이 lv rv이다.

int a = 3;

이라는 코드가 있다고 하자. 이때 a는 주소값을 가진 변수이고 메모리상에 존재한다. 이를 lvalue라고 부른다. (lvalue라고 오른쪽에만 올수 있는게 아니다.)

이제 3을 보면, 3은 우리가 주소값을 가질 수 없다. lvalue에 3을 넣어주고나서 다시 사라진다. 이 값은 왼쪽에 올 수 없다

int a = 3;
int b = a;
// 이건 가능하지만

4 = int a;
// 이건 안된다!

지금까지 주구장창 써오던 레퍼런스는 좌측값에만 쓸 수 있다.

int a;
int& l_a = a;
int& r_b = 3;

여기서 마지막 코드는 오류가 난다는것이다. 3은 주소가 없으니 레퍼런스(다른 의미로는 주소값)을 가질수 없는게 당연하다.

int& func1(int& a) { return a; }
int func2(int b) { return b; }

int main() {
  int a = 3;
  func1(a) = 4;
  std::cout << &func1(a) << "\n";

  int b = 2;
  a = func2(b);    // 1번            
  func2(b) = 5;          // 2번   
  std::cout << &func2(b) << "\n"; // 3번 
}

이러한 코드가 있을때, 1번이 된다는것은 누구나 알거고,
2번 3번은 오류가 발생한다.
2번은 사실 말이 안된다. func2b)의 반환값은 int b, 즉 우측값을 반환하기 때문이다. (b가 좌측값이 아니다!)

3번도 마찬가지로 좌측값의 레퍼런스 문제로 작동하지 않는다.

반면, func1은 레퍼런스를 함수로 받는 경우이기 때문에 가능하다.

이동생성자

이동을 할 떄는 어떻게 해야할까? 소멸자에서 임시생성된 객체(string_content)가 소멸하지 않도록 nullptr로 바꿔준다.

이동생성자가 사용되는 궁극적인 목표는 성능향상이다. 기존의 복사생성으로는 너무 많은 rvalue 복사가 일어나기 때문이다. 따라서 ravlue 참조라는 기능을 이용해서 rvalue의 값을 이동(말그대로 이동이라 shallow copy)을 시켜주는거다.

이 오른쪽값 참조는 &&로 표기한다.

double avgerage() {
  //...some code
  returm avg;
}

int main() {
	int num = 10;
    int &&rnum = num // 1.
    int &&rnum1 = 10; // 2.
    double &&ravg = average();  // 3.
}
    

다음과 같은 코드가 있다고 할 때, 1번은 오류, 2, 3 번은 정상작동이다.

1번은 보이는것처럼 num이 lvalue이기떄문에 오류가 발생하는것이고 나머지 2번 3번은 lvalue만 보기때문에 정상작동한다.

이동생성자

이동생성자는 다음과 같이 생겼다.

MyString::MyString(MyString&& other){
    // ...
}

이동생성자는 다른 개체맴버의 소유권을 가지고 오며, 복사생성자와 다르게 메모리 할당의 과정이 없다. 따라서 복사생성자보다 빠르며 메모리 공간이 절약된다.

얕은 복사와 비슷하다.

std::move()

우측값만이 아니라 좌측값도 이동하고싶다면 어떻게 해야할까?
swap 함수를 생각해보면

template <typename T>
void my_swap(T &a, T &b) {
  T tmp(a);
  a = b;
  b = tmp;
}

이렇게 temp 임시 변수를 만들어서 해결해도 되지만, 이는 쓸데없는 복사가 3번이나 이루어지게 된다.
이를 깔끔하게 해결하기 위해 나온것이 utility.h의 move이다.

#include <iostream>
#include <utility>

class A {
 public:
  A() { std::cout << "일반 생성자 호출!" << std::endl; }
  A(const A& a) { std::cout << "복사 생성자 호출!" << std::endl; }
  A(A&& a) { std::cout << "이동 생성자 호출!" << std::endl; }
};

int main() {
  A a;

  std::cout << "---------" << std::endl;
  A b(a);

  std::cout << "---------" << std::endl;
  A c(std::move(a));
}

출처

여기서 move는 lvalue였던 a를 rvalue로 바꿔주는 역할을 한다.

최근에는 STL 컨테이너에도 이동생성과 이동대입이 생겨서 이걸 다 구현할 필요가 없다.

0개의 댓글