모두의 코드
씹어먹는 C++ - <12 - 1. 우측값 레퍼런스와 이동 생성자>

작성일 : 2018-03-24 이 글은 4709 번 읽혔습니다.

이번 강좌에서는

안녕하세요 여러분! 지난번 STL 강좌는 어떠셨나요? 이번 강좌에서는 C++ 11 에서 추가된 우측값 레퍼런스에 대해서 다루어보도록 하겠습니다. 처음에 보면 약간 생소할 수 있는데 천천히 읽어보시기 바랍니다.

 복사 생략(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_) {
    data_ = a.data_;
    std::cout << "복사 생성자 호출!" << std::endl;
  }
};

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

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

성공적으로 컴파일 하였다면

실행 결과

일반 생성자 호출!
복사 생성자 호출!
일반 생성자 호출!

와 같이 나옵니다.

뭔가 예상했던 것과 조금 다르지요?

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

이 부분에서 "일반 생성자 호출!" 한번 만 출력되었습니다. 아마 정석대로 였다면,

A(2)

를 만들면서 "일반 생성자 호출!" 이 한 번 출력되어야 되고, 생성된 임시 객체로 c 가 복사 생성되면서 "복사 생성자 호출!" 이 될 것이기 때문이지요. 그런데 왜 "일반 생성자 호출!" 한 번 밖에 출력되지 않았을 까요? 복사 생성자가 왜 불리지 않았을까요?

사실 생각해보면 굳이 임시 객체를 한 번 만들고, 이를 복사 생성할 필요가 없습니다. 어차피 A(2) 로 똑같이 c 를 만들거면, 차라리 c 자체를 A(2) 로 만들어진 객체로 해버리는 것이랑 똑같기 때문이지요.

따라서 똑똑한 컴파일러는 복사 생성을 굳이 수행하지 않고, 만들어진 임시로 만들어진 A(2) 자체를 c 로 만들어버립니다. 이렇게, 컴파일러 자체에서 복사를 생략해 버리는 작업을 복사 생략(copy elision) 이라고 합니다.

컴파일러가 복사 생략을 하는 경우는 (함수의 인자가 아닌) 함수 내부에서 생성된 객체를 그래도 리턴할 때, 수행할 수 있습니다. 물론 C++ 표준을 읽어보면 반드시 복사 생략을 해라 라는 식이 아니라, '복사 생략을 할 수 도 있다' 라는 뜻으로 써 있습니다. 즉, 경우에 따라서는 복사 생략을 해도 되는 경우에, 복사 생략을 하지 않을 수도 있다는 뜻이지요.

이전에 만들어 놓았던 MyString 클래스를 다시 살펴보도록 해봅시다.

#include <iostream>
#include <cstring>

class MyString {
  char *string_content;  // 문자열 데이터를 가리키는 포인터
  int string_length;     // 문자열 길이

  int memory_capacity;  // 현재 할당된 용량

 public:
  MyString();

  // 문자열로 부터 생성
  MyString(const char *str);

  // 복사 생성자
  MyString(const MyString &str);

  void reserve(int size);
  MyString operator+(const MyString &s);
  ~MyString();

  int length() const;

  void print();
  void println();
};

MyString::MyString() {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = 0;
  memory_capacity = 0;
  string_content = nullptr;
}

MyString::MyString(const char *str) {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = strlen(str);
  memory_capacity = string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++) string_content[i] = str[i];
}
MyString::MyString(const MyString &str) {
  std::cout << "복사 생성자 호출 ! " << std::endl;
  string_length = str.string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++)
    string_content[i] = str.string_content[i];
}
MyString::~MyString() { delete[] string_content; }
void MyString::reserve(int size) {
  if (size > memory_capacity) {
    char *prev_string_content = string_content;

    string_content = new char[size];
    memory_capacity = size;

    for (int i = 0; i != string_length; i++)
      string_content[i] = prev_string_content[i];

    if (prev_string_content != nullptr) delete[] prev_string_content;
  }
}
MyString MyString::operator+(const MyString &s) {
  MyString str;
  str.reserve(string_length + s.string_length);
  for (int i = 0; i < string_length; i++)
    str.string_content[i] = string_content[i];
  for (int i = 0; i < s.string_length; i++)
    str.string_content[string_length + i] = s.string_content[i];
  str.string_length = string_length + s.string_length;
  return str;
}
int MyString::length() const { return string_length; }
void MyString::print() {
  for (int i = 0; i != string_length; i++) std::cout << string_content[i];
}
void MyString::println() {
  for (int i = 0; i != string_length; i++) std::cout << string_content[i];

  std::cout << std::endl;
}

