본문 바로가기

3.구현/C or C++

[c++] 특수 멤버함수 사용하기: 복사 생성자/배정 연산자, 이동 생성자/배정 연산자, =default, =delete

들어가기

컴파일러가 정의되어 있지 않다면 자동 생성하는 특수 멤버함수(special member function)가 있다. 기본 생성자, 소멸자, 복사 생성자, 복사 배정 연산자, 이동 생성자, 이동 배정 연산자 들이다. 이동 생성자(move constructor)와 이동 배정 연산자(move assignment operator)는 C++11에서 추가되었다. 이들 특수 멤버 함수 자동 생성과 구현시 서로 간에 어떤 영향이 있는지 살펴볼려고 한다.

작성자: ospace114@empal.com, http://ospace.tistory.com/

Box 클래스

먼저 간단한 Box 클래스를 만들어보자. 단순 값 한개를 관리하게 최대한 단순하게 정의했다.

class Box {
public:
    int getValue() { return value; }
    void setValue(int value) { this->value = value; }
private:
    int value = -1;
};

특수 멤버함수는 별도로 정의되어 있지 않았다. 이럴경우 내부적으로 특수 멤버함수 필요시 public, 비가상(non-virtual)으로 생성된다. 자동으로 생성되면서 실행에는 문제가 없다.
다음은 특수 멤버 함수의 기본 시그니처이다.

  • 기본 생성자: Box()
  • 복사 생성자: Box(const Box& other)
  • 이동 생성자: Box(Box&& other) noexcept
  • 복사 배정 연산자: Box& operator=(const Box& other)
  • 이동 배정 연산자: Box& operator=(Box&& other) noexcept
  • 소멸자:virtual ~Box()
  • 주소 관련 연산자
    • Box * operator&(void)
    • Box const * operator&(void) const
    • Box volatile * operator&(void) volatile
    • Box const volatile * operator&(void) const volatile

여기서 살펴볼 특수 멤버 함수는 주소 괄련 연산자를 제외한 앞에 6개 멤버 함수이다.

복사 생성자와 복사 배정 연산자

복사 생성자와 복사 배정 연산자는 기존 객체를 복사해서 새로운 객체를 만들거나 할당한다. 복사 연산에 대한 자동 생성은 복사 생성자 또는 복사 배정 연산자가 없으면 각각 생성된다. 만약 어느하나라도 정의되었다면 기본 생성자는 필수로 정의가 필요하다.

유념할 부분으로 3의 법칙(Rule of Three)이 있다. 복사 생성자, 복사 배정 연산자, 소멸자 중에 하나라도 정의했다면 나머지 두개도 정의하는게 좋다. 이 말은 직접 자원관리를 했기에 나머지도 같이 관리가 필요하다는 의미이다. 물론 소멸자는 정의하지 않아도 자동 생성되어 에러가 발생하지 않지만 같이 관리하는게 좋다.

  • 복사 생성자를 정의하면 기본 생성자도 정의해야 한다.
  • 복사 배정 연산자는 복사 생성자를 재사용하도록 정의한다.
  • 3의 법칙으로 복사 생성자, 복사 배정 연산자, 소멸자를 같이 관리한다.

복사 생성자와 복사 배정 연산자를 다음 처럼 정의할 수 있다. 복사 생성자를 정의할 때에 기본 생성자도 정의해야 한다.

class Box {
public:
    Box() {}
    Box(const Box& other) : value(other.value) {} // 복사 생성자
    Box& operator=(const Box& other) {            // 복사 배정 연산자
      value = other.value;
      return *this;        
  }
    //...
};

위의 코드에서 복사 배정 연산자가 복사 생성자를 사용해서 처리할 수 있다. 이때에는 swap()을 사용하여 좀 더 안전하게 처리할 수 있다.

class Box {
public:
    Box() {}
    Box(const Box& other) : value(other.value) {}
    Box& operator=(const Box& other) {
      Box(other).swap(*this);
      return *this;        
  }
    Box& swap(Box& other) {
        std::swap(value, other.value);
        return *this;
    }
    //...
};

앞의 예제는 복제할 값이 단순하지만, 복잡해질 수록 복사 생성자와 복사 배정 연산자를 별도로 처리는게 어렵기 때문에 복사 배정 연산자에서 복사 생성자를 사용하는게 유지보수 측면에서도 좋다.

이동 생성자와 이동 배정 연산자

이동 생성자는 객체의 소유권 이전과 데이터 이동을 처리하는 생성자이다. 이동 생성자에 의해 이전 객체는 소유권을 잃어버리거나 데이터가 이동되서 리셋된다.

