메모리 복사가 아닌 자원의 이동, 원래 가지고 있던 포인터는 nullptr로 초기화
#include <string>
int main()
{
std::string s1 = "Practice make perfect";
std::string s2 = s1;
std::string s3 = "Practice make perfect";
std::string s4 = std::move(s3);
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
std::cout << s3 << std::endl; // ""
std::cout << s4 << std::endl;
}
#include <string>
template<class T>
void swap1(T& lhs, T& rhs)
{
T tmp = lhs;
lhs = rhs;
rhs = tmp;
}
template<class T>
void swap2(T& lhs, T& rhs)
{
T tmp = std::move(lhs);
lhs = std::move(rhs);
rhs = std::move(tmp);
}
int main()
{
std::string s1 = "Practice make perfect";
std::string s2 = "To be or not to be";
swap1(s1, s2);
}
컴파일러가 만든 복사 생성자는 얕은 복사로 진행
사용자가 복사 생성자를 만들어 깊은 복사하여 해결 가능
하지만 성능상의 문제가 존재
#include <cstring>
class Person
{
char* name;
int age;
public:
Person(const char* s, int a) : age(a)
{
name = new char[strlen(s) + 1];
strcpy_s(name, strlen(s) + 1, s);
}
~Person() { delete[] name; }
Person(const Person& p) : age(p.age)
{
name = new char[strlen(p.name) + 1];
strcpy_s(name, strlen(p.name) + 1, p.name);
}
};
Person foo()
{
Person p("john", 20);
return p; // 함수가 객체를 값으로 반환하면 임시객체를 반환
}
int main()
{
Person ret = foo(); // 임시객체이므로 바로 파괴됨
}
메모리를 복사하지 말고 주소를 복사하면 보다 효율적일 것으로 예상
임시객체를 위한 복사 생성자를 추가로 만들자!
class Person
{
...
// lvalue와 rvalue를 모두 받을 수 있음
Person(const Person& p) : age(p.age) // 복사 생성자
{
name = new char[strlen(p.name) + 1];
strcpy_s(name, strlen(p.name) + 1, p.name);
}
// rvalue만 받을 수 있음
Person(Person&& p) : name(p.name), age(p.age) // move 생성자
{
p.name = nullptr;
}
};
int main()
{
Person robert("robert", 30);
Person p1 = robert;
Person p2 = foo();
}
move() 함수에선 rvalue 캐스팅만 해주고 실제 자원 이동은 move 생성자에서 진행
class Object
{
public:
Object() = default;
Object(const Object& obj) { std::cout << "copy ctor" << std::endl;}
Object(Object&& obj) { std::cout << "move ctor" << std::endl;}
};
Object foo()
{
Object obj;
return obj;
}
int main()
{
Object obj1;
Object obj2 = obj1; // copy
Object obj3 = foo(); // move
Object obj4 = static_cast<Object&&>(obj1); // move
Object obj5 = std::move(obj2); // move
}
std::move()를 사용했는데 클래스에 move 생성자 구현이 없다면
복사 생성자를 사용
move 생성자가 있다면 최적화 되지만 없더라도 오류는 발생하지 않음
std::move() 구현
#include <type_traits>
class Object
{
public:
Object() = default;
Object(const Object& obj) { std::cout << "copy ctor" << std::endl;}
Object(Object&& obj) { std::cout << "move ctor" << std::endl;}
};
template<class T>
constexpr std::remove_reference_t<T>&& move(T&& obj) noexcept
{
return static_cast<std::remove_reference_t<T> &&>(obj);
}
int main()
{
Object obj1;
Object obj2 = obj1; // copy
Object obj3 = move(obj1); // move
Object obj4 = move(Object()); // move
}
move() 함수는 lvalue와 rvalue 모두 받을 수 있도록 유연하게 설계하는 것이 좋음
그래서 move(T&& obj)로 선언했다면 reference collapsing 규칙에 의해 static_cast<T&&>(obj)에 lvalue가 들어왔을때 rvalue 캐스팅이 되지 않고 lvalue를 반환하여 move 생성자가 아닌 복사 생성자를 호출해버림;
레퍼런스 속성을 없애주는 std::remove_reference_t()를 사용하여 move() 함수를 구현해야함
복사 생성자와 move 생성자의 자동 생성 규칙
class String
{
public:
String() = default;
String(const String& obj) { std::cout << "String copy ctor" << std::endl;}
String(String&& obj) { std::cout << "String move ctor" << std::endl;}
String& operator=(const String&) { std::cout << "String copy assignment" << std::endl; return *this;}
String& operator=(String&&) { std::cout << "String move assignment" << std::endl; return *this;}
};
class Object
{
String name;
public:
Object() = default;
Object(const Object& obj) : name(obj.name) {}
Object& operator=(const Object& obj) { name = obj.name; return *this;}
Object(Object&& obj) : name(std::move(obj.name)) {}
Object& operator=(Object&& obj) { name = std::move(obj.name);return *this;}
};
int main()
{
Object obj1;
Object obj2 = obj1;
obj2 = obj2;
Object obj3 = std::move(obj1);
obj3 = std::move(obj1);
}
사용자가 복사 계열과 move 계열 함수를 모두 제공하지 않을 때
사용자가 복사 생성자(또는 복사 대입 연산자)만 제공하는 경우
복사 생성자만 제공한 경우 복사 대입 연산자를 컴파일러가 제공하는 것이 과연 바람직한 것일까 - C++11때 잘못된 설계란 걸 알게 됨
(복사 생성자를 사용자가 구현했다는 건 복사 대입 방법 또한 디폴트와 달라질 거란 의미, 사용자가 복사 대입 연산자까지 구현하는 것이 맞음)
사용자가 move 생성자(또는 move 대입 연산자)만 제공하는 경우
move 대입 연산자가 없으면 복사 대입 연산자를 사용하는데 복사 대입 연산자가 없으므로 에러 (삭제됨과 제공 안함을 구분)
class Object
{
String name;
public:
Object() = default;
// 복사 생성자 제공
Object(const Object& obj) : name(obj.name) {}
Object(Object&& obj) = default;
Object& operator=(Object&& obj) = default;
Object& operator=(const Object& obj) = default;
};
#include <cstring>
class Person
{
char* name;
int age;
public:
Person(const char* s, int a) : age(a)
{
name = new char[strlen(s) + 1];
strcpy_s(name, strlen(s) + 1, s);
}
~Person() { delete[] name; }
Person(const Person& p) : age(p.age)
{
name = new char[strlen(p.name) + 1];
strcpy_s(name, strlen(p.name) + 1, p.name);
}
Person& operator=(const Person& p)
{
if (this == &p) return *this;
age = p.age;
delete[] name;
name = new char[strlen(p.name) + 1];
strcpy_s(name, strlen(p.name) + 1, p.name);
return* this;
}
Person(Person&& p) noexcept
: name(p.name), age(p.age)
{
p.name = nullptr;
}
Person& operator=(Person&& p) noexcept
{
if ( this == &p) return *this;
delete[] name;
age = p.age;
name = p.name;
p.name = nullptr;
return* this;
}
};
int main()
{
Person robert("john", 30);
}
#include <string>
class Person
{
std::string name;
int age;
public:
Person(const std::string& s, int a) : name(s), age(a)
{
}
};
int main()
{
Person robert("john", 30);
}
STL을 잘쓰자!