Post

[ STUDY/Effective C++ ] 2022. 10. 28. 23:20

항목13에서 본 shared_ptr(auto_ptr은 이후에 삭제되었다고한다)은 힙이 아닌 다른 자원에는 맞지않다라는 견해가 일반적이다.

하지만 모든 자원이 힙에서 생기지는 않는다. 그래서 자원 관리 클래스를 우리가 스스로 만들어야할 필요성이 있다.

 

class Lock{
public:
    explicit Lock(Mutex *pm)
    :mutexPtr(pm)
    { lock(mutexPtr);}
    
    ~Lock() {unlock(mutexPtr);}
    
    private:
    Mutex *mutexPtr;
};

잠금을 관리하는 클래스를 하나 만들고 , RAII법칙을 따라한다고하자. 

Lock m11(&m);
Lock m12(m11);

하지만 여기서 복사를 하게 된다면 어떻게 될까?

 

복사할때 이루어지는 동작과 관련해서 선택지를 고를 수 있다.

 

1. 복사를 금지한다.

복사하면 안되는 RAII클래스(위와 같은 Lock)의 경우에는 복사가 아예 안되도록 막아놓아야한다.

이 경우에는 복사함수를 private 멤버로 만들면된다.

 

2. 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.

해당 자원을 참조화는 객체의 개수에 대한 카운트를 증가시키는 식으로 RAII 객체의 복사 동작을 만들어야한다.

shared_ptr이 이러한 방식을 사용하고있다.

 

하지만, shared_ptr의 경우에는 참조카운트가 0이되면 삭제가 되기때문에 Lock을 해제만 하고싶지 삭제를 하고싶지않은 사람에게는 다소 목적이 안맞을 수 있다.

class Lock {
public:
    explict Lock(Mutex *pm)
    :mutexPtr(pm, unlock) //삭제자로 umlock함수 사용
    {
    lock(mutexPtr.get());
    }
private:
    std::tr1::shared_ptr<Mutex> mutexPtr; //원시포인터 대신, shared_ptr사용
};

이런경우에는 shared_ptr에 '삭제자'(deleter)라는 것이 있다. 삭제자란 shared_ptr의 참조 카운트가 0이되면 호출되는 함수 혹은 함수객체를 일컫는 말이다. 이 삭제자를 shared_ptr 생성자의 두번째 매개변수에다가 선택하여 넣어줄 수가 있다.

 

이러면 소멸자를 선언할 필요 없이 비정적데이터 멤버(mutexPtr)의 소멸자가 자동으로 호출되는데, mutexPtr의 소멸자는 shared_ptr의 삭제자가 자동으로 호출되어진다!

 

3. 관리하고 있는 자원을 진짜로 복사한다.

 자원관리 객체를 복사하면 그 객체의 자원까지 복사되어야하기때문에 깊은 복사를 수행해야한다. 

 

4. 관리하고 있는 자원의 소유권을 옮긴다.

 

Post

[ STUDY/Effective C++ ] 2022. 9. 22. 12:40

자원을 객체에 넣음으로써,  C++가 자동으로 호출해 주는 소멸자에 의해 해당 자원을 저절로 해제할 수 있다.

 

표준라이브러리에 auto_ptr클래스가 있는데, 이 클래스는 포인터와 비슷하게 동작하는 스마트포인터로서 가리키고있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계되어있다.

void f()
{
    Investment *pInv(createInvestment());
    //createInvestment() 는 Investment 클래스의 객체를 동적할당하고 포인터 반환함수
    ...
    delete pInv;
 }
void f()
{
    std::auto_ptr<Investment>pInv(createInvestment());
    ...
 }

위의 코드에서는 delete에 도달하기전에 빠져나갈 요소들이 분명 많아보이지만, 밑에 auto_ptr을 쓰는 경우에는 위의 코드에서 생길 수 있는 자원누출을 막을 수 있다.

 

여기서, 자원관리에 객체를 사용하는 방법의 두가지 특징을 알 수 있다.

1. 자원을 획득한 후에 자원 관리 객체에게 넘긴다.

자원획득 초기화( Resource Acquisition Is Initialization: RAII) 라는 용어가 있다. 자원획득과 자원관리 객체의 초기화가 한문장에서 이루어진다는것이다. 

2.  자원관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.

 

 

