태크놀로지

[Effective C++] 대입연산자, 제대로 메모리 관리를 하고 있는가? 본문

C++

[Effective C++] 대입연산자, 제대로 메모리 관리를 하고 있는가?

원택 2020. 11. 24. 18:25

1. 대입연산자는 *this의 참조자를 반환하게 하자 (convention)

int x,y,z;
x = y = z = 15;

위와 같이 대입연산은 여러개가 사슬처럼 엮일 수 있다.

x = (y = (z = 15));

또 하나의 특성은, 우측 연관 연산이라는 점이다.

 

class Widget{
public:
	Widget& operator= (const Widget& rhs){
		//...
		return *this;
	}
}

이렇게 대입 연산이 사슬처럼 엮이려면 대입연산자는 좌변인자에 대한 참조자를 반환하도록 구현되어야 한다. (일종의 convention)

+=, -=, *= 등 모든 형태의 대입연산자에서도 Convention이 적용된다.

 

2. operator=에서 자기대입에 대한 처리가 빠지지 않도록 하자

class Widget;

Widget w;
...
w = w;

자기대입이란, 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는것

 

// ex1)
for(int i= 0; ...) for(int j= 0; ...)
a[i] = a[j];
// ex2)
*px = *py;

이러한 상황이 일어나지 않을것이라고 착각할 수 있는데, 위와 같은 상황으로 인해 자기대입연산이 일어날 수 있다.

 

자기대입으로 인해 발생하는 상황

여러 곳에서 하나의 객체를 참조하는 상태(중복참조)가 발생한다.

  • 중복참조의 상황을 구현해야할경우, 기본클래스의 참조자나 포인터를 사용하자.

자기대입에 대해 안전하게 구현해보자.

class Resource {

};

class Widget {
private:
	Resource* pb; // 힙 할당한 리소스

public:
	void swap(Widget& rhs){}

public:
	// 안전하지 않은 operator=
	Widget& operator=(const Widget& rhs) {
		// 만약 this = rhs (자기대입)이라면
		// pb는 삭제된 자기 리소스를 복사하게 됨
		delete pb;
		pb = new Resource(*rhs.pb);
		return *this;
	}

	// 대책1 : 일치성검사
	Widget& operator=(const Widget& rhs) {

		// 객체가 같은지, 자기대입인지를 검사한다.
		if (this == &rhs) return *this;

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

	// 대책2 : 코드 문맥순서 변경
	Widget& operator=(const Widget& rhs) {

		// 원본포인터를 사본 포인터에 받아놓고, 사본포인터를 통해 원본데이터 삭제
		Resource* orig = pb;
		pb = new Resource(*rhs.pb);
		delete orig;

		return *this;
	}

	// 대책3 : 복사 후 맞바꾸기 (copy and swap)
	// 자기대입안전성 (+ 예외 안전성)
	Widget& operator=(const Widget& rhs) {

		Widget temp(rhs); // 객체 소멸자를 이용한 자원관리 방법
		swap(temp); // + 예외안전성 (자세한 부분은 자원관리파트에서 진행)
		return *this;
	}
};

안전하지 않은 operator=

만약 this = rhs (자기대입)이라면, pb는 삭제된 자기 리소스를 복사하게 된다.

 

대책1 :  일치성검사 operator=

객체가 같은지, 자기대입인지를 검사한다. 같을경우 복사과정없이 자기자신을 리턴한다.

 

대책2 : 코드 문맥순서 변경 operator=

원본포인터를 사본 포인터에 받아놓고, 사본포인터를 통해 원본데이터를 삭제한다.

 

대책3 : 복사 후 맞바꾸기 (copy and swap) operator=

자기대입안전성 뿐만아니라 swap함수에서의 예외 안전성까지 더해진다.

+ swap 함수 구현방법 (예외안전성에 대한 자세한 부분은 자원관리파트에서 진행)

 

3. 객체의 모든 부분을 빠짐없이 복사하자.

복사생성자와 대입연산자 선언을 하지 않을경우, 컴파일러가 자동으로 만들어냄.

-> 복사시 객체의 비트만 복사하기 때문에, 기본타입객체가 아닐경우 제대로 복사가 일어나지 않음.

 

클래스 상속 복사생성자,대입연산자 구현시 주의해야할 경우

class Customer {
private:
	int name;

public:
	Customer() : name(-1) {}
	virtual ~Customer() = default;

	Customer(const Customer& rhs) : name(rhs.name) {}
	Customer& operator=(const Customer& rhs) {
		name = rhs.name;
		return *this;
	}

	void SetName(const int _name) {
		name = _name;
	}
	const int GetName() const {
		return name;
	}
};

class PriorityCustomer : public Customer {
private:
	int priority;

public:
	PriorityCustomer() : priority(-1) {};
	virtual ~PriorityCustomer() = default;

	// 기본생성자의 데이터복사가 일어나지 않고있음.
	PriorityCustomer(const PriorityCustomer& rhs) : priority(rhs.priority){}
	PriorityCustomer& operator=(const PriorityCustomer& rhs) {
		priority = rhs.priority;
		return *this;
	}
};

int main()
{
	PriorityCustomer a;
	a.SetName(5);
	PriorityCustomer b(a);
	cout << b.GetName() << endl; // 5가 나와야하지만 기본값 -1이 나옴
}

PriorityCustomer 복사생성자,복사대입연산자에서 기본생성자의 데이터(name) 복사가 일어나지 않고 있다.

컴파일시 name은 a와 같은 5가 나와야하지만, 기본값 -1이 나온다.

 

// 파생클래스 복사뿐만 아니라, 기본클래스까지 복사
PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), priority(rhs.priority) {}
PriorityCustomer& operator=(const PriorityCustomer& rhs) {
	Customer::operator=(rhs);
	priority = rhs.priority;
	return *this;
}

다음과 같이 파생클래스의 데이터를 복사할 뿐만 아니라, 기본클래스의 복사생성자,대입연산자를 호출하여 기본클래스의 데이터까지 복사해주어야한다.

 

복사대입연산자와 복사생성자에서의 공통된 부분 분리

복사대입연산자에서 복사생성자를 호출하거나, 복사생성자에서 복사대입연산자를 호출하지마라.

생성자의 역할은 새로 만들어진 객체를 호출하는 것, 대입연산자'이미' 초기화가 끝난 객체에게 값을 대입하는것

  • 복사대입연산자에서 복사생성자를 호출 : 복사대입연산자에서는 이미 만들어진 객체인데, 복사생성자를 호출한다는것은 이미 존재하는 객체를 '생성'하라는 꼴이다.
  • 복사생성자에서 복사대입연산자를 호출 : 복사생성자에서 객체를 생성중에 있는데, 복사대입연산자를 호출한다는것은 아직 생성중인 객체에 값을 대입하는것이다.

해결책 : 제3의 함수에다 분리해놓고 양쪽에서 이것을 호출하여 사용

 

(1-3) 정리

  • 대입연산자는 *this의 참조자를 반환하도록 만들어라
  • operator= 구현시, 자기대입에 대해 제대로 처리하자. (1.원본객체와 복사대상객체의 주소비교, 2.문장의 순서를 적절히 조정, 3.copy and swap)
  • 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스부분을 빠뜨리지 않고 복사해야한다. *상속대입연산자 작성시 주의*
  • 클래스의 복사 함수를 구현할때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지마라. (제 3의 함수에다 분리해놓고 양쪽에서 이것을 호출하여 사용)

출처

effective c++ / 스콧 마이어스