2장. 생성자, 소멸자 및 대입연산자 🎺
안녕하세요! 두두코딩 입니다 ✋
오늘은 Effective C++ 2장 중 항목 6번째에 대해 알아보겠습니다.
🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!
궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇
컴파일러가 만드는 기본 멤버가 필요 없는 경우
우리가 부동산 중개 시스템을 만든다고 가정해보자. (지금 시점에는 부동산이 핫하다.. ♨) 중개 시스템을 만들때 가장 중요한 거는 파는 집을 클래스로 만드는 것이다.
class HomeForSale() { ... };
이 부동산 중개 시스템에서 가장 중요한 것은 모든 자산 즉, 부동산은 세상에 하나밖에 없는 고유한 것이라는 것이다. 즉, 모양은 비슷할지라도 동일한 집은 없다는 말이다. 따라서, Copy 본은 있으면 절대 안된다는 점을 기억하고 시스템을 만들어야한다.
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 복사 생성자
h1 = h2; // 복사 대입 연산자
우리는 위와같이, 복사 생성자와 복사 대입 연산자를 사용할 수 없게 만들어야 고유한 자산 을 유지할 수 있다. 생각해보자. 우리는 복사 생성자와 복사 대입 연산자를 사용하지 않기 위해 만들지 않는다고 가정해보자.
다들 기억하겠지만, 이전 포스팅에서 다룬 개념으로 컴파일러는 기본 멤버함수들을 만들어 준다. 따라서, 우리가 안쓸 것이라고 해도 컴파일러가 만들어버려 고유한 자산을 유지할 수 없게 된다.
그럼 컴파일러가 만들 수 없게 할려면 사용자 정의를 통해 직접 생성자 혹은
연산자를 만들어줘야한다. 그럼 우리는 이런 고민을 하게 된다. “어.. public
영역에 넣으면 불리는거아니야..”
해당 고민을 하기 전에.. 아래의 룰을 기억해보자.
❗ 누가 생성자 혹은 연산자를 꼭 public
접근 지정자에 넣으라고 했는가?
그냥 우리는 익숙해서 public
에 넣는 것을 생각했을 것이다. 즉, 접근지정자는
사용자가 지정해서 넣어주면된다. 컴파일러가 생성하지 못하고 외부에서 접근하지
못하도록 우리는 private
에 적게 될 경우 해당 문제를 막을 수 있다.
복사 개념을 private 영역으로 옮기자
우리는 위의 해결 방법으로 private
영역에 복사 생성자 와 복사 대입 연산자를
작성하는 것을 생각해냈다. 하지만, 이 방법만 사용할 경우 100점 만점 중 90점 밖에
획득하지 못한다. 10점은 어디갔나? private
영역이라 하더라도 멤버함수 혹은
friend 함수로 접근이 가능해진다. 이것 까지 막으려면 정의 (define)을 하지
않아야한다. 즉, 선언만 하도록 해야한다.
해당 방법을 사용할 경우 실수로 멤버 함수 혹은 friend에서 복사 개념에 접근할 경우 링크 시점에 링크에러가 발생하기 때문에 해당 문제를 해결할 수 있다.
Tip 링크 시점에 에러가 발생한다니? 이 무슨소리인가 생각할 수 있다. 보통 “선언”만 해두면 컴파일러는 정의는 어디있을거야하고, 해당 파일을 탐색한다. 만약 해당파일에 정의가 없으면 다른 파일에 있겠지 하고 안심하고 컴파일 오류를 내지 않는다. 이럴 경우 “링커”가 모든 번역단위들을 링킹하는 과정에서 “엇.. 정의가 없다..!! 😱” 하고 링크 에러를 출력해주는 것을 “링크 시점 에러”라고 한다.
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
위의 코드와 같이, 구성할 경우 만약 사용자가 복사를 하려고하면 컴파일 에러를 출력하려고 할 것이고, 멤버함수 혹은 friend에서 의도치 않게 접근할 경우 링크 에러를 출력할 것이다.
한가지 더 생각해보자면, 링크 에러를 컴파일 에러로 당겨서 처리할 수도 있다. 굳이 이렇게 당겨야돼? 라고 생각할수도 있지만, 에러는 가능한 빨리 만나는게 프로그래머한테 이득이다.
링크에러 시점을 컴파일 에러 시점으로 당기는 법
복사 생성자와 복사 대입 연산자를 private
로 선언하되, 해당 테크닉을
HomeForSale
클래스에 적용하는 것이아니라 외부의 클래스로 할당하자. 이후
HomeForSale
클래스에 파생해서 사용하도록 하자. (상속 개념 이용)
class Uncopyable {
protected: // 생성과 소멸을 허용
Uncopyable();
~Uncopyable();
private: // 복사는 방지
Uncopyable (const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable {
...
// 복사 생성자도, 복사 대입연산도 허용 안됨.
};
protected 생성자 개념은 여기 포스팅에서 다뤘다.
C++에서는 컴파일러가 생성한 복사 함수 혹은 생성자 들은 기반 클래스에 있는 생성자 혹은 복사 함수들을 호출하도록 되어져있다. 하지만, 기반클래스의 private으로 복사 생성자들이 생성되어져 있기 때문에 컴파일 자체에서 부터 오류를 출력하게 된다.
구체적으로, 멤버함수나, 멤버변수에서 접근하려고 할 경우 “복사 생성자”를 호출하는데, “복사 생성자” 내에서 기반 클래스의 “복사 생성자”를 호출해 컴파일 자체의 오류를 출력하게 만드는 것이다.
즉, “링크 에러시점”을 “컴파일 시점 에러”로 변경한 것이다.
modern C++에서는?
현대에 사용되고 있는 mordern c++에서는 해당 테크닉을 사용하지 않아도 쉽게 해결
할 수 있다. delete
라는 키워드를 사용하는 것이다. delete
의 자세한 내용은
여기를 클릭해서
알아보자.
그렇다면 해당 테크닉을 알 필요가 없는 것 아닌가? 생각할수도 있다. 하지만, legacy 코드를 대부분 유지하고 지금까지 살아남은 프로그램들은 이전부터 만들어졌을 가능성이 많다. 따라서, 해당 기법들이 많이 적용되어있을 것이다. 그때 당황하지 말고 지금 해당 기법을 잘 익혀두기 바란다!
Appendix
컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를
private
에 선언한 후 구현은 하지 않는 채로 두면 된다.