개요
C++에서 operator=를 구현할 때 자주 간과되는 중요한 문제가 있습니다. 바로 '자기대입(self-assignment)'입니다. 자기대입은 객체가 자신에게 할당되는 상황을 말하며, 이를 적절히 처리하지 않으면 예기치 않은 버그와 자원 누수를 초래할 수 있습니다. 이번 글에서는 Effective C++의 11번째 항목을 바탕으로, 자기대입 문제의 본질과 이를 효과적으로 해결하는 방법에 대해 알아보겠습니다.
자기대입 문제의 이해
자기대입이 왜 문제가 될까요? 다음과 같은 코드를 생각해봅시다:
class Widget {
private:
int* data;
public:
Widget& operator=(const Widget& rhs) {
delete data; // 기존 데이터 삭제
data = new int(*rhs.data); // 새 데이터 할당 및 복사
return *this;
}
};
Widget w;
w = w; // 자기대입 상황
C++
복사
이 코드에서 w = w와 같은 자기대입이 발생하면, data가 삭제된 후 같은 data를 참조하려 하기 때문에 미정의 동작(undefined behavior)이 발생합니다. 이는 심각한 버그로 이어질 수 있습니다.
전통적인 해결 방법
자기대입을 처리하는 가장 간단한 방법은 대입 연산 시작 부분에서 자기대입 여부를 확인하는 것입니다:
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) return *this; // 자기대입 체크
delete data;
data = new int(*rhs.data);
return *this;
}
C++
복사
이 방법은 자기대입 문제를 해결하지만, 여전히 예외 안전성 문제가 남아있습니다. new int(*rhs.data)에서 예외가 발생하면 Widget은 불완전한 상태가 됩니다.
예외 안전성을 고려한 개선
예외 안전성을 확보하려면 순서를 조정하여 복사를 먼저 수행한 후 삭제하는 방식을 사용할 수 있습니다:
Widget& Widget::operator=(const Widget& rhs) {
int* pOrig = data;
data = new int(*rhs.data); // 새 데이터를 먼저 복사
delete pOrig; // 그 후 원본 삭제
return *this;
}
C++
복사
이 방식은 자기대입과 예외 상황 모두에 안전합니다. 하지만 더 간결하고 우아한 해결책이 있습니다.
Copy and Swap 기법
'copy and swap' 관용구를 사용하면 자기대입과 예외 안전성 문제를 동시에 해결할 수 있습니다:
class Widget {
private:
int* data;
public:
void swap(Widget& other) noexcept {
std::swap(data, other.data);
}
Widget& operator=(Widget rhs) { // 주목: 값으로 전달받음
swap(rhs);
return *this;
}
};
C++
복사
이 방식은 효율적이며, 자기대입과 예외에 대해 안전합니다.
개인적 견해
C++의 연산자 오버로딩은 언어의 강력한 기능이지만, 동시에 많은 복잡성을 내포하고 있습니다. 자기대입 처리를 통해 이러한 복잡성의 한 측면을 직접 경험할 수 있었습니다. 이는 단순히 C++의 특정 기능에 국한된 것이 아니라, 견고한 소프트웨어 설계의 기본 원칙을 반영하고 있다는 점에서 더욱 의미가 있습니다.
결론
operator=에서의 자기대입 처리는 단순해 보이지만, 깊이 있는 프로그래밍 원칙을 담고 있습니다. 이를 통해 우리는 연산자의 동작에 대해 더 깊이 이해할 수 있으며, 예외 안전성과 효율성이라는 두 마리 토끼를 모두 잡는 방법을 배울 수 있습니다. 이러한 세심한 고려는 고품질의 C++ 코드 작성에 필수적이며, 더 나아가 모든 프로그래밍 언어에서 적용될 수 있는 중요한 사고방식을 기르는 데 도움이 됩니다.