int main() {
  MyString str1("abc");
  MyString str2("def");
  std::cout << "-------------" << std::endl;
  MyString str3 = str1 + str2;
  str3.println();
}

성공적으로 컴파일 하였다면

와 같이 나옵니다.

string_content = nullptr;

nullptr 는 C++ 11 에 새로 추가된 키워드로, 기존의 NULL 대체합니다.

C 언어에서의 NULL 은 단순히 #define 으로 정의되어 있는 상수값 0 인데, 이 때문에 NULL 이 값 0 을 의미하는 것인지, 아니면 포인터 주소값 0 을 의미하는 것인지 구분할 수 가 없었습니다.

하지만 nullptr 로 '포인터 주소값 0' 을 정확히 명시해 준다면 미연에 발생할 실수를 줄여 줄 수 있게 됩니다.

MyString str3 = str1 + str2;

이 부분에서 두 개의 문자열을 더한 새로운 문자열로 str3 를 생성하고 있습니다.

MyString MyString::operator+(const MyString &s) {
  MyString str;
  str.reserve(string_length + s.string_length);
  for (int i = 0; i < string_length; i++)
    str.string_content[i] = string_content[i];
  for (int i = 0; i < s.string_length; i++)
    str.string_content[string_length + i] = s.string_content[i];
  str.string_length = string_length + s.string_length;
  return str;
}

위 함수가 str1 + str2 를 실행 시에 호출되는데, 먼저 빈 MyString 객체인 str 을 생성합니다. (생성자 호출! 출력됨) 그 후에, reserve 함수를 이용해서 공간을 할당하고, str1str2 를 더한 문자열을 복사하게 됩니다.

이렇게 리턴된 strstr3 을 생성하는데 전달되어서, str3 의 복사 생성자가 호출 됩니다.

하지만, 이미 예상했겠지만, 굳이 str3 의 복사 생성자를 또 호출할 필요가 없습니다. 왜냐하면, 어차피 똑같이 복사해서 생성할 것이면, 이미 생성된 (str1 + str2) 가 리턴한 객체를 str3 셈 치고 사용하면 되기 때문이지요. 이전의 예제에서는 컴파일러가 불필요한 복사 생성자 호출을 복사 생략을 통해 수행하지 않았지만, 이 예제의 경우, 컴파일러가 복사 생략 최적화를 수행하지 않았습니다.

위 과정을 그림으로 간단히 살펴보면 아래와 같습니다.

만약에 str1str2 의 크기가 엄청 컸다면 어떨까요? 쓸데 없는 복사를 두 번 하는데 상당한 자원이 소모될 것입니다.

그렇다면 이러한 문제를 C++ 에서는 어떠한 방식으로 해결하고 있을까요?

 좌측값 (lvalue) 와 우측값 (rvalue)

모든 C++ 표현식 (expression) 의 경우 두 가지 카테고리로 구분할 수 있습니다. 하나는 이 구문이 어떤 타입을 가지냐 이고, 다른 하나는 어떠한 종류의 '값' 을 가지냐 입니다. 값에 종류가 있어? 라고 생각 하실 수 있는데, 아래 예시를 살펴보도록 합시다.

int a = 3;

위 표현식에서 먼저 'a' 를 살펴보도록 합시다. 우리는 a 가 메모리 상에서 존재하는 변수 임을 알고 있습니다. 즉 'a' 의 주소값을 & 연산자를 통해 알아 낼 수 있다는 것입니다. 우리는 보통 이렇게 주소값을 취할 수 있는 값을 좌측값 (lvalue) 라고 부릅니다. 그리고 좌측값은 어떠한 표현식의 왼쪽 오른쪽 모두에 올 수 있습니다 (왼쪽에만 와야 하는게 아닙니다).

반면에 오른쪽에 있는 '3' 을 살펴보도록 합시다. 우리가 '3' 의 주소값을 취할 수 있나요? 아닙니다. '3' 은 왼쪽의 'a' 와는 다르게, 위 표현식을 연산할 때만 잠깐 존재할 뿐 위 식이 연산되고 나면 사라지는 값입니다. 즉, '3' 은 실체가 없는 값입니다.

