2017년 3월 31일 금요일

C++ Special Member Functions

 Special Member Function이라함은 컴파일러가 필요할 때 직접 생성해주는 멤버 함수를 말한다. 즉 아래 함수들이 Special Member Function들이다.
  • default constructor
  • destructor
  • copy constructor
  • copy assignment operator
  • move constructor
  • move assignment operator
 이 함수들은 사용자가 정의하지 않은 경우 컴파일러가 기본 구현을 제공하는 함수들이다. 하지만 항상 기본 구현이 제공되는 것은 아니고 몇 가지 조건이 만족될 때만 기본 구현을 제공한다. 이번 글에서는 그 규칙에 대해서 정리해보고자 한다.

1. 기본 생성자(default constructor)

 클래스에 생성자가 하나도 정의되어 있지 않은 경우에만 기본 생성자를 제공한다. 기본 생성자(default constructor)는 인자가 하나도 없는 생성자를 말한다. 클래스의 생성자가 단 하나도 정의되지 않은 경우에는 컴파일러가 기본 생성자를 제공하는데 제공되는 기본 생성자의 구체적인 구현은 각 멤버의 기본 생성자가 호출되는 것과 같다. 따라서 컴파일러가 생성하는 기본 생성자를 이용하려면 클래스의 모든 멤버들도 역시 기본 생성자를 제공해야 한다는 조건이 하나 더 붙는다.

2. 소멸자(destructor)

 클래스에 소멸자가 정의되어있지 않으면 컴파일러가 자동으로 기본 구현을 제공한다. 상속받은 부모 클래스의 소멸자가 virtual인 경우에는 마찬가지로 virtual로 선언된 소멸자를 제공하고 그렇지 않은 경우에는 non-virtual이다. 그리고 디폴트로 noexcept 소멸자를 제공한다.

3. 복사 생성자(copy constructor)

 클래스에 복사 생성자(copy constructor)가 정의되어 있지 않으면 컴파일러가 기본 구현을 제공한다. 하지만 제공되는 구현이 경우에 따라 다르다.
  1. 이동 생성자 또는 이동 대입 연산자가 선언되어 있으면 복사 생성자는 delete된 선언이 제공된다.
  2.  이동 생성자 또는 이동 대입 연산자가 선언되어 있고 복사 생성자가 선언되지 않았을 때는 컴파일러가 복사 생성자를 delete로 선언한다. 즉, delete된 구현을 제공하는 것이다. 이 상태에서 복사 생성자를 호출하는 코드가 어딘가에 존재한다면 컴파일러는 delete된 함수를 호출한다는 에러를 발생시킬 것이다.
  3. 소멸자 또는 복사 대입 연산자가 선언되어있으면 컴파일러가 복사 생성자를 제공하지만 이 기능은 deprecated되었다.
  4.  뭔가 말이 안되는 것 같지만 설명을 해 보자면, 이런 경우 컴파일러가 각 멤버를 복사 생성하는 구현을 제공하긴 하지만 이렇게 기본 구현을 제공하는 기능은 deprecated되었다는 것이다. 이런 저런 환경에서 개발을 하다보면 오래된 API들이 deprecated된 경우를 볼 수 있을 것이다. 호환성때문에 존재하지만 언젠가는 사라질 수 있는 API들을 그런 상태로 선언해 놓는다. 마찬가지로 이러한 컴파일러의 기능도 C++표준에서는 사용하지 말도록 권고하는 것이며 직접 구현을 제공할 것을 권고하는 것이다.
  5. 그 외에는 복사 생성자가 선언되지 않았다면 컴파일러가 기본 구현을 제공한다.