만약 복사 생성자는 정의했지만 이동 생성자가 없는 경우에 이동 생성자 호출할 경우 복사 생성자가 호출된다. 즉, 이동 대신에 복사 작업이 실행된다. 이때에는 이동 연산이 자동 생성되지 않고 복사 연산으로 대처한다. 반대으로 복사 연산을 정의하지 않고 이동 연산를 정의했다면 자동으로 복사 연산이 생성되지 않는다. 이는 이동 연산이 정의되었다면 구현방식이 반경되었기 때문에 복사 연산도 정의가 필요하게 된다. 그래서 앞의 3의 법칙에 이동 연산 2개가 추가된 5의 법칙(Rule of File)이라 한다. 5가지 특수 멤버 함수가 같이 움직인다라고 볼 수 있다.

  • 복사 연산이 있으면 이동 연산이 삭제되고 복사연산이 대신해서 처리한다.
  • 이동 연산을 정의하면 복사 생성자와 기본 생성자가 삭제되기에 같이 정의해야 한다.
  • 5의 법칙으로 이동 연산과 복사 연산, 그리고 소멸자를 같이 관리한다.

이동 생성자 정의해 보자. 이동 생성자와 이동 배정 연산자에서 인자는 {형식}&& 형태로 선언된다.

class Box {
public:
    Box() {}
    // 복사 생성자, 복사 배정 연산자 정의
    Box(Box&& other) noexcept : value(other.value) { // 이동 생성자
        // 소유권 이전, 데이터 이동
        other.value = -1; // 이전 데이터 리셋
    }
    Box& operator = (Box&& other) noexcept { // 이동 배정 연산자
         value = other.value;
         other.value = -1;

         return *this;
     }
    int getValue() { return value; }
    void setValue(int value) { this->value = value; }
private:
    int value = -1;
};

이를 사용한 예제이다.

Box box1;
box1.setValue(10);
Box box2(std::move(box1));
Box box3 = std::move(box1);

이동 배정 연산자도 앞에 복사 배정 연산자처럼 이동 생성자를 사용해서 구현할 수 있다.

= default

매번 모든 특수 멤버 함수를 구현하기 쉽지 않다. 몇몇 멤버 함수는 기존에 기본 생성 방식으로 활용해도 문제 없을 수 있다.

앞에 예제에서 만약 이동 생성자 정의로 인해 복사 생성자와 복사 배정 연산자를 구현해야하는데 기존 기본 생성방식을 그대로 사용하고 싶다면 “= default” 을 지정하면 된다.

class Box {
public:
    Box() {}
    Box(const Box&) = default;
    Box& operator = (const Box&) = default;
    Box(Box&& other) noexcept : value(other.value) { // 이동 생성자
        // 소유권 이전, 데이터 이동
        other.value = -1; // 이전 데이터 리셋
    }
    Box& operator = (Box&& other) noexcept { // 이동 배정 연산자
         value = other.value;
         other.value = -1;

         return *this;
     }
    int getValue() { return value; }
    void setValue(int value) { this->value = value; }
private:
    int value = -1;
};

“= default”은 복사 연산뿐만 아나라 이동 연산에도 적용할 수 있다.

= delete

이번에는 자동 생성되는 특수 멤버함수를 강제 삭제하는 기능이다. C++98에서는 사용하지 않는 특수 멤버함수를 강제로 사용하지 못하게 하려면 private으로 빈 특수 멤버함수를 선언했다.

class Box {
private:
    Box(const Box&);
    Box& operator = (const Box&);
};

이 경우 런타임때 에러가 발생하게 된다. C++11에서는 “=delete”을 지원하여 해당 특수 멤버함수에 붙이게 된다. 이런 멤버 함수를 삭제된 함수(deleted function)이라고 한다. 해당 함수는 public으로 해야 한다. 이렇게 삭제된 함수를 사용할 경우 컴파일 타임에 에러가 발생한다.

class Box {
public:
    Box(const Box&) = delete;
    Box& operator = (const Box&) = delete;
};
void Box::process<void>(void*) = delete; // 외부에서도 가능

외부에서도 멤버함수에 “=delete”을 붙일 수 있다. 이는 일반 함수나 템플릿 함수에도 적용할 수 있다.

bool checkNumber(int val);
bool checkNumber(bool) = delete;

template<> void process<void>(void*) = delete;
template<> void process<const void>(const void*) = delete;

마무리

C++에서 사용하는 특수 멤버 함수에 대해서 살펴보았다. C++11에서 이동 생성자와 이동 배정 연산자가 추가되었다. 이는 오른값 참조에 대한 부분과 연결되어 있어서 오른값 참조를 이해하고 있다면 크기 어렵지 않다. 단지 복사 생성자는 const가 있지만 이동 생성자는 const가 없다. 이는 이동 생성자에서 other에 해당하는 객체에 내에 속성이 이동하면서 변경되기 때문에 const가 없다. 위의 예제는 단순하기 하기 위해 생략했지만, const나 noexcept을 적절히 사용하므로서 더 좋은 구조가 된다. 부족한 글이지만 여러분에게 도움이 되었으면 합니다. 오늘도 좋은 하루되세요. ospace.

참고

[1] 스콧 마이어스 저, 류광 역, Effective Modern C++, 인사이트
[2] Special member functions, https://en.wikipedia.org/wiki/Special_member_functions

반응형