이렇게, 주소값을 취할 수 없는 값을 우측값 (rvalue) 라고 부릅니다. 이름에도 알 수 있듯이, 우측값은 식의 오른쪽에만 항상 와야 합니다. 좌측값이 식의 왼쪽 오른쪽 모두 올 수 있는반면, 우측값은 식의 오른쪽에만 존재해야 합니다.

int a;         // a 는 좌측값
int& l_a = a;  // l_a 는 좌측값 레퍼런스

int& r_b = 3;  // 3 은 우측값. 따라서 오류

여태까지 우리가 다루어왔던 레퍼런스는 '좌측값' 에만 레퍼런스를 가질 수 있습니다. 예를 들어서, a 의 경우 좌측값 이기 때문에, a 의 좌측값 레퍼런스인 l_a 를 만들 수 있습니다.

반면에 3 의 경우 우측값이기 때문에, 우측값의 레퍼런스인 r_b 를 만들 수 없습니다. 따라서 이 문장은 오류가 발생하게 됩니다.

이와 같이 & 하나를 이용해서 정의하는 레퍼런스를 좌측값 레퍼런스 (lvalue 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
}

컴파일 하였다면 위 오류 1, 2, 줄에서 각각 다음과 같은 오류를 볼 수 있습니다.

컴파일 오류

Error C2106'=': left operand must be l-value
Error C2102'&' requires l-value

일단 func1 의 경우 좌측값 레퍼런스를 리턴합니다. 앞서, 좌측값 레퍼런스의 경우 좌측값에 해당하기 때문에,

func1(a) = 4;

의 경우 'func(a) 가 리턴하는 레퍼런스의 값을 4' 로 해라 라는 의미로, 실제로 변수 a 의 값이 바뀌게 됩니다. 또한, func1(a) 가 좌측값 레퍼런스를 리턴하므로, 그 리턴값의 주소값 역시 취할 수 있습니다.

하지만 func2 를 살펴볼까요? func2 의 경우, 레퍼런스가 아닌, 일반적인 int 값을 리턴하고 있습니다. 이 때 리턴되는 값은

a = func2(b);

이 문장이 실행 될 때 잠깐 존재할 뿐 그 문장 실행이 끝나면 사라지게 됩니다. 즉, 실체가 없는 값이라는 뜻이지요. 따라서 func2(b) 는 우측값이 됩니다. 따라서 위와 같이 우측값이 실제 표현식의 오른쪽에 오는 경우는 가능하지만,

func2(b) = 5;

위 문장 처럼 우측값이 왼쪽의 오는 경우는 가능하지 않습니다.

std::cout << &func2(b) << std::endl;  // 오류 2

마찬가지로 우측값의 주소값을 취할 수 없기 때문에 위 문장은 허용되지 않습니다.

그렇다면 앞선 예제에서

MyString str3 = str1 + str2;

를 다시 살펴보도록 합시다. 위 문장은

MyString str3(str1.operator+(str2));

와 동일합니다. 그런데, operator+ 의 정의를 살펴보면,

MyString MyString::operator+(const MyString &s)

로 우측값을 리턴하고 있는데, 이 우측값이 어떻게 좌측값 레퍼런스를 인자로 받는,

MyString(const MyString &str);

를 호출 시킬 수 있었을까요? 이는 & 가 좌측값 레퍼런스를 의미하지만, 예외적으로

const T&

의 타입의 한해서만, 우측값도 레퍼런스로 받을 수 있습니다. 그 이유는 const 레퍼런스 이기 때문에 임시로 존재하는 객체의 값을 참조만 할 뿐 이를 변경할 수 없기 때문입니다.

 그렇다면 이동은 어떻게?

그렇다면 앞서 MyString 에서 지적한 문제를 해결할 생성자의 경우 어떠한 방식으로 작동해야 할까요?

위와 같이 간단합니다. str3 생성 시에 임시로 생성된 객체의 string_content 가리키는 문자열의 주소값을 str3string_content 로 해주면 됩니다.