auto_ptr은 자신이 소멸될때 가리키고 있는 대상에 대해서도 자동으로 delete를 해준다. 

그렇기때문에 객체를 가리키는 auto_ptr이 둘이상이라면 자원이 두번 이상 삭제되는 결과가 나온다.

이러한 상황을 막기위해 auto_ptr객체를 복사하면 원본 객체는 null로 만든다.

 

하지만 auto_ptr을 쓰지 못하는 상황(STL 컨테이너)이라면 참조카운팅 방식 스마트 포인터(reference-counting smart pointer: RCSP)방식 또한 좋다.

가비지 컬렉션과 비슷하게 어떤 자원을 가리키는 외부 객체의 개수를 유지하고있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트포인터인데, 가비지 컬렉션과 다르게 서로가리키고 있는 상황에서도 없앨 수 있단게 특징이다.

 

void f()
{
    std::tr1::shared_ptr<Investment>pInv(createInvestment());
    // tr1::shared_ptr이 대표적인  RCSP이다.
    ...
 }

 

하지만 여기서 중요하게 봐야할건, auto_ptr, shared_ptr 둘다 동적으로 할당한 배열에 대해서는 쓰지 못한다. 

왜냐하면 동적으로 할당도니 배열은 vector나 string으로 대체할 수 있기때문이다.

 

 

Post

[ STUDY/Effective C++ ] 2022. 9. 8. 16:26

복사생성자와 복사대입연산자를 통틀어 객체 복사 함수(copy function)이라고한다. 객체 복사함수는 컴파일러가 자동으로 만들어내기도하며 복사되는 객체가 갖고있는 데이터를 빠짐없이 복사하는 동작을하고있다.

 

하지만 컴파일러가 아닌 우리가 객체 복사함수를 만드는것이 컴파일러에게는 꽤나 기분이 나쁠 수도있다. 그래서 우리에게 복사함수 구현이 틀려도 조용히 있을경우가 있다.

 

class Customer{
public:
	...
	Customner(const Customer& rhs);
	Customer& operator=(const Customer& rhs);
	...
private:
	std::string name;
};



Customer::Customer(const Customer& rhs)
: name(rhs.name) //rhs데이터 복사
{
	logCall("customer copy constructor");
}

Customer& Customer::oprator=(const Customer& rhs)
{
	loCall("Customer copy assignment operator");
	name = rhs.name; //rhs데이터 복사
	return *this;
}

복사함수를 직접 구현하였을 경우의 클래스이다. 문제될것이 없어보이지만 여기서 데이터 멤버 하나를 추가하면 문제가 생기기 시작한다.

private:
	std::string name;
	Data lastTransaction;

추가가되는 순간 이전에 있던 복사함수들은 완전 복사가 아닌 부분복사가된다. 이럴경우 우리는 클래스에 데이터 멤버를 추가하였다면 추가한 데이터 멤버를 처리하도록 복사 함수를 다시 작성해야한다.(생성자도 다시!)

 

 

그리고, 클래스를 상속한다면 조금 더 머리가 어지러워진다.

class PriorityCustomer: public Customer{
public:
	...
	PriorityCustomer(const PriorityCustomer& rhs);
	PriorityCustomer& operator=(const PriorityCustomer& rhs);
	...
private:
	int priority;
};



PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority) //rhs데이터 복사
{
	logCall("PriorityCustomer copy constructor");
}

PriorityCustomer& PriorityCustomer::oprator=(const PriorityCustomer& rhs)
{
	loCall("PriorityCustomer copy assignment operator");
	priority = rhs.priority; //rhs데이터 복사
	return *this;
}

PriorityCustomer를 복사하는것같지만, 사실 Customer로부터 상속한 데이터 멤버들은 복사가 되지않고있다.

그나마 복사생성자에서는 Customer에서 상속한 name이나 lastTransaction을 초기화해주겠지만, 복사 대입연산자에서는 기본 클래스 데이터 멤버가 변경되지않고 그대로 있게된다.

 

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), ////////기본 클래스의 복사 생성자 호출
	priority(rhs.priority) 
{
	logCall("PriorityCustomer copy constructor");
}