4. 복사 대입 연산자(copy assignment operator)

 클래스에 복사 대입 연산자(copy assignment operator)가 정의되어 있지 않으면 컴파일러가 기본 구현을 제공한다. 복사 생성자와 복사 대입 연산자가 서로 대칭될 뿐 같은 규칙으로 제공된다.
  1. 이동 생성자 또는 이동 대입 연산자가 선언되어 있으면 복사 대입 연산자는 delete된 선언이 제공된다.
  2.  이 규칙은 복사 생성자와 같다.
  3. 소멸자 또는 복사 생성자가 선언되어 있으면 컴파일러가 복사 대입 연산자를 제공하지만 이 기능은 deprecated되었다.
  4.  복사 생성자 대신 복사 대입 연산자가 되었을 뿐 규칙은 복사 생성자의 경우와 같다. 즉 제공은 되지만 사용을 하지 않는 게 좋다.
  5. 그 외에는 복사 대입 연산자가 선언되지 않았다면 컴파일러가 기본 구현을 제공한다.

5. 이동 생성자(move constructor)

 이동 생성자(move constructor)는 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자가 모두 선언되지 않았을 때만 기본 구현이 제공된다. 기본 구현은 각 멤버별로 이동 생성자가 호출되는 것과 같다.

6. 이동 대입 연산자(move assignment operator)

 이동 대입 연산자(move assignment operator)는 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자가 모두 선언되지 않았을 때만 기본 구현이 제공된다. 즉, 이동 생성자와 같은 규칙이다. 기본 구현은 각 멤버별로 이동 대입 연산자가 호출되는 것과 같다.


 C++98 시대의 C++ 개발자들에게는 3의 규칙이라는 것이 있어서 소멸자, 이동 생성자, 이동 대입 연산자 중에 하나라도 직접 선언했다면 나머지 두 개도 직접 선언해야 한다는 암묵적 규칙이 있었다. 3개 함수중에 하나라도 기본 제공되는 구현이 적당하지 않다면 나머지 2개도 적당하지 않을 확률이 매우 높기 때문에 생긴 규칙이다. 하지만 위 규칙에서 보듯이 암묵적일 뿐 컴파일러 강제사항은 아니다.

 C++11이 되면서 move semantics 개념이 도입되어 이동 연산이 추가되고 3의 규칙은 이동 함수들에도 확장되었다. (5의 규칙?) 하지만 과거의 코드들과 호환성을 유지해야 할 필요성 때문에 이동 함수들만 5개 함수들 중 하나라도 선언되어 있으면 자동 생성되지 않도록 결정되었고 나머지는 거의 그대로 유지가 되고 있다. 복사 함수들이 이동 함수들이 선언된 경우에는 delete로 처리되는 것도 3의 규칙의 확장선 상의 규칙이며 C++11에서 추가되었는데 이것은 이동 함수들이 선언된 경우이므로 C++98 시대의 코드에는 영향을 미치지 않기 때문이다. 3의 규칙과 달리 확장된 규칙들은 컴파일러 강제사항이기 때문에 직접 처리할 수 밖에 없다.

 그렇다면 virtual 소멸자가 필요해서 소멸자를 선언했을 뿐인데 다른 모든 special member function들의 기본 구현을 이용할 수 없게 된다면 어떨까? 이 경우 개발자는 확실히 이것이 불편하다고 느낄 것이다. 이런 경우를 위해서 기본 구현을 간단히 선언할 수 있는 방법이 있다.
class Sample {
public:
  virtual ~Sample() = default; // default destructor

  Sample(const Sample&) = default;            // default copy constructor
  Sample& operator=(const Sample&) = default; // default copy assignment

  Sample(Sample&&) = default;            // default move constructor
  Sample& operator=(Sample&&) = default; // default move assignment

 
  ...
};
함수의 구현을 제공하는 대신 선언부에 '= default'를 붙여주면 컴파일러가 제공하는 기본 구현이 그대로 적용된다. 위의 규칙 중 3번, 4번에서 deprecated된 기능을 사용하지 않고 기본 구현을 사용하는 간단한 방법으로 이렇게 default 선언을 사용하면 사용자가 직접 함수를 선언한 것이 되므로 deprecated된 구현을 사용하는게 아니며 추후에 생길 수 있는 문제를 피할 수 있다. 또한 default 구현을 이용하겠다는 뜻으로 이렇게 명시적으로 선언하는 것은 좋은 습관이다.

0 개의 댓글:

댓글 쓰기