문제는 이렇게 하게 되면, 임시 객체가 소멸 시에 string_content 를 메모리에서 해제하게 되는데, 그렇게 되면 str3 가 가리키고 있던 문자열이 메모리에서 소멸되게 됩니다. 따라서 이를 방지 하기 위해서는, 임시 생성된 객체의 string_contentnullptr 로 바꿔주고, 소멸자에서 string_contentnullptr 이면 소멸하지 않도록 해주면 됩니다. 매우 간단하지요?

하지만, 이 방법은 기존의 복사 생성자에서 사용할 수 없습니다. 왜냐하면 우리는 인자를 const MyString& 으로 받았기 때문에, 인자의 값을 변경할 수 없게 되지요. 즉 임시 객체의 string_content 값을 수정할 수 없기에 문제가 됩니다.

이와 같은 문제가 발생한 이유는 const MyString& 이 좌측값과 우측값 모두 받을 수 있다는 점에서 비롯되었습니다. 그렇다면, 좌측값 말고 우측값만 특이적으로 받을 수 있는 방법은 없을까요? 바로 C++ 11 부터 제공하는 우측값 레퍼런스를 이용하면 됩니다. (참고로 C++ 11 가 기본으로 설정되어 있지 않는 컴파일러는 사용 불가능 합니다. 비주얼 스튜디오 2017 버전의 경우 자동으로 사용 가능하게 설정 되어 있으니 걱정하실 필요 없습니다.)

 우측값 레퍼런스

#include <iostream>
#include <cstring>

class MyString {
  char *string_content;  // 문자열 데이터를 가리키는 포인터
  int string_length;     // 문자열 길이

  int memory_capacity;  // 현재 할당된 용량

 public:
  MyString();

  // 문자열로 부터 생성
  MyString(const char *str);

  // 복사 생성자
  MyString(const MyString &str);

  // 이동 생성자
  MyString(MyString &&str);

  void reserve(int size);
  MyString operator+(const MyString &s);
  ~MyString();

  int length() const;

  void print();
  void println();
};

MyString::MyString() {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = 0;
  memory_capacity = 0;
  string_content = nullptr;
}

MyString::MyString(const char *str) {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = strlen(str);
  memory_capacity = string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++) string_content[i] = str[i];
}
MyString::MyString(const MyString &str) {
  std::cout << "복사 생성자 호출 ! " << std::endl;
  string_length = str.string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++)
    string_content[i] = str.string_content[i];
}
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;
}
MyString::~MyString() {
  if (string_content) delete[] string_content;
}
void MyString::reserve(int size) {
  if (size > memory_capacity) {
    char *prev_string_content = string_content;

    string_content = new char[size];
    memory_capacity = size;

    for (int i = 0; i != string_length; i++)
      string_content[i] = prev_string_content[i];

    if (prev_string_content != nullptr) delete[] prev_string_content;
  }
}
MyString MyString::operator+(const MyString &s) {
  MyString str;
  str.reserve(string_length + s.string_length);
  for (int i = 0; i < string_length; i++)
    str.string_content[i] = string_content[i];
  for (int i = 0; i < s.string_length; i++)
    str.string_content[string_length + i] = s.string_content[i];
  str.string_length = string_length + s.string_length;
  return str;
}
int MyString::length() const { return string_length; }
void MyString::print() {
  for (int i = 0; i != string_length; i++) std::cout << string_content[i];
}
void MyString::println() {
  for (int i = 0; i != string_length; i++) std::cout << string_content[i];

  std::cout << std::endl;
}

int main() {
  MyString str1("abc");
  MyString str2("def");

  std::cout << "-------------" << std::endl;
  MyString str3 = str1 + str2;
  str3.println();
}

성공적으로 컴파일 하였다면

와 같이 나옵니다.

먼저 우측값 레퍼런스를 사용한 이동 생성자의 정의 부분 부터 살펴봅시다.

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;
}

우측값의 레퍼런스를 정의하기 위해서는 좌측값과는 달리 & 를 두 개 사용해서 정의해야 합니다. 즉, 위 생성자의 경우 MyString 타입의 우측값을 인자로 받고 있습니다.

그렇다면 한 가지 퀴즈! 과연 str 자체는 우측값 일까요 좌측값 일까요? 당연히도 좌측값 입니다. 실체가 있기 때문이지요 (str 이라는 이름이 있잖아요). 다시 말해 str 은 타입이 'MyString 의 우측값 레퍼런스' 인 좌측값 이라 보면 됩니다. 따라서 표현식의 좌측에 올 수도 있습니다. (마지막 줄 처럼)

