특수 멤버 함수( special member functions )
컴파일러가 자동으로 작성하는 특수 멤버 함수는 모두 6개인데, 구체적으로는 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자입니다.
이 글에서는, 이 멤버 함수들이 생성되는 데는 필요한 적절한 조건과, 그 조건이 생기게 된 이유를 정리합니다.
먼저, 이 특수 멤버 함수들이 자동으로 작성되는 가장 기본적인 조건은, 명시적인 멤버 함수가 있지 않아야 되다는 것입니다.
예를 들어, 선언된 소멸자를 가진 클래스가 있다면, 컴파일러는 그 클래스의 소멸자를 자동으로 작성하지 않습니다.
그리고, 이러한 특수 멤버 함수들은 실행에 필요한 경우에만 작성됩니다.
class Widget;
int main(){
Widget w1; // 기본 생성자 작성
// do something...
Widget w2( w1 ); // 이 때, 복사 생성자 작성
// ...
}
w1이 선언될 때, 객체가 만들어지고, 만약 다른 생성자가 없다면, 이때 기본 생성자가 작성됩니다.
마찬가지로, w2의 복사 생성자가 필요할 때( 객체가 복사 생성자를 통해 초기화될 때 ) 작성됩니다.
또한, 명시적으로 컴파일러가 생성한 멤버 함수를 사용하겠다고 선언한 경우에도 자동으로 특수 멤버 함수들을 작성합니다.
이러한 내용을 명시적으로 선언하기 위해 "= default " 구문을 사용합니다.
class Widget{
public:
// 컴파일러가 생성하는 복사 생성자를 사용하겠다는 선언
Widget( const Widget& w) = default;
};
기본 생성자( default constructor )
기본 생성자는 다른 생성자가 하나도 존재하지 않는 경우에만 자동으로 작성됩니다.
따라서, 다음과 같은 경우 컴파일러가 오류를 발생합니다.
class Widget{
public:
Widget( int val ){
cout << "Parameter constructor called\n";
};
};
int main(){
Widget w1; // 기본 생성자 필요. error !
}
위에서 w1을 초기화하려면 기본 생성자가 필요합니다.
그런데, 이미 매개변수를 받는 다른 생성자가 존재하므로, 기본 생성자는 자동으로 생성되지 않습니다.
따라서, 컴파일러는 w1를 초기화할 수 없다는 오류를 발생합니다.
소멸자( destructor )
소멸자는 상속과 가상함수를 사용하여 다형성( polymorphism ) 기능을 구현한 경우, 반드시라고 해도 무방할 정도로 가상함수로 선언하는 것이 중요합니다.
그리고, 기반( base ) 클래스의 소멸자를 가상함수로 선언하면, 이 클래스를 상속받은 클래스들의 소멸자는 자동으로 가상함수가 됩니다.
가상함수에 관한 내용은 여기에서 볼 수 있습니다.
아래의 예문은 가상함수로만 선언하고 다른 내용이 필요 없는 경우, 자동으로 생성된 소멸자를 사용하겠다고 선언하는 방법을 보여줍니다.
class Base{
public:
virtual ~Base() = default;
};
class Derived : public Base {
// 이 클래스의 소멸자는 자동으로 가상함수입니다.
};
복사 생성자( copy constructor )와 복사 대입 연산자( copy assignment operator )
컴파일러가 자동으로 생성하는 복사 생성자와 복사 대입 연산자( 합쳐서 부를 때는 복사 연산이라고 하겠습니다. )가 하는 일은 멤버 별( member-wise ) 복사 연산을 수행하는 것입니다.
예를 들어, 다음과 같은 클래스가 있을 때, 맨 마지막 문장에서 호출되는 이 클래스의 복사 생성자는 w1 객체의 각 멤버 변수를 w2 객체의 멤버 변수로 복사하는 일을 수행합니다.
w1 객체의 d1은 w2 객체의 d2에 복사하고, d2는 w2 객체의 d2에 복사하고...
class Widget1{
std::vector<int> d1;
std::string d2;
int d3;
int* d4;
public:
Widget1(){
d4 = new int[10];
}
~Widget1(){
delete [] d4;
}
};
Widget1 w1;
Widget1 w2( w1 ); // w1의 멤버들을 복사
이때 주목할 것은, 포인터인 d4가 가리키는 객체의 주소도 그대로 복사된다는 것입니다.
다른 말로 하면, 자동으로 생성되는 복사 연산에서는 얕은 복사를 수행한다는 것입니다.
따라서, 위의 코드는 프로그램이 종료되는 예외를 발생시킵니다.
이러한 내용에 대한 글은 여기에서 볼 수 있습니다.
그리고, 이동 연산( 이동 생성자와 이동 대입 연산자를 합해서 )이 하나라도 있다면, 복사 연산 함수는 자동으로 작성되지 않습니다.
이것은, 만약 명시적으로 이동 연산을 구현했다면, 이 클래스의 복사 연산도 기본적인 복사 연산의 기능( 멤버 별 복사 )과는 다른 복사 기능이 필요한 것이라는 추론에 의한 결론입니다.
마찬가지 이유로, 이동 생성자와 이동 대입 연산자는 서로 독립적이지 않습니다.
만약, 이동 생성자가 존재한다면, 이동 대입 연산자는 자동으로 작성되지 않고, 반대의 경우도 성립합니다.
그런데, 복사 생성자와 복사 대입 연산자는 서로 독립적입니다.
이것은, 복사 생성자와 복사 대입 연산자가 서로 다른 행동을 할 것이라는 추론의 결과가 아니라, 이미 기존에 독립적이라는 가정하에 작성되었던 코드들의 안전을 위한 것입니다.
따라서, C++에서는 복사 연산 중 하나라도 존재하는 경우, 다른 복사 연산이 자동으로 작성되도록 놔두는 것을 비권장하고 있습니다.
만일, 특별한 이유로 복사 생성자를 구현했지만, 복사 대입 연산자는 자동으로 작성된 함수를 사용하고 싶다면, 명시적으로 이러한 의도를 다음과 같이 표현할 수 있습니다.
class Widget{
public:
Widget( const Widget& w){
cout << "Copy constructor called\n";
}
Widget& operator= ( const Widget& w) = default; // 명시적으로 표시
};
이동 생성자( move constructor )와 이동 대입 연산자( move assignment operator )
컴파일러가 자동으로 생성하는 이동 연산이 하는 일은 복사 연산과 비슷하게, 멤버 별( merber-wise ) 이동 연산을 수행하는 것입니다.
그리고, 이러한 이동 연산은 이전 항목에서 말한 것과 같이, 서로 비독립적입니다.
만약, 이동 연산 중 어느 하나가 존재하면, 다른 연산은 자동으로 생성되지 않는다는 뜻입니다.
또한, 이 이동 연산들은 "3의 법칙( Rule of Three )"의 논리에 따라, 복사 연산( 복사 생성자와 복사 대입 연산자 )이나 소멸자가 있는 경우에 자동으로 생성되지 않습니다.
여기서 3의 법칙이란,
만일 복사 생성자와 복사 대입 연산자, 그리고 소멸자 중 하나라도 선언했다면 나머지 둘도 선언해야 한다
는 것입니다.
이러한 법칙의 논리는, 만약 위의 세 가지 함수 중 어느 하나라도 직접 구현했다면, 이는 이 함수들을 담고 있는 클래스가 어떤 식으로든 특별한 자원 관리를 수행한다는 것을 예상할 수 있다는 것입니다.
그리고, 이러한 자원 관리가 문제없이 돌아가려면, 대부분은 복사 연산에서는 깊은 복사와 같은 특별한 기능을 구현해야 할 것이고, 소멸자는 사용한 자원을 해제할 의무가 있다는 생각을 바탕으로 하고 있습니다.
이러한 논리는 이동 연산에도 당연히 적용할 수 있으므로, 만약 복사 연산이나 소멸자가 있는 경우 이동 연산을 자동으로 생성하지 않겠다는 주장은 타당합니다.
이동 연산에 관한 글은 여기에서 볼 수 있습니다.
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 좌측값 참조( l-value reference )의 성질 (0) | 2024.09.29 |
---|---|
[C++] 이름이나 표현식의 타입을 알려주는 decltype (2) | 2024.09.13 |
[C++] auto의 형식 추론( type deduction ) 규칙 (0) | 2024.09.13 |
[C++] 완벽한 전달( perfect forwarding )에 대한 설명 (1) | 2024.09.08 |
[C++] 템플릿의 형식 추론( template type deduction ) 규칙 (0) | 2024.09.07 |