ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 05. 특수 멤버 함수의 자동 생성에 관하여
    C++/Effective C++ 2025. 12. 1. 19:41

    원제목 : Know what functions C++ silently writes and calls (C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자)

     

    특수 멤버 함수란 컴파일러에 의해 자동으로 생성되는 멤버 함수입니다.

    목록으로 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자가 있으며, C++11 이후에는 이동 생성자, 이동 대입 연산자가 추가됩니다.

    이들은 모두 public이며 inline입니다. 또한 상속 관계에 있을 경우, 상위 클래스의 virtual 유무를 따라갑니다.

    특수 멤버 함수들은 C++이 제공하는 기본 기능, 편의 기능임과 동시에 개발자가 신경써서 관리해야 할 영역이기도 합니다.

     

    이번 글에서는 자동으로 생성되는 특수 멤버 함수에 대해 알아보도록 하겠습니다.

     


     

    1. 형태 및 자동 생성 조건

     

    더보기

    다음과 같이 빈 클래스를 작성했을 경우

    class Empty{};

    이 클래스는 컴파일러에 의해 다음과 같이 특수 멤버 함수가 추가될 수 있습니다.

    class EMpty(){
    public:
      // 기본 생성자
      Empty() { ... }
      // 소멸자
      ~Empty() { ... }
      // 복사 생성자
      Empty(const Empty& rhs) { ... }
      // 복사 대입 연산자
      Empty& operator=(const Empty& rhs) { ... }
    };

    (이동에 관련된 특수 멤버 함수들은 다른 문단에 기술하도록 하겠습니다.)

    이러한 특수 멤버 함수들이 작성되는 조건은, 해당 함수가 필요한 상황이 되었을 경우입니다.

    코드로 표현하면 다음과 같습니다.

    // 기본 생성자 및 소멸자
    Empty e1; 
    // 복사 생성자
    Empty e2(e1);
    // 복사 대입 연산자
    e2 = e1;

    자동으로 생성될 특수 멤버 함수가 이미 정의되어 있을 경우에는 컴파일러가 특수 멤버 함수를 생성하지 않습니다.

     

    별도로, 생성자는 인자의 형태를 무시하고 선언 여부를 확인합니다.

    class NotEmpty{
    public:
      NotEmpty(const int num, const string str) : foo(num), bar(str) { }
    private:
      int foo;
      string bar;
    };

    위와 같이 특정한 인자를 받는 생성자가 정의된 클래스라면, 기본 생성자가 자동으로 생성되지 않습니다.

     

    2. 자동 생성이 되지 않는 경우

     

    더보기

    특수 멤버 함수는 필요에 의해 자동으로 생성된다는 것을 알아보았습니다.

    하지만 특수 멤버 함수가 선언되지 않은 모든 경우에 컴파일러가 이를 자동으로 생성하지는 않습니다.

    해당 특수 멤버 함수가 적절하고, 합리적이어야 한다는 조건이 있습니다.

     

    다음과 같은 클래스를 예시로 들어보겠습니다.

    class MyClass{
    public:
      MyClass(string& name) : nameRef(name) { }
    private:
      string& nameRef;
    };

    참조자를 가지는 멤버 변수를 사용하기 위해, 생성자를 통해 참조자를 초기화하도록 만들었습니다.

    이 클래스를 사용하는 적법한, 적법하지 않은 코드들은 다음과 같습니다.

    string str("Hello world!");
    
    // Legal : Constructor
    Foo a(hi);
    // Legal : Copy constructor
    Foo b(a);
    Foo c = b;
    // Illegal : Copy assignment operator
    c = b;

    참조자는 변경될 수 없기 때문에, 이미 생성된 객체의 참조자를 변경하는 복사 대입 연산자는 적법하지 않습니다.

    따라서 컴파일러가 복사 대입 연산자의 자동 생성을 거부하며, 컴파일이 되지 않습니다.

     

    위와 같이 참조자가 있는 객체의 경우, 복사 대입 연산자를 다음과 같이 정의할 수 있습니다.

    class MyClass{
    public:
      MyClass(string& name) : nameRef(name) { }
      MyClass& operator=(const MyClass& rhs){
        nameRef = rhs.nameRef;
        return *this;
      }
    private:
      string& nameRef;
    };

    다음과 같이 복사 대입 연산자가 멤버 내부의 값을 수정하도록 하면 복사 대입이 동작합니다.

     

    위와 같이 컴파일 에러가 발생한다면 특수 멤버 함수를 적절하게 구현하여 해결이 가능합니다.

     

    3. 자동 생성의 위험성

     

    더보기

    이전 문단과 같이, 컴파일 에러가 발생하는 경우는 특수 멤버 함수를 적절하게 구현하여 해결이 가능합니다.

    하지만 런타임에 위험한 상황이 발생할 수 있지만 컴파일 되는 경우가 있을 수 있습니다.

    class BadArray{
    public:
      BadArray(size_t n) : data(new int[n]), size(n) {}
      ~BadArray() { delete[] data; }
    private:
      int* data;
      size_t size;
    };

    위와 같이 배열을 래핑한 Array 클래스를 가정해보겠습니다.

    이 클래스를 다음과 같이 사용한다면

    // data[100]
    BadArray foo(100);
    // new data[100] ??
    BadArray bar = foo;

    bar는 foo에서 생성된 foo::data[100]과 같은 주소를 참조합니다.

    얕은 복사가 발생하여, 이후 foo와 bar이 소멸될 때 int[100]이 두 번 해제되는 미정의 동작이 발생합니다.

     

    위와 같은 문제는 다음과 같이 깊은 복사를 구현하는 것으로 해결이 가능합니다.

    class SafeArray{
    public:
      SafeArray(size_t n) : data(new int[n]), size(n) {}
      ~SafeArray() { delete[] data; }
      // Deep copy!
      SafeArray(const SafeArray& rhs) : data(new int[rhs.size]), size(rhs.size) {
        std::copy(rhs.data, rhs.data + size, data);
      }
    private:
      int* data;
      size_t size;
    };

     

    이 외에도 멤버 변수들에 대한 특정한 관리법이 필요한 경우, 동기화가 필요한 경우 등

    기본 생성되는 특수 멤버 함수가 부적절한 경우를 고려해야 합니다.

     

    4. C++11 : 이동 시맨틱

     

    더보기

    C++11에 r-value reference가 추가되면서, 이동이라는 개념이 생겨났습니다.

    이에 따라 클래스에 새로운 특수 멤버 함수가 추가되었는데, 이동 생성자, 이동 대입 연산자가 그것입니다.

    형태는 다음과 같습니다.

    class MyClass{
    public:
      // Move constructor
      MyClass(MyClass&& rhs) { ... };
      // Move assignment operator
      MyClass& operator=(MyClass&& rhs) { ... }
    };

    컴파일러는 이동 연산이 필요할 경우 이동과 관련된 특수 멤버 함수를 생성하여 사용합니다.

    생성자, 소멸자, 복사 생성자, 복사 이동 연산자는 각각 자신이 선언되지 않을 경우 자동 생성 조건을 만족합니다.

    하지만 이 생성 조건에는 다른 특수 멤버 함수보다 더 강한 조건이 있습니다.

    1. 소멸자가 선언되어 있지 않다.

    2. 복사 생성자, 복사 대입 연산자가 선언되어 있지 않다.

    3. 이동 생성자, 이동 대입 연산자가 선언되어 있지 않다.

    위 3가지 조건을 모두 만족했을 경우에만, 컴파일러는 프로그램에 필요한 이동 생성자 혹은 이동 대입 연산자를 작성합니다.

     


     

    특수 멤버 함수들이 자동 생성되는 경우, 해당 경우에서 발생할 수 있는 특이한 상황들을 알아보았습니다.

    이에 관해, Rule of zero, three, five라는 지침이 있습니다.

    각각의 특수 멤버 함수에 대해, 어느 하나가 작성될 경우에 대응하는 지침입니다.

    Rule of three는 각각 소멸자, 복사 생성자, 복사 대입 연산자를 의미합니다.

    셋 중 어느 하나가 작성되었을 경우, 다른 두 가지도 작성되어야 한다는 지침입니다.

    Rule of five는 three에 이동 생성자, 이동 대입 연산자가 추가된 지침입니다.

    Rule of zero는 five의 다섯 특수 멤버 함수를 모두 작성하지 않을 수 있는 설계를 지향하자는 지침입니다.

     

    이번 글의 내용은 C++의 핵심인 자원의 직접적인 관리와 밀접하게 연결되어 있습니다.

    특수 멤버 함수에 대한 모든 것을 다루지는 않았지만, 기본적인 내용을 숙지하는 데 이용하시면 좋겠습니다.

     

    이번 글이 도움이 되셨기를 바랍니다.

    감사합니다.

    댓글

Designed by Tistory.