string_content = str.string_content;

이제 위와 같이 우리가 바라던 대로 임시 객체의 string_content 가 가리키는 메모리를 새로 생성되는 객체의 메모리로 옮겨주기만 하면 됩니다. 기존의 복사 생성자의 경우 문자열 전체를 새로 복사해야 했지만, 이동 생성자의 경우 단순히 주소값 하나만 달랑 복사해주면 끝이기 때문에 매우 간단합니다.

// 임시 객체 소멸 시에 메모리를 해제하지
// 못하게 한다.
str.string_content = nullptr;

한 가지 중요한 부분은 인자로 받은 임시 객체가 소멸되면서 자신이 가리키고 있던 문자열을 delete 하지 못하게 해야 합니다. 만약에 그 문자열을 지우게 된다면, 새롭게 생성된 문자열 str3 도 같은 메모리를 가리키고 있기 때문에 str3 의 문자열도 같이 사라지는 셈이 되기 때문입니다.

따라서 strstring_contentnullptr 로 바꿔줍니다.

MyString::~MyString() {
  if (string_content) delete[] string_content;
}

그리고 물론 소멸자 역시 바꿔줘야만 합니다. string_contentnullptr 가 아닐 때 에만 delete 를 하도록 말이죠.

일반적으로 우측값 레퍼런스는 아래와 같은 방식으로 사용할 수 있습니다.

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

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

일단 우측값 레퍼런스의 경우 반드시 우측값의 레퍼런스만 가능합니다. 따라서, r_b 의 경우 우측값 '3' 의 레퍼런스가 될 수 있겠지만, rr_b 의 경우 a 가 좌측값이기 때문에 컴파일 되지 않습니다.

우측값 레퍼런스의 재미있는 특징으로 레퍼런스 하는 임시 객체가 소멸되지 않도록 붙들고 있는다는 점입니다. 예를 들어서,

MyString&& str3 = str1 + str2;
str3.println();

의 경우 str3str1 + str2 에서 리턴되는 임시 객체의 레퍼런스가 되면서 그 임시 객체가 소멸되지 않도록 합니다. 실제로, 아래 println 함수에서 더해진 문자열이 잘 보여집니다.

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

만약에 여러분이 MyString 을 C++ 컨테이너들, 예를 들어 vector 에 넣기 위해서는 한 가지 주의할 점이 있습니다. 바로 이동 생성자를 반드시 noexcept 로 명시해야 한다는 점입니다.

vector 를 예를 들어서 생각해봅시다. vector 는 새로운 원소를 추가 할 때, 할당해놓은 메모리가 부족하다면, 새로운 메모리를 할당한 후에, 기존에 있던 원소들을 새로운 메모리로 옮기게 됩니다.

복사 생성자를 사용 하였을 경우 위와 같이 원소가 하나씩 하나씩 복사 됩니다. 그런데 만약에 이 복사 생성하는 과정에서 예외가 발생하였다고 해봅시다.

해결책은 간단합니다. 새로 할당해놓은 메모리를 소멸시켜 버린 후, 사용자에게 예외를 전달하면 됩니다. 새로 할당한 메모리를 소멸 시켜 버리는 과정에서 이미 복사된 원소들도 소멸 되버리므로 자원이 낭비되는 일도 없을 것입니다.

반면에 이동 생성자를 사용하였을 경우는 어떨까요? 이동 생성하는 과정에서 예외가 발생했더라면, 꽤나 골치아파집니다. 복사 생성을 하였을 경우 새로 할당한 메모리를 소멸시켜 버려도, 기존의 메모리에 원소들이 존재하기 때문에 상관 없지만, 이동 생성의 경우 기존의 메모리에 원소들이 모두 이동되어서 사라져버렸기에, 새로 할당한 메모리를 섯불리 해제해버릴 수 없기 때문입니다.

따라서 vector 의 경우 이동 생성자에서 예외가 발생하였을 때 이를 제대로 처리할 수 없습니다. 이는 C++ 의 다른 컨테이너들도 동일합니다.

이 때문에 vector 는 이동 생성자가 noexcept 가 아닌 이상 이동 생성자를 사용하지 않습니다.

아래 실제 예제를 통해 살펴보겠습니다.

#include <iostream>
#include <cstring>
#include <vector>