PriorityCustomer& PriorityCustomer::oprator=(const PriorityCustomer& rhs)
{
	loCall("PriorityCustomer copy assignment operator");
    ////////////////////기본 클래스 부분 대입
    Customer::operator=(rhs);
    /////////////////////
	priority = rhs.priority; 
	return *this;
}

그럴 경우에는 기본 클래스의 복사 생성자를 호출하거나, 기본 클래스 부분을 대입하면된다. 객체 복사함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠트리지말고 복사해야한다. 

Post

[ STUDY/Effective C++ ] 2022. 8. 31. 19:03

 

Widget w;
w= w; //자기대입
a[i] = a[j]; //자기대입 가능성이 큼

중복참조로 인해 자기대입이 발생하는경우들이 있다. 자기대입이 발생하게되면 자원관리를 제대로 못할 수 있고 자원관리는 곧 프로그램 자체와 밀접하게 관련이 있다.

 

 

Widget&
Widget::operator=(const Widget& rhs)
{
    delete pb; // 현재 비트맵 사용을 중지
    pb = new Bitmap(*rhs.pb); //rhs비트맵을 사용하도록 함
    
    return *this;
}

여기서 *this와 rhs가 같은 객체일 가능성이 있다. 만약 그렇다면 rhs의 객체까지 delete가 적용되어 해당 Widget객체는 자신의 포인터멤버를 통해 가리키던 객체가 삭제되는 상태가 발생하게된다.

 

 

 

Widget&
Widget::operator=(const Widget& rhs)
{
    if(this==&rhs) return *this;

    delete pb; 
    pb = new Bitmap(*rhs.pb); 
    
    return *this;
}

이를 해결하기위해서는 if문을 통해 일치성 검사를 하여 자기대입을 점검하는 방법이 있다. 

하지만 이또한 자기대입에 대한 문제점만 해결할 뿐, 만약 자기대입이 아니라서 다음 단계로 갔는데

new Bitmap에서 예외가 터지는 경우는 아직 해결이 되지 않았다.

 

Widget&
Widget::operator=(const Widget& rhs)
{
    Bitmap *pOrig = pb; //원래 pb 어딘가에 기억해둠
    pb = new Bitmap(*rhs.pb); //pb가 *pb의 사본을 가리키게함
    delete pOrig; //원래 pb삭제
    
    return *this;
}

코드의 순서를 바꿔주기만 했을뿐인데도, 이제는 new Bitmap부분에서 예외가 발생하더라도 pb는 변경되지않은 상태가 유지되기때문에 예외에서 안전하다.

그리고 일치성 검사를 하는 if문을 없애도 괜찮은 이유는 operator=을 예외에 안전하게 구현하면 대부분이 자기대입에도 안전한 코드가 나온다. 또 일치성 검사를 하는만큼 코드가 커지는데다 실행 시간 속력이 줄어들 수 있으며 등등의 비효율적인 모습이 결과적으로 나타날 수 있으니 되도록 피할 수 있으면 하지않는것이 좋다.

 

class Widget {
	...
	void swap(Widget& rhs);  //*this의 데이터 및 rhs의 데이터를 맞바꿈
	...
};

Widget& Widget::operator=(const Widget& rhs)
{
	Widget temp(rhs);  // rhs의 데이터에 대한 사본을 만듬 (값에 의한 전달을 이용하면 사본생김)
	swap(temp);  // *this의 데이터를 그 사본의 것과 맞바꿈
	return *this;
}
/*위의 코드를 좀 더 깔끔하게*/
Widget& Widget::operator=(const Widget rhs) //값에 의한 전달에 의해 매개변수로 사본생성!
{
	
	swap(rhs);  // *this의 데이터를 사본의 것과 맞바꿈
	return *this;
}

 

예외안정성과 자기대입 안정성을 동시애 가진 operator=을 구현하는 또 다른 방법으로는 '복사 후 맞바꾸기'가 있다.

swap함수를 이용하는데, 여기서 쓰인 swap함수는 클래스가 기존에 가지고있던 포인터변수를 매개변수로 받은 변수의 포인터와 맞바꾼다는 의미인것같다. 

그래서 이를 이용하여 사본과 맞바꾸게되면 opwerator=을 자기대입과 예외에 대해서 안정성을 가지고 구현할 수 있다.

여기에 대한 자세한 설명은 항목 29에서 더 확인해볼 수있다.

 

 

▲ top