6 분 소요

1. move

template< class T >
typename std::remove_reference<T>::type&& move( T&& t ) noexcept; // (until C++14)
template< class T >
constexpr std::remove_reference_t<T>&& move( T&& t ) noexcept; // (since C++14)
  • 객체 t가 이동 될 수 있음 을 알려줌
  • t가 가진 자원을 다른 객체에게 효율적으로 전달

2. 코드로 알아보기

2.1. 복사 생성자를 const lvalue reference로 만드는 이유

#include <iostream> 
using namespace std; 
  
class Point 
{ 
  int x = 0; 
  int y = 0; 
public: 
  Point() : x(0), y(0) {} 
  Point(int a, int b) : x(a), y(b) {} 

  // 복사 생성자 모양
  // 1. call by value, 복사 생성자가 무한히 호출되는 표기라서 컴파일 에러     
  // 2. call by reference, 임시객체를 받을 수 없음, 값타입을 반환하는 함수의 리턴값을 받을 수 없음 
  // 3. const lvalue reference 
  Point(const Point& pt) : x(pt.x), y(pt.y) {} 
}; 
Point foo() 
{ 
  Point pt; 
  return pt; 
} 
  
int main() 
{ 
  Point p1; 
  Point p2(1, 2); 
  Point p3(p2);     // Point( Point ) 모양의 생성자 필요 
  Point p4 = foo(); // 임시객체는 rvalue이기 때문에 참조를 받지 못함 
}

2.2. move 생성자 개념

#include <iostream>
#include <string.h>
using namespace std; 
  
class Cat 
{ 
  char* name; 
  int   age; 
public: 
  Cat(const char* s, int a) : age(a) 
  { 
    name = new char[strlen(s) + 1]; 
    strcpy(name, s); 
  } 
  // 깊은 복사로 구현한 복사 생성자 
  Cat(const Cat& c) : age(c.age) 
  { 
    // 메모리 할당 후, 메모리를 통째로 복사 
    name = new char[strlen(c.name) + 1]; 
    strcpy(name, c.name); 
  } 
  ~Cat() { delete[] name; } 
}; 
int main() 
{ 
  Cat c1("NABI", 2); 
  Cat c2 = c1;
}
#include <iostream>
#include <string.h>
using namespace std; 
  
class Cat 
{ 
  char* name; 
  int   age; 
public: 
  Cat(const char* s, int a) : age(a) 
  { 
    name = new char[strlen(s) + 1]; 
    strcpy(name, s); 
  } 
  ~Cat() { delete[] name; } 
    
  Cat(const Cat& c) : age(c.age) 
  { 
    cout << "copy" << endl; 
    name = new char[strlen(c.name) + 1]; 
    strcpy(name, c.name); 
  } 

  // 임시객체(rvalue)를 복사할 때 사용할 새로운 생성자 
  // "move" 생성자 라고 부름 
  Cat(Cat&& c) : age(c.age), name(c.name) 
  { 
    cout << "move" << endl; 
    c.name = nullptr; // 원본객체의 자원 reset 
  } 
}; 

int main() 
{ 
  Cat c1("NABI", 2); 
  Cat c2 = c1;                        // copy 
  Cat c3 = static_cast<Cat&&>(c2);    // lvalue지만 copy 후 삭제, move 
  Cat c4 = std::move(c1);             // 위처럼 캐스팅, move
}
#include <iostream> 
#include <string> 
#include <vector> 
using namespace std; 
  
int main() 
{ 
  string s1 = "hello"; 
  // string s2 = s1; 
  string s2 = std::move(s1); 

  cout << s1 << endl; 

  vector<int> v1(10, 3); 
  // vector<int> v2 = v1; 
  vector<int> v2 = std::move(v1); 

  cout << v1.size() << endl;  // 0 
}

2.3. move 개념 활용

  • 클래스를 만들 때 되도록 move 생성자를 제공, 그렇지 않으면 복사 생성자를 사용하게 됨
#include <iostream> 
#include <string> 
#include <algorithm> 
using namespace std; 

// move와 알고리즘 
// 아래 코드는 복사에 의한 swap, 성능 저하의 요인 
/* 
template<typename T> void Swap(T& a, T& b) 
{ 
  T tmp = a; 
  a = b; 
  b = tmp; 
} 
*/ 
template<typename T> void Swap(T& a, T& b) 
{ 
  T tmp = std::move(a); 
  a = std::move(b); 
  b = std::move(tmp); 
} 
  
int main() 
{ 
  string s1 = "hello"; 
  string s2 = "world"; 

  Swap(s1, s2); 
}

2.4. rule of 5

  • C++98 : rule of 3 (소멸자, 복사생성자, 대입연산자)
  • C++11 : rule of 5 (+ move 생성자, move 대입연산자)
#include <iostream> 
#include <string.h> 
#include <vector> 
using namespace std; 

class Data 
{ 
  int* buff; 
  string str; 
  int data; 
public: 
  Data(string s) : str(s) { buff = new int[100]; } 
  ~Data() { delete[] buff; } 

  // 복사 생성자 
  Data(const Data& d) : str(d.str), data(d.data) 
  { 
    // 포인터 멤버는 메모리 자체를 복사 
    buff = new int[100]; 
    memcpy(buff, d.buff, sizeof(int) * 100); 
  } 

  // 대입 연산자 
  Data& operator=(const Data& d) 
  { 
    // 대입 연산자는 항상 자신과의 대입을 조사해야 함
    if (&d == this) return *this; 
    str = d.str; 
    data = d.data; 

    // 기존 버퍼는 제거( 메모리 크기가 다를 수 있으므로 )  
    delete[] buff; 
    buff = new int[100]; 
    memcpy(buff, d.buff, sizeof(int) * 100); 

    return *this; 
  } 