class MyString {
  char *string_content;  // 문자열 데이터를 가리키는 포인터
  int string_length;     // 문자열 길이

  int memory_capacity;  // 현재 할당된 용량

 public:
  MyString();

  // 문자열로 부터 생성
  MyString(const char *str);

  // 복사 생성자
  MyString(const MyString &str);

  // 이동 생성자
  MyString(MyString &&str);

  ~MyString();
};

MyString::MyString() {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = 0;
  memory_capacity = 0;
  string_content = nullptr;
}

MyString::MyString(const char *str) {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = strlen(str);
  memory_capacity = string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++) string_content[i] = str[i];
}
MyString::MyString(const MyString &str) {
  std::cout << "복사 생성자 호출 ! " << std::endl;
  string_length = str.string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++)
    string_content[i] = str.string_content[i];
}
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;
}
MyString::~MyString() {
  if (string_content) delete[] string_content;
}

int main() {
  MyString s("abc");
  std::vector<MyString> vec;
  vec.resize(0);

  std::cout << "첫 번째 추가 ---" << std::endl;
  vec.push_back(s);
  std::cout << "두 번째 추가 ---" << std::endl;
  vec.push_back(s);
  std::cout << "세 번째 추가 ---" << std::endl;
  vec.push_back(s);
}

성공적으로 컴파일 하였다면

실행 결과

생성자 호출 ! 
첫 번째 추가 ---
복사 생성자 호출 ! 
두 번째 추가 ---
복사 생성자 호출 ! 
복사 생성자 호출 ! 
세 번째 추가 ---
복사 생성자 호출 ! 
복사 생성자 호출 ! 
복사 생성자 호출 !

위와 같이 기껏 이동 생성자를 만들어놓았는데, vector 가 확장할 때 마다 복사 생성자를 이용하는 것을 볼 수 있습니다. 하지만 이동 생성자에 noexcept 를 추가하면 어떨까요.

#include <iostream>
#include <cstring>
#include <vector>

class MyString {
  char *string_content;  // 문자열 데이터를 가리키는 포인터
  int string_length;     // 문자열 길이

  int memory_capacity;  // 현재 할당된 용량

 public:
  MyString();

  // 문자열로 부터 생성
  MyString(const char *str);

  // 복사 생성자
  MyString(const MyString &str);

  // 이동 생성자
  MyString(MyString &&str) noexcept;

  ~MyString();
};

MyString::MyString() {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = 0;
  memory_capacity = 0;
  string_content = NULL;
}

MyString::MyString(const char *str) {
  std::cout << "생성자 호출 ! " << std::endl;
  string_length = strlen(str);
  memory_capacity = string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++) string_content[i] = str[i];
}
MyString::MyString(const MyString &str) {
  std::cout << "복사 생성자 호출 ! " << std::endl;
  string_length = str.string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; i++)
    string_content[i] = str.string_content[i];
}
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;
}
MyString::~MyString() {
  if (string_content) delete[] string_content;
}

int main() {
  MyString s("abc");
  std::vector<MyString> vec;
  vec.resize(0);

  std::cout << "첫 번째 추가 ---" << std::endl;
  vec.push_back(s);
  std::cout << "두 번째 추가 ---" << std::endl;
  vec.push_back(s);
  std::cout << "세 번째 추가 ---" << std::endl;
  vec.push_back(s);
}

성공적으로 컴파일 하였다면

실행 결과

생성자 호출 ! 
첫 번째 추가 ---
복사 생성자 호출 ! 
두 번째 추가 ---
복사 생성자 호출 ! 
이동 생성자 호출 !
세 번째 추가 ---
복사 생성자 호출 ! 
이동 생성자 호출 !
이동 생성자 호출 !

와 같이 제대로 이동 생성자를 호출함을 알 수 있습니다.

자 이것으로 이번 강좌는 여기서 마치도록 하겠습니다. 다음 강좌에서는 C++ 11 에 우측값 레퍼런스와 함께 새로 추가된 move 에 대해 살펴보도록 하겠습니다.

 생각 해보기

문제 1

사실 C++ 에서 값의 종류로 좌측값 우측값 만이 있는게 아니라 조금 더 세부적으로 나눠어집니다. 이에 대해 자세히 알아보고 싶으신 분들은 여기를 참조해주세요(난이도 : 상)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <12 - 1. 우측값 레퍼런스와 이동 생성자> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 20 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요