방프리

16.1.28. Effective C++ 1. C++에 왔으면 C++의 법을 따릅시다. (항목5, 6) 본문

C++/Effective C++

16.1.28. Effective C++ 1. C++에 왔으면 C++의 법을 따릅시다. (항목5, 6)

방프리 2020. 1. 4. 02:12

항목 5 : C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자!

 

클래스를 선언할 시 Default로 생성되는 함수 네 가지가 있다. 생성자, 소멸자, 복사 생성자, 복사 대입 연산자이다. 이 네 함수는 클래스가 생성되면 무조건 생성되는 함수들이다. 즉, 사용자가 함수를 만들지 않아도 기본적으로 생성된다는 뜻이다. 정말 좋지 않은가? 자동으로 알아서 만들어 주니 말이다. 

하지만 앞날을 생각해본다면 절대로 좋은 현상이 아닐 뿐더러 오류의 발생의 여지가 충분히 있다. 

 

예제를 한번 들여다보자 

 

template <typename T>

class cNameObject

{

     public : 

       cNameObject(const char *name, const T& value);

       cNameObject(const std::string& name, const T& value);

       ~cNameObject();

       .....

 

     private :

       std::string nameValue;

       T objectValue;

}

 

이 코드를 잘 들여다 보면 위에서 언급한 함수가 빠져 있음을 알 수 있을 것이다. 그렇다 복사 생성자와 복사 대입 연산자가 빠져있다. 이 두 개의 함수는 지금 현재 클래스에서 빠져 있으므로 컴파일러가 자체적으로 기본형을 만들어주게 된다.

 

위의 코드에서는 문제가 될 사항이 그리 많아보이진 않지만 현재의 방식은 꽤나 위험해보인다. 일반적으로 최종 결과 코드가 '적법해야' 하고 '이치에 닿아야만' 라는 조건이 만족해야만 컴파일러가 operator=의 자동 생성 거부를 하지 않기 때문이다. (즉, 컴파일러가 지정해준 기본 함수는 도통 믿을 것이 못된다는 뜻이다.)

 

위의 코드의 내용을 살짝 바꾸어보겠다.

 

template <class T>

class cNameObject

{a

     public : 

      // 이 생성자는 이제 상수 타입의 name을 취하지 않는다. nameValue가 

      // 비상수 string의 참조자가 되었기 때문이다. 참조할 string을 가져야 하기 

      // 때문에 char*는 없애 버렸다.

       cNameObject(const std::string& name, const T& value);

       ~cNameObject();

       .....

 

     private :

       std::string nameValue;

       T objectValue;

}

 

//////////////////////////////////////////

int main(void)

{

     std::string newDog("Persephone");

     std::string oldDog("Satch");

 

     cNameObject<int> p(newDog, 2);

     cNameObject<int> s(oldDog, 36);

 

     p = s;

}

 

이렇게 될 경우 p의 nameValue가 가리키고 있는 string 객체를 s의 nameValue가 가리키고 있는 string 객체로 참조자를 바꾸라는 소리가 된다. 즉, 참조자 자체가 바뀌어야 한다는 말이 된다.

흠.... 어딘가 많이 이상하지 않은가? C++의 참조자는 원래 자신이 참조하고 있는 것과 다른 객체를 참조할 수 없으니 말이다. 그렇다면 p.nameValue의 string 객체 자체가 바뀌어야 하는가? 그럴 경우 연관되어 있는 모든 변수나 실제로 연산에 관여하지 않는 객체들까지 영향을 받아 굉장히 꼬이게 된다.

결국 컴파일러는 오류를 도출해낼 수 밖에 없고 우리는 이 에러를 찾기 위해 밤을 샐지도 모른다. 

 

이것만은 잊지 말자!

* 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있다. (무조건 만든다고 생각해라)

 

항목 : 6 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 

         금해 버리자!

 

앞의 게시물에서 우리는 클래스가 생성될 때에 컴파일러가 자동으로 생성해주는 함수 4가지를 살펴보았고 그것에 대한 문제점에 대해서도 예제를 통해 알아보았다. 문제점이 있으면 해결하는 방법도 있는 법! 예제를 통해 살펴보자

 

어느 한 부동산 중개업자가 있다. 이 중개 업자는 프로그램을 통해 부동산 중개업 지원용 소프트웨어를 통해 자신의 사업을 넓히려고 개발자에게 주문을 하였다. 이때 중개업자는 한가지 조건을 걸었는데 

모든 자산은 세상에 단 한가지 밖에 없다는 것을 고려해 개발을 진행해달라는 것이었다. 

 

즉, 객체의 사본을 만들 필요가 없다는 뜻이다. 그런데 문제가 발생하였다. 클래스를 생성하면 자동으로 복사생성자와 복사대입연산자를 호출해버리니 어쩔 도리가 없는 것이다. 그래서 생각해낸 방법으로

 

(1) 주석을 달아놓는다. 

 

void main()

{

   HomeForSale h1;   

   HomeForSale h2;

 

   HomeForSale h3(h1);   //h1을 복사하려 합니다 - 컴파일되면 안돼요!!

 

   h1 = h2;   //h2를 복사하려 합니다 - 컴파일하지 말아 주세요!!

}

황당한 방법이다. 설사 다른 사람들이 보고 이렇게 만들지 않는다고 하더라도 분명 어딘가에서 이런 연산이나 행위가 일어날 것이다. 

 

(2) 복사 생성자와 복사 대입 연산자를 private 멤버로 선언한다.

 

문제 해결의 방법이다. 자동으로 생성될 함수를 미리 private로 선언하여 접근하지 못하게 한다면 절대 허락없이 복사나 대입을 하면 에러를 발생시킬 것이다.  

 

(3) 위의 방법을 그대로 진행하되 내용을 정의하지 않는다.

 

위의 방법은 좋긴 하지만 확실한 정답은 아니다. 왜냐하면 friend라는 키워드가 있기 때문이다. friend 함수를 호출하게 되면 클래스 외부에서 다시 정의가 될 가능성이 있기 때문에 아예 선언만 한 뒤 함수의 내용을 정의하지 않는 것이다. 

 

* 이것만은 잊지 말자!

컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private로 선언한 후에 구현은 하지 않은 채로 두십시오. Uncopyable과 비슷한 기본 클래스를 쓰는 것도 한 방법입니다.

 

Comments