배움 저장소
[홍정모의 따라하며 배우는 C++] 15. 의미론적 이동과 스마트 포인터 본문
15.1 이동의 의미와 스마트 포인터
Move Semantics and Smart Pointers
스마트 포인터를 이해하기 위하여 이동의 의미를 이해하자
RAII : Resource Acquisition Is Initialization, ... binds the life cycle of a resource to the lifetime of an object.
RAII를 위반하는 경우
- 동적할당 메모리를 저장하고 있는 포인터가 소멸될 때 동적할당메모리도 해제되어야 한다. 아래와 같은 경우 메모리를 해제하기 이전에 함수가 종료되어 메모리 누수가 발생하게 된다.
int *ptr = new int[10];
// work with dynamic mem
if (true) {
return;
}
delete [] ptr;
if 조건문에 메모리 해제 코드를 추가하자
if (true) {
delete [] ptr;
return;
}
이렇게 중간에 프로그램이 종료되는 경우가 생기면 메모리 해제를 잊을 수 있다. Smart Pointer가 이를 해결해준다.
auto_ptr
- auto_ptr로 스마트 포인터 개념을 이해해보자. c+11부터 사용하지 않고, c++17부터 사라졌다.
- Resource 클래스는 생성자와 소멸자가 호출될 때 각 문자열 출력으로 이를 알린다. 소멸자가 호출로 메모리 누수를 확인하자.
AutoPtr을 사용하여 동적 메모리를 할당하고, 메모리 해제를 자동으로 수행시켜 보자
- 갑작스럽게 프로그램이 종료되어도 메모리해제가 실행된다
class Resource {
public:
int m_data[100];
Resource(){ cout << "Resource created!" << endl; }
~Resource() { cout << "Resource destroyed!" << endl; }
};
template<class T>
class AutoPtr{
public:
T *m_ptr;
AutoPtr(T* ptr = nullptr):m_ptr(ptr){}
~AutoPtr(){
if(m_ptr != nullptr) delete m_ptr;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
int main() {
try {
AutoPtr<Resource> autoptr(new Resource);
if (true) { throw -1; }
cout << "try is done!" << endl;
}
catch (...) {
cerr << "Catching it!" << endl;
}
}
Resource created!
Resource destroyed!
Catching it!
auto_ptr의 한계
- 대입 연산자를 활용하여 auto_ptr을 다른 인스턴스에 할당해보자. 영역이 끝이나면 소멸자가 각각 호출되는데 둘 다 동일한 동적할당 메모리를 해제하여 에러가 발생한다.
template<class T>
class AutoPtr{
public:
T *m_ptr;
AutoPtr(T* ptr = nullptr):m_ptr(ptr){}
~AutoPtr(){
if(m_ptr != nullptr) delete m_ptr;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
int main() {
AutoPtr<int> ptr1(new int);
AutoPtr<int> ptr2;
cout << ptr1.m_ptr << endl; // >> 0088DE20
cout << ptr2.m_ptr << endl; // >> 00000000
ptr2 = ptr1;
cout << ptr1.m_ptr << endl; // >> 0088DE20
cout << ptr2.m_ptr << endl; // >> 0088DE20
}
Move Semantics
- move semantics는 동적할당 메모리, 포인터 소유권을 넘긴다
- 복사된 인스턴스로 인해 메모리해제가 두 번 실행되었다. 동적할당 메모리의 소유권을 이전하면 문제가 해결된다.
- 소유권 이전이 포함된 복사생성자, 대입연산자를 구현하자. 두 인스턴스가 동적할당 메모리를 공유할 일이 사라진다.
AutoPtr(AutoPtr& a) {
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
}
AutoPtr& operator = (AutoPtr& a) {
if(&a == this){ return *this; }
delete m_ptr;
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
return *this;
}
위를 구현하고 프로그램을 다시 실행시키면 에러가 사라진다.
template<class T>
class AutoPtr{
public:
T *m_ptr;
AutoPtr(T* ptr = nullptr):m_ptr(ptr){}
~AutoPtr(){
if(m_ptr != nullptr) delete m_ptr;
}
AutoPtr(AutoPtr& a) {
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
}
AutoPtr& operator = (AutoPtr& a) {
if(&a == this){ return *this; }
delete m_ptr;
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
int main() {
AutoPtr<int> ptr1(new int);
AutoPtr<int> ptr2;
cout << ptr1.m_ptr << endl; // >> 0131D678
cout << ptr2.m_ptr << endl; // >> 00000000
ptr2 = ptr1; // Move semantics
cout << ptr1.m_ptr << endl; // >> 00000000
cout << ptr2.m_ptr << endl; // >> 0131D678
}
- 위에서 구현된 복사연산자와 대입연산자는 값을 복사하지 않는다.
- value semantics(copy semantics)가 아닌 move semantics를 사용하고 있다.
- semantics의 종류는 세 가지다. reference semantics(pointer)도 있다.
Syntax VS Semantics
- Syntax : 컴파일 OX를 따지는 문법
- Semantics : 프로그래머가 구현하려는 의도/의미
auto_ptr의 극복할 수 없는 한계
- 함수의 매개변수로 auto_ptr을 사용할 수 없다. auto_ptr의 소유권이 지역변수로 이동해버리면 auto_ptr의 동적할당 메모리는 사라져버린다. 프로그래머가 이를 막기 위하여 여럿 노력을 해야한다.
- 이러한 문제를 해결하기 위해 더 좋아진 스마트 포인터가 등장했고 auto_ptr은 사용하지 않게되었다.
15.2 오른쪽-값 참조
R-value References
Move Semantics(소유권 이동)을 사용하는 경우와 사용하지 않는 경우를 구분할 수 있는 오른쪽-값 참조를 알아보자
R-value와 L-value 복습
- x는 L-value로 값 5를 저장공간 x에 저장하고 있다. 이 때 값 5는 R-value로 임시로 사용되었다가 사라진다.
int x = 5;
const int c_x = 10;
L-value References
- 참조하고 있는 값이 주소를 가지고 있으면 L-value Reference를 초기화할 수 있다.
int &ref1 = x; // OK, Modifiable L-value
int &ref2 = c_x; // Er! Non-modifiable L-value
int &ref3 = 5; // Er! R-value
const int& c_ref1 = x; // OK, Modifiable L-value
const int& c_ref2 = c_x;// OK, Non-modifiable L-value
const int& c_ref3 = 5; // OK, R-value
R-value Reference
- R-value만 가질 수 있는 참조자이다. Ampersand(&)를 두개 사용한다. L-value reference와 구분하기 위함이다. 곧 사라질 값을 참조한다는 개념이 낯설다. R-value와 L-value를 구분하기 위해서 만들어졌다. R-value Reference에 담긴 임시 값은 해당 변수와 소멸을 함께한다.
int&& r_ref1 = x; // Er! Modifiable L-value
int&& r_ref2 = c_x; // Er! Non-modifiable L-value
int&& r_ref3 = 5; // OK, R-value
R-value와 L-value를 구분하는 함수 오버로딩
- R-value Reference 자료형 매개변수는 R-value만 받는다. 주소를 가지지 않는 임시값만 인자로 받을 수 있다.
void distinguish(int& l_ref) {
cout << "L-value Reference" << endl;
}
void distinguish(int&& r_ref) {
cout << "R-value Reference" << endl;
}
int main() {
int x = 5;
int &ref = x;
int&& r_ref = 5;
distinguish(x); // >> L-value Reference
distinguish(ref); // >> L-value Reference
distinguish(r_ref); // >> L-value Reference
distinguish(5); // >> R-value Reference
}
R-value Reference를 활용하여 Move semantics 사용하기
- R-value Reference는 임시값이기 때문에 소유권을 이전해도 된다! L-value Reference로 받은 매개변수는 누군가 소유권을 가지고 있으므로 소유권을 가져올 경우 코드 밖에서 사용이 불가능하거나 에러를 발생시킨다.
- R-value ref와 L-value ref 오버로딩을 활용하여 move semantics를 활용할 수 있다.
15.3 이동 생성자와 이동 대입
Move constructors and Move assignment
Move Constructor(이동 생성자)는 복사 생성자와 달리 소유권을 이전해준다
- 이동생성자를 호출한 인스턴스에게 소유권을 넘긴다. 인자로 사용된 인스턴스의 소유권은 사라진다.
- Move semantics를 대입 연산자에서도 구현할 수 있다.
예제에 필요한 클래스이다.
- Resource 클래스는 동적 메모리를 할당 받아 관리한다.
- AutoPtr 클래스는 Resource 인스턴스를 동적으로 할당받아 관리한다.
class Resource {
public:
int *m_data = nullptr;
unsigned m_length = 0;
public:
Resource(unsigned length=0):m_length(length) {
cout << "Resource Constructor!" << endl;
m_data = new int [m_length];
}
Resource(const Resource& r): Resource(r.m_length) {
cout << "Resource Deep Copy Constructor!" << endl;
for (unsigned i = 0; i < m_length; ++i) {
m_data[i] = r.m_data[i];
}
}
~Resource() {
cout << "Resource Destructor!" << endl;
if(m_data != nullptr) delete[] m_data;
}
Resource& operator = (Resource& r) {
cout << "Resource Deep Copy assignmnet Operator!" << endl;
if(this == &r) return *this;
if(this->m_data != nullptr) delete [] m_data; // important to check n_ptr
this->m_length = r.m_length;
this->m_data = new int[this->m_length];
for (unsigned i = 0; i < m_length; ++i)
this->m_data[i] = r.m_data[i];
return *this;
}
void print() {
for(unsigned i=0; i<m_length; ++i)
cout << m_data[i] << " ";
cout << endl;
}
};
class AutoPtr{
public:
Resource* m_ptr;
AutoPtr(Resource* ptr = nullptr) :m_ptr(ptr) {
cout << "AutoPtr Constructor" << endl;
}
~AutoPtr() {
cout << "AutoPtr Destructor" << endl;
if (m_ptr != nullptr) delete m_ptr;
}
AutoPtr(const AutoPtr& a) {// L-value ref
cout << "AutoPtr Deep Copy Constructor" << endl;
//deep copy, slow.
m_ptr = new Resource;
*m_ptr = *a.m_ptr;// addres is diff, val is same
// call class Resource's = operator
}
AutoPtr& operator = (const AutoPtr& a) {
cout << "AutoPtr Copy assignmnet Operator" << endl;
if (&a == this) { return *this; }
if(this->m_ptr != nullptr) delete m_ptr;
m_ptr = new Resource;
*m_ptr = *a.m_ptr;// addres is diff, val is same
// call class Resource's = operator
return *this;
}
};
generateResource( ) 함수를 실행하여 어떤 생성자가 호출되는지 확인하자
AutoPtr generateResource() {
AutoPtr resource(new Resource(10000000));
return resource; // copied and return
}
int main() {
AutoPtr test_resource;
test_resource = generateResource();
}
주의) release 모드와 debug 모드가 다르게 작동한다
debug 모드:
- 인스턴스를 반환할 때 복사 생성자를 호출하고 원본은 소멸자를 호출한다.
- 복사 생성된 인스턴스는 R-value return값으로 사용된다. 영역 밖에서 값을 할당해주고 소멸한다.
release모드:
- 최적화 된 코드를 실행한다. 복사 생성자를 생략하고 내부 값을 영역 밖 변수에 값을 할당하고 난 뒤에 소멸한다.
1. Debug Mode || 2. Release Mode
AutoPtr Constructor || AutoPtr Constructor
Resource Constructor! || Resource Constructor!
AutoPtr Constructor || AutoPtr Constructor
AutoPtr Deep Copy Constructor || AutoPtr Copy assignmnet Operator
Resource Constructor! || Resource Constructor!
Resource Deep Copy assignmnet Operator! || Resource Deep Copy assignmnet Operator!
AutoPtr Destructor || AutoPtr Destructor
Resource Destructor! || Resource Destructor!
AutoPtr Copy assignmnet Operator ||
Resource Constructor! ||
Resource Deep Copy assignmnet Operator! ||
AutoPtr Destructor ||
Resource Destructor! ||
AutoPtr Destructor || AutoPtr Destructor
Resource Destructor! || Resource Destructor!
Timer 클래스를 추가하여 복사 생성자가 얼마나 비효율적인지 계산해보자. 동적할당 메모리의 모든 값을 복사생성자로 옮기면 속도는 느리다. Timer.h: 10.5 의존 관계 - [홍정모의 따라하며 배우는 C++] 10.객체들 사이의 관계에 대해
참고) cout 출력은 프로그램 실행 속도에 큰 영향을 준다. 다음 코드를 입력하여 cout 출력을 비활성화 할 수 있다
streambuf* orig_buf = cout.rdbuf();
cout.rdbuf(NULL); // disconnect cout from buffer stop cout
cout << "cout didn't work!! in here" << endl;
cout.rdbuf(orig_buf); // restore cout
cout << "cout work as expected" << endl;
테스트 코드
- 아래 코드를 실행하면 0.084 ~ 0.088의 시간이 소요된다. 대입 생성자 대신 이동 생성자를 사용해보자
int main() {
streambuf* orig_buf = cout.rdbuf();
cout.rdbuf(NULL); // disconnect cout from buffer stop cout
Timer timer;
{
AutoPtr test_resource;
test_resource = generateResourece();
}
cout.rdbuf(orig_buf);
timer.elapse();
cout << "cout work as expected" << endl;
}
AutoPtr 클래스에 이동 생성자와 이동 대입연산자를 구현해보자
- 이동 생성자와 이동 대입연산자는 값을 복사하지 않고 소유권만 이전한다.
AutoPtr(AutoPtr&& a):m_ptr(a.m_ptr) { // R-value ref
cout << "AutoPtr Move Constructor" << endl;
a.m_ptr = nullptr; // Move functions should always leave
} // both objects in a well-defined state
AutoPtr& operator = (AutoPtr&& a) { // R-value ref
cout << "AutoPtr Move assignmnet Operator" << endl;
if (&a == this) { return *this; } // prevent self assignment
if(this->m_ptr != nullptr) delete m_ptr;
// shallow copy
this->m_ptr = a.m_ptr;
a.m_ptr = nullptr; // Move functions should always leave
// both objects in a well-defined state
return *this;
}
테스트 코드를 다시 실행시키면 이동연산자와 이동 생성자가 실행됨을 알 수 있다. 속도는 훨씬 빨라졌다.
0.0093~0.0100이다. 복사 생성자를 사용했을 때는 0.084~0.088이다. Move Semantic을 사용하면 9배이상 빨라진다.
1. Debug Mode || 2. Release Mode
AutoPtr Constructor || AutoPtr Constructor
Resource Constructor! || Resource Constructor!
AutoPtr Constructor || AutoPtr Constructor
AutoPtr Move Constructor ||
AutoPtr Destructor ||
AutoPtr Move assignmnet Operator || AutoPtr Move assignmnet Operator
AutoPtr Destructor || AutoPtr Destructor
AutoPtr Destructor || AutoPtr Destructor
Resource Destructor! || Resource Destructor!
15.4 std::move
std::move( )함수는 매개변수를 R-value로 반환한다. 이를 활용하여 복사생성자 대신 이동생성자를 호출 할 수 있다.
Resource 클래스는 깊은 복사 생성자와 깊은 복사 대입연산자가 구현되었다
Autoptr 클래스는 깊은 복사 생성자, 깊은 복사 대입연산자, 이동생성자, 이동 대입 연산자가 구현되었다
class Resource {
public:
int *m_data = nullptr;
unsigned m_length = 0;
public:
Resource(unsigned length=0):m_length(length) {
cout << "Resource Constructor!" << endl;
m_data = new int [m_length];
}
Resource(const Resource& r): Resource(r.m_length) {
cout << "Resource Deep Copy Constructor!" << endl;
for (unsigned i = 0; i < m_length; ++i)
m_data[i] = r.m_data[i];
}
~Resource() {
cout << "Resource Destructor!" << endl;
if(m_data != nullptr) delete[] m_data;
}
Resource& operator = (Resource& r) {
cout << "Resource Deep Copy assignmnet Operator!" << endl;
if(this == &r) return *this;
if(this->m_data != nullptr) delete [] m_data; // important to check n_ptr
this->m_length = r.m_length;
this->m_data = new int[this->m_length];
for (unsigned i = 0; i < m_length; ++i)
this->m_data[i] = r.m_data[i];
return *this;
}
void print() {
for(unsigned i=0; i<m_length; ++i)
cout << m_data[i] << " ";
cout << endl;
}
};
class AutoPtr{
public:
Resource* m_ptr;
AutoPtr(Resource* ptr = nullptr) :m_ptr(ptr) {
cout << "AutoPtr Constructor" << endl;
}
~AutoPtr() {
cout << "AutoPtr Destructor" << endl;
if (m_ptr != nullptr) delete m_ptr;
}
AutoPtr(const AutoPtr& a) {// L-value ref
cout << "AutoPtr Deep Copy Constructor" << endl;
//deep copy, slow.
m_ptr = new Resource;
*m_ptr = *a.m_ptr;// addres is diff, val is same
// call class Resource's = operator
}
AutoPtr& operator = (const AutoPtr& a) {
cout << "AutoPtr Copy assignmnet Operator" << endl;
if (&a == this) { return *this; }
if(this->m_ptr != nullptr) delete m_ptr;
m_ptr = new Resource;
//deep copy
*m_ptr = *a.m_ptr;// addres is diff, val is same
// call class Resource's = operator
return *this;
}
//AutoPtr(const AutoPtr& a) = delete;
//AutoPtr& operator = (const AutoPtr& a) = delete;
AutoPtr(AutoPtr&& a):m_ptr(a.m_ptr) { // R-value ref
cout << "AutoPtr Move Constructor" << endl;
m_length = a.m_length;
a.m_ptr = nullptr; // Not necessary, buf safe
}
AutoPtr& operator = (AutoPtr&& a) { // R-value ref
cout << "AutoPtr Move assignmnet Operator" << endl;
if (&a == this) { return *this; } // prevent self assignment
if(this->m_ptr != nullptr) delete m_ptr;
// shallow copy
this->m_ptr = a.m_ptr;
a.m_ptr = nullptr; // Not necessary, but safe
return *this;
}
Resource* operator ->() const { return this->m_ptr; }
Resource& operator *() const { return *(this->m_ptr); }
};
아래의 코드를 실행하면 복사생성자가 호출된다.
int main() {
AutoPtr r1(new Resource(10000000));
cout << r1.m_ptr << endl;
AutoPtr r2 = r1;
cout << r1.m_ptr << endl;
cout << r2.m_ptr << endl;
}
Resource Constructor!
AutoPtr Constructor
005BDD40
AutoPtr Deep Copy Constructor
Resource Constructor!
Resource Deep Copy assignmnet Operator!
005BDD40
005BDC28
...
이동 생성자와 복사 생성자 모두 구현되었을 때 대입연산자는 복사생성자를 호출한다
std::move( )를 활용하여 이동생성자를 호출할 수 있다
- std::move( )는 매개변수를 r-value로 반환한다. R-value ref를 매개변수로 받은 이동 연산자를 호출한다.
AutoPtr r2 = std::move(r1);
Resource Constructor!
AutoPtr Constructor
00E8DF10
AutoPtr Move Constructor
00000000
00E8DF10
...
이동연산자가 호출된다. 소유권이 이전되고 기존의 인스턴스는 소유권을 잃는다.
std::move( )를 활용하여 swap 구현하기
- swap( ) 함수는 std::move( ) 함수를 활용하기 좋은 예제이다. 원본이 소유권을 잃어도 임시 값이기 때문에 상관없다. deep-copy보다 효율적으로 사용할 수 있다.
Resource 클래스에 멤버함수를 추가하였다. 이 함수는 멤버 배열의 모든 값을 특정값으로 만든다.
CopySwap( )함수와 MoveSwap( ) 함수를 구현하였다
void Resource::setAll(const int& v) {
for(unsigned i=0; i<m_length; ++i)
m_data[i] = v;
}
void CopySwap(AutoPtr& left, AutoPtr& right) {
AutoPtr temp = left; // Copy constructor
left = right; // copy assignment
right = temp; // copy assignment
}
void MoveSwap(AutoPtr& left, AutoPtr& right) {
AutoPtr temp(std::move(left)); // move constructor
left = std::move(right); // r-value assignment oper
right = std::move(temp); // r-value assignment oper
}
int main() {
AutoPtr r1(new Resource(3));
r1->setAll(3);
AutoPtr r2(new Resource(5));
r2->setAll(5);
r1->print();
r2->print();
// CopySwap(r1,r2);
MoveSwap(r1,r2);
r1->print();
r2->print();
}
MoveSwap( )함수를 보자. std::move( )을 사용하여 이동생성자와 이동 대입연산자를 호출했다.
...
3 3 3
5 5 5 5 5
AutoPtr Move Constructor
AutoPtr Move assignmnet Operator
AutoPtr Move assignmnet Operator
AutoPtr Destructor
5 5 5 5 5
3 3 3
...
move 함수에서 사용된 인자는 소유권을 잃을 수 있다. 이를 주의하자.
Standard library에서 std::move( ) 활용하기
- AutoPtr 내부에서 R-value 복사생성자와 R-value 대입연산자가 구현되었기 때문에 move( )를 사용했을 때 결과 값이 달라졌다. standard library에도 R-value 복사생성자와 R-value 대입연산자가 구현되어있기 때문에 move( )를 활용할 수 있다. string은 내부에 R-value를 위한 멤버함수가 구현되어 있어 move( )를 활용할 수 있다
vector<string> v;
string lvalue = "Hello";
/* Call function take L-value reference */
v.push_back(lvalue);
cout << lvalue << endl; // >> Hello
cout << v[0] << endl; // >> Hello
/* Call function take R-value reference*/
string rvalue_test = "World"; // loose
v.push_back(std::move(rvalue_test)); // Move semantics
cout << rvalue_test << endl; // >>
cout << v[1] << endl; // >> World
std::move( )를 활용하여 swap함수를 구현해보자
template<typename T>
void CopySwap(T& left, T& right) {
T temp = left; // Copy constructor
left = right; // copy assignment
right = temp; // copy assignment
}
template<typename T>
void MoveSwap(T& left, T& right) {
T temp(std::move(left)); // move constructor
left = std::move(right); // r-value assignment oper
right = std::move(temp); // r-value assignment oper
}
int main() {
string l = "Hello";
string r = "World";
cout << l << " " << r << endl;
MoveSwap<string>(l, r);
cout << l << " " << r << endl;
}
15.5 std::unique_ptr
데이터의 소유권이 단일 인스턴스인 경우에 unique_ptr을 사용한다.
- <memory.h>를 포함하여 사용할 수 있다. unique_ptr 인스턴스가 소멸될 때 동적할당 메모리를 해제해준다.
unique_ptr<int> u_iptr(new int(4));
unique_ptr<string> u_sptr(new string("Unique Pointer!"));
cout << *u_iptr << endl; // >> 4
cout << *u_sptr << endl; // >> Unique Pointer!
생성자를 직접 호출하기 보다 make_unique를 사용하자
- make_unqiue는 exception이 잘 구현되어있다.
- 코드가 간결해진다.
- new keyword를 사용하지 않아도 된다.
auto ai = std::make_unique<int>(4);
// unique_ptr<int>
auto as = std::make_unique<string>("Unique Pointer");
// unique_ptr<string>
cout << *ai << endl; // >> 4
cout << *as << endl; // >> Unique Pointer
unique_ptr은 대입연산자로 값을 복사할 수 없다
- unique_ptr에 담긴 정보는 다른 인스턴스와 공유할 수 없다. 문법적으로 금지되어있다.
unique_ptr<string> u_ptr1(new string("Hello"));
unique_ptr<string> u_ptr2;
u_ptr1 = u_ptr2; // Error! operator= is a deleted function
unique_ptr은 move를 활용하여 R-value reference 대입연산자를 사용할 수 있다
- 소유권을 잃은 unique_ptr은 nullptr이다. nullptr는 00000000이므로 false가 출력된다
unique_ptr<string> u_ptr1(new string("Hello"));
unique_ptr<string> u_ptr2;
cout << u_ptr1 << endl; // >> 00B58DE8
cout << u_ptr2 << endl; // >> 00000000
cout << boolalpha;
cout << "u_ptr1:" << static_cast<bool>(u_ptr1) << endl; // >> u_ptr1:true
cout << "u_ptr2:" << static_cast<bool>(u_ptr2) << endl; // >> u_ptr2:false
// u_ptr1 = u_ptr2; Error!
u_ptr2 = move(u_ptr1);
cout << "u_ptr1:" << static_cast<bool>(u_ptr1) << endl; // >> u_ptr1:false
cout << "u_ptr2:" << static_cast<bool>(u_ptr2) << endl; // >> u_ptr2:true
unique_ptr의 멤버함수 오버로딩
- 포인터처럼 -> 연산자와 *연산자 모두 사용가능하다. 멤버함수로 오버로딩되어 있다.
unique_ptr<string> u_ptr1(new string("Hello"));
u_ptr1->push_back('A');
cout << *u_ptr1 << endl; // >> HelloA
unique_ptr을 함수 매개변수로 사용하는 경우
- 아래 코드는 에러를 발생시킨다.
- unique_ptr은 특정 데이터를 소유할 수 있는 단일 인스턴스로 복사생성자를 호출할 수 없도록 지워버렸다.
void ArguTypeTest(unique_ptr<string> u_ptr){}
int main() {
unique_ptr<string> u_ptr(new string("Hello"));
ArguTypeTest(u_ptr); //Error! copied constructor is deleted!
}
아래와 같이 소유권을 이전시키면 호출가능하다. 소유권을 받은 지역변수는 소멸되고 원본은 nullptr이 된다.
void ArguTypeTest(unique_ptr<string> u_ptr){}
int main() {
unique_ptr<string> u_ptr(new string("Hello"));
cout << u_ptr << " " << *u_ptr << endl; // >> 01174ED0 Hello
ArguTypeTest(move(u_ptr)); // OK
cout << u_ptr << " " << *u_ptr << endl; // >> 00000000
}
소유권 되돌려받기
- 소유권을 가져간 지역변수에게서 소유권을 다시 뺏어오자. ArguTypeTest의 매개변수 local_uptr를 unique_ptr로 반환해주자. 함수 외부에서 소유권을 가져갈 수 있다.
unique_ptr<string> ArguTypeTest(unique_ptr<string> local_uptr){
return local_uptr;
}
int main() {
unique_ptr<string> u_ptr(new string("Hello"));
cout << u_ptr << " " << *u_ptr << endl; // >> 01174ED0 Hello
u_ptr = ArguTypeTest(move(u_ptr)); // Move Semantics
cout << u_ptr << " " << *u_ptr << endl; // >> 01174ED0 Hello
}
L-value reference를 매개변수로 사용하면 간단하다.
void ArguTypeTest(unique_ptr<string> &local_uptr){
// do sth..
}
unique_ptr가 저장하고 있는 값을 매개변수로 사용하고 싶다면 어떻게 해야할까?
- 멤버함수 get( )을 호출해보자. 데이터의 주소를 반환한다.
void ArguTypeTest(string* s) {
// do sth
}
int main() {
unique_ptr<string> u_ptr(new string("Hello"));
ArguTypeTest(u_ptr.get());
cout << *u_ptr.get() << endl; // >> Hello
cout << *u_ptr << endl; // >> Hello
}
unique_ptr을 사용할 때 주의하기
(1) 같은 데이터를 공유하는 unique_ptr는 만들지말자
- 아래 코드를 실행하면 중복하여 메모리를 해제한다. 프로그램이 중단되고 하단의 cout은 출력되지 않는다.
{
string* s_ptr = new string("Hello");
unique_ptr<string> u_ptr1(s_ptr);
unique_ptr<string> u_ptr2(s_ptr);
}
cout << "Program is work without any Problem" << endl;
(2) unique_ptr 이외의 주체가 메모리를 해제하면 unique_ptr 소멸자에서 중복하여 메모리를 해제한다
{
string* s_ptr = new string("Hello");
unique_ptr<string> u_ptr(s_ptr);
cout << *u_ptr << endl;
delete s_ptr;
}
cout << "Program is work without any Problem" << endl;
(3) 임시 값을 함수 인자로 사용할 때 unique_ptr보다 make_unique를 사용하자
void ArguTypeTest(unique_ptr<string> u_ptr) {
cout << &u_ptr << endl;
cout << u_ptr << endl;
cout << *u_ptr << endl;
}
int main() {
ArguTypeTest(unique_ptr<string>(new string("Hello")));// Bad
ArguTypeTest(make_unique<string>(string("Hello"))); // Good
ArguTypeTest(make_unique<string>("Hello")); // very Good
}
참고) https://stackoverflow.com/questions/53870522/why-use-stdmake-unique-in-c17
15.6 std::shared_ptr
소유권을 여러 인스턴스가 공유할 수 있는 shared_ptr을 사용해보자
- shared_ptr은 특정 데이터를 몇 개의 인스턴스와 공유하고 있는지 기록한다.
- 다른 인스턴스가 해당 데이터를 가지고 있지 않을 때에만 메모리를 해제한다.
class Resource {
public:
int *m_data = nullptr;
unsigned m_length = 0;
public:
Resource(unsigned length=0):m_length(length) {
cout << "Resource Constructor!" << endl;
m_data = new int [m_length];
}
~Resource() {
cout << "Resource Destructor!" << endl;
if(m_data != nullptr) delete[] m_data;
}
};
int main() {
Resource* r = new Resource(5);
{
shared_ptr<Resource> s_ptr1(r);
{
shared_ptr<Resource> s_ptr2(s_ptr1);
cout << &s_ptr1 << " " << s_ptr1 << endl;
cout << &s_ptr2 << " " << s_ptr2 << endl;
}
cout << "Out of inner block!" << endl;
}
cout << "Out of outer block!" << endl;
}
share_ptr은 같은 정보를 공유할 수 있다. 아래 s_ptr1과 s_ptr2가 담고 있는 값이 동일함을 확인하자.
>> Resource Constructor!
>> 00D3F758 010BE010
>> 00D3F748 010BE010
>> Out of inner block!
>> Resource Destructor!
>> Out of outer block!
share_ptr의 잘못된 사용
- 새로운 인스턴스를 만들 때 기존의 인스턴스를 참조해야 한다. 아래와 같이 데이터로 바로 초기화하면 인스턴스의 개수를 셀 수 없어 메모리 해제를 두 번 하게된다.
Resource* r = new Resource(5);
shared_ptr<Resource> s_ptr1(r);
shared_ptr<Resource> s_ptr2(r);
{
Resource* r = new Resource(5);
{
shared_ptr<Resource> s_ptr1(r);
{
shared_ptr<Resource> s_ptr2(r);
cout << &s_ptr1 << " " << s_ptr1 << endl;
cout << &s_ptr2 << " " << s_ptr2 << endl;
}
cout << "Out of inner block!" << endl;
}
cout << "Out of outer block!" << endl;
}
cout << "execute code without problem" << endl;
에러가 발생한다. 마지막 execute code without problem이 출력되지 않는다.
Resource Constructor!
00D3F9C4 00D5DF10
00D3F9B4 00D5DF10
Resource Destructor!
Out of inner block!
Resource Destructor!
실수를 줄이는 make_shared
- auto keyword와 함께사용하자. 코드가 간결해진다
auto good_ptr1 = std::make_shared<string>("Hello");
auto good_ptr2 = good_ptr1;
shared_ptr<string> s_ptr1(new string("Hello"));
shared_ptr<string> s_ptr2(s_ptr1);
15.7 순환 의존성 문제와 std::weak_ptr
Circular dependency issues and
shared_ptr의 순환 의존성 문제를 알아보자.
순환참조를 막아줄 수 있는 스마트 포인터 weak_ptr를 알아보자.
- shared_ptr은 해당 데이터를 공유하는 인스턴스 개수를 세고있다. weak_ptr은 그렇지 않아 순환참조를 막아줄 수 있다.
순환 의존성 문제를 보여주기 위한 클래스이다
class Person {
public:
string m_name;
shared_ptr<Person> m_partner;
Person(const string& name) : m_name(name) {
cout << m_name << " is created" << endl;
}
~Person() {
cout << m_name << " is deleted" << endl;
}
friend bool PartnerUp(shared_ptr<Person> &left, shared_ptr<Person> &right) {
if(!left || !right) return false;
left->m_partner = right;
right->m_partner = left;
cout << left->m_name << " " << right->m_name << " is partner\n";
return true;
}
};
Circular Dependency(순환 의존성)
- 아래 코드를 실행하면 소멸자가 호출되지 않는다. 서로의 멤버변수로 초기화되어 shared_ptr의 데이터를 공유하고 있는 인스턴스가 2개가 되었다.
- 인스턴스가 1개가 될 수 없어 소멸자는 호출되지 앟는다. 이를 순환 의존성 문제라 한다.
- 순환 의존성 문제는 메모리 누수를 발생시킨다.
int main() {
auto dwayne = make_shared<Person>("Dwayne");
auto vin = make_shared<Person>("Vin");
cout << dwayne.use_count() << endl; // >> 1
cout << vin.use_count() << endl; // >> 1
PartnerUp(dwayne, vin);
cout << dwayne.use_count() << endl; // >> 2
cout << vin.use_count() << endl; // >> 2
}
weak_ptr사용해보기
- 클래스 멤버변수 unique_ptr을 weak_ptr으로 바꾸어주자.
- 변경 이후 위 코드를 실행하면 소멸자가 호출되고 메모리가 해제된다.
//shared_ptr<Person> m_partner; // change this
weak_ptr<Person> m_partner; // to this
문제점: weak_ptr는 내부에 저장된 멤버에 접근할 수 없다
int main() {
auto dwayne = make_shared<Person>("Dwayne");
auto vin = make_shared<Person>("Vin");
PartnerUp(dwayne, vin);
cout << dwayne->m_partner->m_name;// Error!
cout << vin->m_partner->m_name; // Error!
}
weak_ptr에 저장된 데이터를 사용하려면 shared_ptr로 변환시켜주어야 한다
- weak_ptr 클래스의 멤버함수 .lock( )을 사용하자. shared_ptr로 변환시킨다.
const shared_ptr<Person> Person::getWeakPtrData() {
return m_partner.lock();
}
위를 활용하면 에러없이 실행가능하다.
int main() {
auto dwayne = make_shared<Person>("Dwayne");
auto vin = make_shared<Person>("Vin");
PartnerUp(dwayne, vin);
cout << dwayne->getWeakData() << endl;
cout << vin->getWeakData() << endl;
//cout << dwayne->m_partner.lock() << endl;
//cout << vin->m_partner.lock() << endl;
}
'Programming Language > C++' 카테고리의 다른 글
[홍정모의 따라하며 배우는 C++] 17. std::string 문자열 클래스 (0) | 2022.01.02 |
---|---|
[홍정모의 따라하며 배우는 C++] 16. 표준 템플릿 라이브러리 (0) | 2022.01.01 |
[따라하며 배우는 C++] 14. 예외처리 (0) | 2021.12.30 |
[홍정모의 따라하며 배우는 C++] 13. 템플릿 (0) | 2021.12.30 |
[홍정모의 따라하며 배우는 C++] 12. 가상함수들 (0) | 2021.12.28 |