ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Special member function
    C++/Effective Modern C++ 2022. 5. 5. 13:38

    특수 멤버 함수(Special member function)는 C++이 자동으로 작성하는 멤버 함수들을 가리킵니다.

    C++98까지는 기본 생성자, 소멸자, 복사 생성자, 복사 배정 연산자가 이에 해당되었고, C++11에 이동 시맨틱이 추가되면서 이동 생성자, 이동 배정 연산자가 추가되었습니다.

    위 함수들은 클라이언트 코드에서 호출되었을 경우에만 작성되며, 묵시적으로 public이며 inline입니다.

    또한, 자동 생성될 때 상위 클래스가 가상 소멸자인 경우를 제외하면 모두 non-virtual함수입니다.

     

    특수 멤버 함수의 작성 조건은 우선적으로 클라이언트 코드에서의 호출이지만, 작성되지 않는 경우도 존재합니다.

    특정 조건 하에서 호출되어도 작성되지 않는 경우들이 존재하는데, 대표적인 예시로 기본 생성자의 경우는 다른 생성자가 존재할 경우 작성되지 않습니다.

    이번 글에서는 특수 멤버 함수들의 생성 규칙에 관해서 살펴보도록 하겠습니다.

     


     

    1. Signature

     

    더보기

    특수 멤버 함수들의 목록은 다음과 같습니다.

    • 기본 생성자
    • 소멸자
    • 복사 생성자
    • 복사 배정 연산자
    • 이동 생성자
    • 이동 배정 연산자

    위 함수들이 특수 멤버 함수이며, 이에 대한 서명은 다음과 같습니다.

    class MyClass{
    public:
        MyClass() ; // Constructor
        
        MyClass(const MyClass&) ; // Copy constructor
        MyClass& operator=(const MyClass&) ; // Copy assignment operator
        
        MyClass(MyClass&&) ; // Move constructor
        MyClass& operator=(MyClass&&) ; // Move assignment operator
        
        {virtual} ~MyClasS() ; // Destructor
    };

    소멸자의 경우, 기반 클래스의 소멸자가 가상화 되어있을 경우 가상화 됩니다.

     

    2. 생성자

     

    더보기

    생성자의 경우는 매우 간단합니다.

    서론에 서술된 것 처럼 기본 생성자가 특수 멤버 함수이며, 추가적인 생성자가 작성되지 않았을 경우에만 작성됩니다.

    class MyClass {
    public:
        MyClass(int a) { }
    };
    
    int main(){
        MyClass a;
    }
    C2512 : 'MyClass::MyClass': 사용할 수 있는 적절한 기본 생성자가 없습니다.

     

    3. 소멸자

     

    더보기

    소멸자는 오버로딩이 불가능합니다.

    따라서 소멸자는 서명이 하나이며, 객체가 소멸될 때 무조건 호출됩니다.

    이 때 소멸자가 정의되어 있지 않을 경우 컴파일러가 소멸자를 작성하게 되고, 이 때 객체의 상위 클래스의 소멸자가 가상화 되어있을 경우 작성되는 소멸자도 가상화됩니다.

    #include <iostream>
    using namespace std;
    
    class A {
    public:
        /*virtual*/ ~A() { std::cout << "A\n"; }
    };
    
    class B : public A {
    public:
        ~B() { std::cout << "B\n"; }
    };
    
    class C : public B {
    public:
    };
    
    int main(){
        A* cls = new C();
    
        delete cls;
    }
    /*B*/
    A

    위 예제의 A클래스의 소멸자의 가상화 여부에 따라, 자동작성되는 C의 소멸자의 가상화 여부가 결정됩니다.

     

    4. 복사 생성자, 복사 배정 연산자

     

    더보기

    복사 생성자, 복사 배정 연산자 또한 오버로딩이 불가한 함수입니다.

    이들도 사용자가 선언한 복사 생성자, 배정 연산자가 없을 때에 각각 자동으로 작성됩니다.

    추가로, 이동 관련 연산(이동 생성자, 이동 배정 연산자)이 하나라도 정의되어 있을 경우 비 활성화(삭제)됩니다.

    #include <iostream>
    
    class Myclass {
    public:
        MyClass() { std::cout << "Construct\n"; }
        //MyClass(const MyClass& rhs) { std::cout << "Copy construct\n"; }
        MyClass(MyClass&& rhs) noexcept { std::cout << "Move construct\n"; }
    };
    
    int main() {
        MyClass a;
        MyClass b(a);
    }
    C2280 : 'MyClass::MyClass(const MyClass &)': 삭제된 함수를 참조하려고 합니다.

    이에 더해, 각각의 복사 생성자와 복사 배정 연산자는 서로의 자동 작성에 영향을 끼치지는 않지만, 비 권장 기능입니다.

    사용자가 복사 연산을 직접 정의한다는 뜻은, 그 클래스가 어떤 방식으로든 자원 관리를 수행한다는 뜻이 될 수 있습니다. 이것은 기본으로 작성되는 복사 연산이 클래스에 적합하지 않을 확률이 높다는 뜻이 됩니다.

    자원 관리가 수행되는 클래스의 경우 소멸자까지 그 자원 관리를 수행해야 할 확률이 높으며, 이것은 문법 외 규칙인 Rule of Three(복사 생성자, 복사 배정 연산자, 소멸자 중 하나라도 선언했을 경우, 나머지 함수도 선언해야 한다)라는 지침이 되었습니다.

    이동 관련 연산 또한 마찬가지 입니다. 자원 관리를 수행함에 있어 자동 작성되는 함수가 적합하지 않으므로, 작성되지 않는 것 이며, Rule of Three에 이동 생성자, 이동 배정 연산자가 추가된 Rule of Five라는 지침 또한 존재합니다.

     

    5. 이동 생성자, 이동 배정 연산자

     

    더보기

    위 문단에서, 복사 연산은 각각의 연산(생성자, 배정 연산자)이 독립적이고, 추가적으로 이동 연산의 정의 여부가 연관한다는 것을 볼 수 있었습니다.

    이동 연산(이동 생성자, 이동 배정 연산자)는 조금 더 엄격하여, 사용자 호출이 있을 때 다음의 규칙을 만족해야 자동으로 생성됩니다.

    • 클래스에 그 어떤 복사 연산(복사 생성자, 복사 배정 연산자)도 선언되어 있지 않다.
    • 클래스에 그 어떤 이동 연산(이동 생성자, 이동 배정 연산자)도 선언되어 있지 않다.
    • 클래스에 소멸자가 선언되어 있지 않다.

    이동 연산은 "이동 요청"으로 해석할 수 있습니다.

    이동 시맨틱이 없는 C++98과 그 이전의 클래스에 대해 이동 연산을 수행하는 것은 내부적으로는 복사를 통해 이루어집니다.

    이와 같은 방식이 의외의 성능 하락을 보일 수 있는데, 이는 예제를 통해 살펴보도록 하겠습니다.

    class MyClass {
    public:
        MyClass() { std::cout << "Log : Construct\n"; }
        //~MyClass() { std::cout << "Log : Destruct\n"; }
        ...
    };

    위와 같은 객체를 정의했을 때, 이 객체는 컴파일러에 의해 복사 연산, 이동 연산, 소멸자가 자동으로 정의됩니다. 하지만 이후 유지 보수 과정에서 주석처리 된 소멸자를 정의할 경우를 생각해보도록 하겠습니다.

    소멸자를 정의할 경우 이동 연산이 자동 작성되지 않지만, 복사 연산에는 영향을 미치지 않습니다. 이동에 관한 연산이 비 활성화 되었지만, 이들은 실제로 잘 컴파일 되며, 이동 연산은 복사에 의해 만족됩니다.

    여기서 이동 연산이 복사를 통해 수행되므로, 성능에 큰 문제가 발생할 수 있습니다.

     

    위와 같은 이유로, 아래와 같이 기본 연산을 명시적으로 활성화 하는 것이 도움이 될 수 있습니다.

    class MyClass {
    public:
        MyClass() { std::cout << "Log : Construct\n"; }
        ~MyClass() { std::cout << "Log : Destruct\n"; }
        
        MyClass(const MyClass&) = default; // Copy constructor
        MyClass& operator=(const MyClass&) = default; // Copy assignment operator
        
        MyClass(MyClass&&) = default; // Move constructor
        MyClass& operator=(MyClass&&) = default; // Move assignment operator
        ...
    };

     

    6. 템플릿

     

    더보기

    멤버 함수 템플릿은 특수 멤버 함수의 자동 작성과는 무관합니다.

    예제를 살펴보도록 하겠습니다.

    class MyClass {
        ...
        template<typename T>
        MyClass(const T& rhs) { ... }
        
        template<typename T>
        MyClass& operator=(const T& rhs) { ... }
        ...
    };

    위와 같이, 경우에 따라서는 복사 생성자, 복사 배정 연산자와 동일한 서명이 될 수 있는 함수 템플릿이 존재하더라도, 복사 생성자, 복사 배정 연산자에 대해서는 컴파일러가 해당 특수 함수들을 자동으로 작성합니다.

     


     

    클래스의 생성 및 소멸에 관련된 함수는 메모리, 시간과 직접적인 연관을 가지고 있습니다.

    이러한 특수 함수들의 자동 작성 조건을 숙지하지 않는다면, 본문의 내용처럼 의도치 않은 성능 하락이 발생할 수 있습니다.

    이번 글이 특수 멤버 함수의 자동 생성에 관해 익히는 데 도움이 되었길 바랍니다.

     

    감사합니다.

    댓글

Designed by Tistory.