우측값과 이동 연산

·2022년 6월 20일
0

cpp_study

목록 보기
19/25

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

복사 생략(Copy Elision)

#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)); // ❓ 일반 생성자 호출
}

위 코드에서, A c(A(2)); // ❓ 일반 생성자 호출 이 부분의
cpp에서는 복사 생성을 굳이 수행하지 않고, 임시로 만들어진 A(2)를 그냥 2로 취급해 일반 생성자만 호출하게 된다.

좌측값과 우측값

  • 좌측값(lvalue): 주소값을 취할 수 있는 값(식의 왼오 모두 올 수 있음)
  • 우측값(rvalue): 주소값을 취할 수 없는 값(실체가 없는 값)

좌측값 레퍼런스(lavlue reference)

& 하나를 이용해서 정의하는 레퍼런스
좌측값 레퍼런스 자체도 좌측값이 됨

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

int main() {
  int a = 3;
  func1(a) = 4;
  std::cout << &func1(a) << std::endl;
  int b = 2;
  a = func2(b); // 가능
  func2(b) = 5; // 오류 1
  std::cout << &func2(b) << std::endl; // 오류 2
}

func2(b)는 우측값, func1(a)는 좌측값에 해당하게 됨.

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

이동 생성자를 이용하면 기존의 메모리에 원소들이 모두 이동됨.
따라서 똑같은 내용으로 메모리를 한번만 차지함(복사 생성자는 두번 차지).

우측값 레퍼런스를 사용한 이동 생성자의 정의 부분은 다음과 같다.

MyString::MyString(MyString&& str) {
  std::cout << "이동 생성자 호출 !" << std::endl;
  string_length = str.string_length;
  string_content = str.string_content;
  memory_capacity = str.memory_capacity;
  // 임시 객체 소멸 시에 메모리를 해제하지
  // 못하게 한다.
  str.string_content = nullptr;
}

우측값의 레퍼런스를 정의하기 위해서 좌측값과는 달리 &를 2개 사용해서 정의해야 함.
여기서 위 생성자의 경우 MyString 타입의 우측값을 인자로 받음.
str은 "타입이 <MyString의 우측값 레퍼런스>인 좌측값"이라고 보면 됨.
-> 표현식의 좌측에 올 수도 있음.

null ptr 메모리 해제?

우측값 레퍼런스의 예시

int a;
int& l_a = a;
int& ll_a = 3; // 불가능
int&& r_b = 3;
int&& rr_b = a; // 불가능

이동 생성자 작성 시 주의할 점, noexcept

이동 생성 과정에서 예외가 발생하는 경우, 기존의 메모리에 원소들이 모두 이동되어 사라져서 새로 할당한 메모리를 섯불리 해제할 수 없음.

-> vector 및 cpp의 다른 컨테이너들은 이동 생성자가 noexcept가 아닌 이상 이동 생성자를 사용하지 않음.

=> noexcept 추가하면 이동 생성자 사용.

MyString::MyString(MyString &&str) noexcept {
  std::cout << "이동 생성자 호출 !" << std::endl;
  string_length = str.string_length;
  string_content = str.string_content;
  memory_capacity = str.memory_capacity;
  // 임시 객체 소멸 시에 메모리를 해제하지
  // 못하게 한다.
  str.string_content = nullptr;
}

move 문법과 완벽한 전달

좌측값이 우측값으로 취급될 수 있게 바꿔주는 함수: move 함수

move semantics

#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)); // 이동
}

std::move 함수가 인자로 받은 객체를 우측값으로 변환해서 리턴해 줌.
실제로는 단순한 타입 변환만 수행함(즉 우측값을 받는 함수들이 오버로딩되며 수행되는 것).

std::move는 이동은 수행하지 않음!

완벽한 전달(perfect forwarding)

#include <iostream>
#include <vector>

template <typename T>
void wrapper(T u) {
  g(u);
}
class A {};

void g(A& a) { std::cout << "좌측값 레퍼런스 호출" << std::endl; }

void g(const A& a) { std::cout << "좌측값 상수 레퍼런스 호출" << std::endl; }

void g(A&& a) { std::cout << "우측값 레퍼런스 호출" << std::endl; }

int main() {
  A a;
  const A ca;
  std::cout << "원본 --------" << std::endl;
  g(a);
  g(ca);
  g(A());
  std::cout << "Wrapper -----" << std::endl;
  wrapper(a);
  wrapper(ca);
  wrapper(A());
}

/*
[출력]
원본 --------
좌측값 레퍼런스 호출
좌측값 상수 레퍼런스 호출
우측값 레퍼런스 호출
Wrapper -----
좌측값 레퍼런스 호출
좌측값 레퍼런스 호출
좌측값 레퍼런스 호출
*/

위와 같이 출력된 이유는 C++ 컴파일러가 템플릿 타입을 추론할 때, 템플릿 인자 R가 레퍼런스가 아닌 일반적인 타입이라면 const를 무시하기 때문이다.

-> 즉, T가 전부 다 class A로 추론됨

따라서 아래와 같이 모든 조합의 템플릿 함수들을 정의해 주면 된다.

template <typename T>
void wrapper(T& u, T& v) {
  g(u, v);
}

template <typename T>
void wrapper(const T& u, T& v) {
  g(u, v);
}

template <typename T>
void wrapper(T& u, const T& v) {
  g(u, v);
}

template <typename T>
void wrapper(const T& u, const T& v) {
  g(u, v);
}

보편적 레퍼런스(Universal reference)

forward 키워드를 사용하면 위에서 거쳤던 복잡한 오버로딩(모든 조합에 대한 오버로딩)을 생략할 수 있음.

#include <iostream>

template <typename T>
void wrapper(T&& u) {
  g(std::forward<T>(u));
}

class A {};

void g(A& a) { std::cout << "좌측값 레퍼런스 호출" << std::endl; }
void g(const A& a) { std::cout << "좌측값 상수 레퍼런스 호출" << std::endl; }
void g(A&& a) { std::cout << "우측값 레퍼런스 호출" << std::endl; }

int main() {
  A a;
  const A ca;
  std::cout << "원본 --------" << std::endl;
  g(a);
  g(ca);
  g(A());
  std::cout << "Wrapper -----" << std::endl;
  wrapper(a);
  wrapper(ca);
  wrapper(A());
}

레퍼런스 겹침 규칙

#include <iostream>

void show_value(int&& t) { 
	std::cout << "우측값 : " << t << std::endl;
}

int main() {
    show_value(5); // 우측값 ok!
    int x = 3;
    show_value(x); // 애러 
}

이때 int&& t는 우측값 외에는 못 받음.

보편적 레퍼런스(Universal reference): 템플릿 인자 T 에 대해서, 우측값 레퍼런스를 받는 형태

profile
이것저것 개발하는 것 좋아하지만 서버 개발이 제일 좋더라구요..

0개의 댓글