  // move 생성자 
  Data(Data&& d) : str(std::move(d.str)), data(d.data), buff(d.buff) 
  { 
    d.buff = nullptr; 
  } 

  // move 대입 연산자 
  Data& operator=(Data&& d) 
  { 
    // 대입 연산자는 항상 자신과의 대입을 조사해야 함
    if (&d == this) return *this; 
    str = std::move(d.str); 
    data = d.data; 

    // 기존 버퍼는 제거( 메모리 크기가 다를 수 있으므로 )  
    delete[] buff; 
      
    buff = d.buff; 
    d.buff = nullptr; 
      
    return *this; 
  } 
}; 
  
int main() 
{ 
  Data d1("AA"); 
  //Data d2 = d1; // 복사 생성자 
  Data d3("BB"); 
  d3 = d1; // 대입 연산자 d3.operator=(d1) 
  //d3 = d3; 
}

2.5. move와 noexcept

  • noexcept
    • 함수가 예외가 없다고 컴파일러에게 알려주는 것
#include <iostream> 
#include <string> 
#include <type_traits> 
#include <vector> 
using namespace std; 
  
class Object 
{ 
  string data; 
public: 
  Object() = default; 
  ~Object() {} 

  Object(const Object& o) : data(o.data)      { cout << "Copy Ctor" << endl; } 

  Object& operator=(const Object& o) 
  { 
    cout << "Copy =" << endl; 
    if (&o == this) return *this; 
    data = o.data; 
    return *this; 
  } 

  // move 계열 함수를 만들 때 
  // 1. 예외가 없도록 만들고 
  // 2. 함수 () 뒤에 noexcept를 붙이는 것이 좋다. 
  // noexcept : 예외가 없다. 
  // noexcept(true) : 예외가 없다. 
  // noexcept(false) : 예외가 있다. 

  Object(Object&& o) noexcept( is_nothrow_move_constructible_v<string> )       
      : data(move(o.data)){ cout << "Move Ctor" << endl; } 

  Object& operator=(Object&& o) noexcept( 
      is_nothrow_move_constructible_v<string>) 
  { 
    cout << "Move =" << endl; 
    if (&o == this) return *this; 
    data = std::move(o.data); 
    return *this; 
  } 
}; 
  
int main() 
{ 
  vector<Object> v(5); 
  v.resize(10); // move 생성자 5번, vector의 5개 object를 move로 옮김

  Object o1; 
  Object o2 = o1; // 복사생성자 
  Object o3 = std::move(o1); // move 생성자 
  Object o4 = std::move_if_noexcept(o2); // 예외가 없으면 move, 예외가 있으면 복사 생성자 호출 
}

2.6. move와 상수객체

  • const 객체는 move가 될 수 없음, copy가 됨
#include <iostream> 
#include <string> 
#include <type_traits> 
#include <vector> 
using namespace std; 
  
  
class Object 
{ 
  string data; 
public: 
  Object() = default; 
  ~Object() {} 

  Object(const Object& o) : data(o.data) { cout << "Copy Ctor" << endl; } 

  Object& operator=(const Object& o) 
  { 
    cout << "Copy =" << endl; 
    if (&o == this) return *this; 
    data = o.data; 
    return *this; 
  } 

  Object(Object&& o) noexcept : data(move(o.data)) { cout << "Move Ctor" << endl; } 

  Object& operator=(Object&& o ) noexcept 
  { 
    cout << "Move=" << endl; 
    if (&o == this) return *this; 
    data = move(o.data); 
    return *this; 
  } 
}; 
  
// const 개체는 move 될 수 없음!!
int main() 
{ 
  const Object o1; 

  Object o2 = o1;                               // 복사 
  Object o3 = move(o1);                         // 복사
  Object o4 = static_cast<const Object&&>(o1);  // 복사
}

2.7. rule of 0

  • copy / move 계열을 만들지 않으면 컴파일러가 모두 제공
  • copy 계열을 사용자가 제공하면 컴파일러는 move 계열을 제공하지 않음
  • rule of 0
    • 자원을 직접 관리하지 말고 자원 관리 클래스를 사용
    • copy / move 계열을 사용자가 만들 필요가 없음
#include <iostream> 
#include <string> 
#include <type_traits> 
#include <vector> 
using namespace std; 
  
  
class Object 
{ 
  string data; 
public: 
  Object() = default; 
  ~Object() {} 

  Object(const Object& o) : data(o.data) { cout << "Copy Ctor" << endl; } 

  Object& operator=(const Object& o) 
  { 
    cout << "Copy =" << endl; 
    if (&o == this) return *this; 
    data = o.data; 
    return *this; 
  } 

  Object(Object&& o) noexcept : data(move(o.data)) { cout << "Move Ctor" << endl; } 

  Object& operator=(Object&& o)noexcept 
  { 
    cout << "Move =" << endl; 
    if (&o == this) return *this; 
    data = move(o.data); 
    return *this; 
  } 
}; 

class Data 
{ 
  string s;
  vector<int> v;
  Object obj; 
public: 
  Data() = default; 
}; 

int main() 
{ 
  Data d1;
  Data d2 = d1;       // copy
  Data d3 = move(d1); // move
}

참고

codenuri 강석민 강사 강의 내용기반으로 정리한 내용입니다.
코드누리
cppreference


This is personal diary for study documents.
Please comment if I'm wrong or missing something else 😄. 

Top

태그: , ,

카테고리:

업데이트:

댓글남기기