대입 연산자는 *this의 참조자를 반환하게 하자
대입 연산은 여러 개가 사슬처럼 엮일 수 있는 성질을 갖고 있다.
x = y = z = 15;
대입 연산이 가진 또 하나의 특성은 바로 우측 연관 연산이라는 점
x = ( y = ( z = 15)));
15가 z에 대입되고, 그 대입 연산의 결과가 y에 대입된 후에, y에 대한 대입 연산의 결과가 x에 대입되는 것
이렇게 대입 연산이 사슬처럼 엮이려면 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있을 것
만드는 클래스에 대입 연산자가 혹시 들어간다면 이 관례를 지키는 것이 좋다.
"좌변 객체의 참조자를 반환하게 만들자"라는 규약은 단순 대입형 연산자 말고도 모든 형태의 대입 연산자에서 지켜져야 한다. 따르지 않고 코드를 작성하더라도 컴파일이 안 된다거나 하는 것은 아니다. 하지만 이 관례는 모든 기본 제공 타입들이 따르고 있을 뿐만 아니라 표준 라이브러리에 속한 모든 타입에서도 따르고 있다는 점은 무시 못할 것
operator=에서는 자기 대입에 대한 처리가 빠지지 않도록 하자
자기 대입이란, 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말함
가리키는 대상이 같으면 자기 대입이 된다. 언뜻 보기에 명확하지 않은 이러한 자기 대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 상태, 다시 말해 중복 참조라고 불리는 것 때문이다. 같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려하는 것이 일반적
같은 클래스 계통에서 만들어진 객체라 해도 굳이 똑같은 타입으로 선언할 필요는 없다.
파생 클래스 타입의 객체를 참조하거나 가리키는 용도로 기본 클래스의 참조자나 포인터를 사용해도 된다.
자원 관리 용도로 항상 객체를 만들어야 할 것, 이렇게 만든 자원 관리 객체들이 복사될 때 나름대로 잘 동작하도록 코딩
이때 조심해야 하는 것이 대입 연산자, 신경 쓰지 않아도 자기 대입에 대해 안전하게 동작해야 한다.
자원 관리를 하기엔 어려운 일, 어쩌면 자원을 사용하기 전에 해제할 수도 있다.
자기 참조 문제는 operator= 내부에서 *this(대입되는 대상)와 새롭게 만들 객체가 같은 객체일 수도 있기에 좋지 않다.
둘이 같은 객체일 경우 delete 연산자가 *this 객체의 비트맵에만 적용되는 것이 아니라 새로운 객체까지 적용되어 버린다. 이 함수가 끝나는 시점이 되면 해당 객체는 자신의 포인터 멤버를 통해 물고 있던 객체가 어처구니없게도 삭제된 상태가 되는 불상사를 당하게 된다.
이런 에러에 대한 대책은 operator=의 첫머리에서 일치성 검사를 통해 자기 대입을 점검하는 것
이전 버전의 operator=이 자기 대입에 안전하지 못할 뿐만 아니라 예외에도 안전하지 않다.
예외 안전성에 대해서는 이번 것도 여전히 문젯거리를 안고 있다.
특히 신경 쓰이는 부분이 'new Bitmap' 표현식, 이 부분에서 예외가 터지게 되면( 동적 할당에 필요한 메모리가 부족하다든지 Bitmap 클래스 복사 생성자에서 예외를 던진다든지 해서), 객체는 결국 삭제된 Bitmap을 가리키는 포인터만 가지게 됩니다. 이런 포인터는 delete 연산자를 안전하게 적용할 수도 없고, 안전하게 읽는 것조차 불가능
operator=을 예외에 안전하게 구현하면 대개 자기 대입에도 안전한 코드가 나오게 되어 있습니다.
원본 비트맵을 복사해 놓고, 복사해 놓은 사본을 포인터가 가리키게 만든 후, 원본을 삭제하는 순서로 실행하면 된다.
이 방법이 자기 대입을 처리하는 가장 효율적인 방법이라고는 할 수 없겠지만, 동작에는 아무 문제가 없다.
효율이 너무나 신경 쓰인 나머지, 일치성 테스트를 함수 앞단에 도로 붙여 놓고 싶을 수도 있다.
하지만 자기 대입은 자주 일어나지 않는다. 일치성 검사 코드가 들어가면 그만큼 코드가 커지는 데다가, 처리 흐름에 분기를 만들게 되므로 실행 시간 속력이 줄어들 수 있다.
예외 안전성과 자기 대입 안전성을 동시에 가진 operator=을 구현하는 방법으로, 문장의 실행 순서를 수작업으로 저장하는 것 외에 다른 방법이 하나 더 있다.
* operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들자.
원본 객체와 복사 대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 써도 된다.
* 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인
객체의 모든 부분을 빠짐없이 복사
객체의 안쪽 부분을 캡슐화한 객체 지향 시스템 중 설계가 잘 된 것들을 보면, 객체를 복사하는 함수가 두 개만 있다.
복사 생성자와 복사 대입 연산자, 이 둘을 통틀어 객체 복사 함수라고 부른다
객체 복사 함수는 컴파일러가 필요에 따라 만들어내기도 한다.
컴파일러가 생성한 복사 함수는 저절로 만들어졌지만 동작은 기본적인 요구에 아주 충실하다. 복사되는 객체가 갖고 있는 데이터를 빠짐없이 복사한다.
객체 복사 함수를 선언한다는 것은 컴파일러가 만든 기본 동작이 별로라는 것
부분 복사 이어도 컴파일러는 그냥 실행한다.
클래스에 데이터 멤버를 추가했으면, 추가한 데이터 멤버를 처리하도록 복사 함수를 다시 작성할 수밖에 없다.
클래스 상속으로 인해서 파생된 클래스가 상속한 데이터의 멤버들의 사본도 클래스에 들어 있을 때, 복사를 하고 있지 않는다면 기본 생성자에 의해 초기화된다. 복사가 아니라 기본적인 초기화가 된다.
복사 대입 연산자가 기본 클래스의 데이터 멤버를 건드릴 시도도 하지 않고 때문에, 기본 클래스의 데이터 멤버는 변경되지 않고 그대로 있게 된다.
파생 클래스에 대한 복사 함수를 스스로 만든다고 결심했다면 기본 클래스 부분을 복사에서 빠뜨리지 않도록 주의
기본 클래스 부분은 private 멤버일 가능성이 높기 때문에, 직접 건드리긴 어렵다.
파생 클래스의 복사 함수 안에서 기본 클래스의 복사 함수를 호출해야 한다.
즉, 해당 클래스의 데이터 멤버를 모두 복사, 클래스가 상속한 기본 클래스의 복사 함수도 호출해야 한다.
양대 복사 함수(복사 생성자와 복사 대입 연산자)는 비슷하기 때문에 한쪽에서 다른 쪽을 호출하면 된다고 생각하겠지만 절대 불가능하다.
복사 대입 연산자에서 복사 생성자를 호출하는 것부터 말이 안 된다. 객체를 '생성'하는 것이기 때문이다.
특정한 조건에서 객체의 데이터가 훼손되어 버릴 수 있어서 매우 위험하다.
복사 생성자에서 복사 대입 연산자를 호출하는 것 또한 말도 안 된다.
생성자의 역할은 새로 만들어진 객체를 초기화하는 것, 대입 연산자의 역할은 이미 초기화가 끝난 객체에게 값을 주는 것, 초기화된 객체에만 적용된다, 하지만 생성 중인 객체에다가 대입이라는 건, 초기화된 객체에 대해서만 의미를 갖는 동작을 아직 초기화도 안 된 객체에 대해 한다는 것
양쪽에서 겹치는 부분을 별도의 멤버 함수에 분리해 놓은 후에 이 함수를 호출하게 만드는 것
대게 이런 용도의 함수는 private 멤버로 두는 경우가 많고, 이름이 init으로 시작하는 경우도 많다.
안전할 뿐만 아니라 검증된 방법이므로, 복사 생성자와 복사 대입 연산자에 나타나는 코드 중복을 제거하는 방법으로 사용하는 것을 사용하는 것이 좋다.
'C++ > Effective' 카테고리의 다른 글
자원 관리(1) (0) | 2022.02.21 |
---|---|
생성자, 소멸자, 대입 연산자(2) (0) | 2022.02.10 |
생성자, 소멸자, 대입 연산자(1) (0) | 2022.02.04 |
객체 초기화 (0) | 2022.01.31 |
const (0) | 2022.01.25 |