[Effective Modern C ] 항목 17: 특수 멤버 함수들의 자동 작성 조건을 숙지하라 - ChoiChiWon/ccw GitHub Wiki
내용
1. 특수 멤버 함수(special member function)
-
c++이 스스로 기꺼이 작성하는 멤버 함수들을 가리킨다.
-- 기본 생성자, 소멸자, 복사 생성자, 복사 배정 연산자 -
조건
-- 이 함수들은 꼭 필요한 경우에만, 다시 말해 이 함수들이 클래스에 명시적으로 선언되어 있지는 않지만 이 함수들을 사용하는 클라이언트 코드가 존재할 때에만 작성된다.
-- 기본 생성자는 클래스에 생성자가 하나도 선언되어 있지 않을 때 작성된다.
-- 작성된 특수 멤버 함수들은 암묵적으로 공개(public)이자 인라인(inline)이다.
-- 가상 소멸자가 있는 기반의 클래스를 상속하는 파생 클래스의 소멸자를 제외하고는 비가상(non virtual)이다.
-- 가상 소멸자가 있는 기반 클래스를 상속하는 경우 파생 클래스의 소멸자는 가상(virtual)으로 선언된다. -
이동 생성자(move constructor) / 이동 배정 연산자(move assignment operator)
-
조건
-- 이동 연산들은 필요한 때에만 작성되며, 작성되는 경우에는 클래스의 비정적 자료 멤버들에 대해 "멤버별 이동"을 수행한다.
-- 이동 생성자는 주어진 매개변수 rhs의 비정적 자료 멤버 각각을 이용해서 클래스의 해당 자료 멤버들을 각각 이동 생성한다.
-- 이동 배정 연산자는 주어진 매개변수 rhs의 비정적 자료 멤버 각각을 클래스의 해당 자료 멤버들에 각각 이동 배정한다.
-- 이동 생성자는 또한 자기의 기반 클래스 부분을 이동 생성하고(그런 부분이 있으면), 이동 배정 연산자는 자신의 기반 클래스 부분을 이동 배정한다.
class Widget
{
public:
Widget(Widget&& rhs); // 이동 생성자
Widget& operator=(Widget&& rhs); // 이동 배정 연산자
};
2. 규칙
- 멤버별 '이동'의 핵심은 이동할 객체(원본)에 std::move를 적용하는 것이고, 그 적용 결과는 함수 중복적재 해소 과정에서 이동과 복사 중 어떤 것을 수행해야 할지를 결정하는데 쓰인다.
- 멤버별 이동이 이동 연산을 지원하는 자료 멤버들과 기반 클래스들에 대한 이동 연산들로 이루어지며, 만일 이동 연산을 지원하지 않는다면 복사 연산이 수행된다.
- 복사 연산들과 마찬가지로, 클래스 작성자가 명시적으로 선언한 이동 연산들은 자동으로 작성되지 않는다.
- 두 복사 연산은 서로 독립적이다. 즉, 하나를 선언한다고 해서 다른 하나의 작성이 방지되지는 않는다.
- 복사 생성자를 선언했지만 복사 배정 연산자는 선언하지 않았다면, 그러나 복사 배정 연산이 필요한 클라이언트 코드를 작성했다면, 컴파일러가 자동으로 복사 배정 연산자를 작성한다.
- 복사 배정 연산자를 선언했지만 복사 생성자는 선언하지 않았다면, 그러나 복사 생성이 필요한 클라이언트 코드를 작성했다면, 컴파일러가 자동으로 복사 생성자를 작성한다.
p.119
-
두 이동 연산은 둘 중 하나를 선언하면 컴파일러는 다른 하나를 작성하지 않는다.
-- 예를 들어 독자가 어떤 클래스에 대해 이동 생성자를 선언했다면, 그것은 컴파일러가 작성해주는 기본적인 멤버별 이동 생성이 그 클래스에 그리 적합하지 않아서 다른 방식으로 구현해야 하기 때문일 것이다.
-- 그리고 기본적인 멤버별 이동 생성이 적합하지 않다면, 멤버별 이동 배정 역시 적합하지 않을 가능성이 크다.
-- 그래서 이동 생성자가 선언되어 있으면 컴파일러가 이동 생성자를 작성하지 못하게 한 것이다. -
복사 연산(생성 또는 배정)을 하나라도 명시적으로 선언한 클래스에 대해서는 이동 연산들이 작성되지 않는다.
-- 그 근거는, 복사 연산을 선언했다는 것은 일반적인 객체 복사 방식(멤버별 복사)이 그 클래스에 대해 적합하지 않다는 뜻이고, 만일 복사 연산들에 대해 멤버별 복사가 적합하지 않다면 이동 연산들에 대한 멤버별 이동 역시 적합하지 않을 가능성이 크다는 것이다. -
이동 연산(생성 또는 배정)을 하나라도 명시적으로 선언하면 컴파일러는 복사 연산들을 비활성화한다.
-- 구체적으로는 복사 연산들을 '삭제'해서 비활성화 한다.
-- 멤버별 이동이 적절한 객체 이동 방식이 아니라면, 멤버별 복사가 적절한 객체 복사 방식이라고 기대할 이유는 없다. -
3의 법칙(Rule of Three)
-- 만일 복사 생성자와 복사 배정 연산자, 소멸자 중 하나라도 선언했다면 나머지 둘도(즉, 셋 다) 선언해야 한다는 것이다.
-- 어떤 클래스의 복사 배정 연산의 의미를 프로그래머가 직접 지정해야 할 필요성은 거의 항상 그 클래스가 어떤 형태로든 자원 관리를 수행하기 때문에 생긴다.
-- 그런 클래스에서는 거의 항상, 한 복사 연산이 수행하는 자원 관리를 다른 복사 연산에서도 수행해야하며, 클래스의 소멸자 역시 그 자원의 관리에 참여한다(보통의 경우 자원을 해제). -
클래스에 사용자가 선언한 소멸자가 있다는 것은 그 클래스의 복사 연산들에 단순한 멤버별 복사가 적합하지 않음을 뜻할 가능성이 크다.
-
만일 클래스에 소멸자가 선언되어 있으면, 복사 연산들을 자동으로 작성하지 않는 것이 바람직하다는 결론으로 이어진다.
-
복사 연산을 하나라도 선언하면 이동 연산들의 암묵적 작성이 배제된다는 점과 그 추론의 결합에 의해, 결과적으로 c++11은 사용자 선언 소멸자가 있는 클래스에 대해서는 이동 연산들을 작성하지 않는다.
-
클래스에 대한 이동 연산들은 다음 세 조건이 모두 만족될 때에만, 그리고 필요할 때에만, 자동으로 작성된다.
-- 클래스에 그 어떤 복사 연산도 선언되어 있지 않다.
-- 클래스에 그 어떤 이동 연산도 선언되어 있지 않다.
-- 클래스에 소멸자가 선언되어 있지 않다.
// 기본 행동 사용("=default") 예
class Widget
{
public:
~Widget(); // 시용자 선언 소멸자
// 기본 복사 생성자
// 기본 행동 ok
Widget(const Widget&) = default;
// 기본 복사 배정
// 기본 행동 ok
Widget& operator=(const Widget&) = default;
};
3. "=default"
- 가상으로 만드는 것 외에 변경할 것이 없는, 즉 기본 구현이 적합한 경우
- 소멸자를 직접 선언하면 이동 연산들의 자동 작성이 금지된다.
- 만일 그러한 사용자 선언 소멸자를 두면서도 이동 능력을 지원하고 싶다면, 이동 연산들에 "=default"를 지정하면 된다.
- 이동 연산들을 직접 선언하면 복사 연산들이 비활성화되며, 만일 이동과 함께 복사도 지원하고 싶다면 역시 마찬가지로 복사 연산들에 "=default"를 지정하면 된다.
class Base
{
public:
virtual ~Base() = default; // 소멸자를 가상으로
Base(Base&&) = default; // 이동 지원
Base& operator=(Base&&) = default;
Base(Const Base&) = default; // 복사 지원
Base& operator=(const Base&) = default;
4. 명시적으로 "=default"를 작성하는 예
- 독자의 의도가 더 명확해질 뿐만 아니라 상당히 미묘한 버그들을 피하는 데에도 도움이 된다.
// 이 클래스가 복사 연산들과 이동 연산들, 그리고 소멸자를 전혀 선언하지 않는다면,
// 그리고 그런 함수들을 사용하는 클라이언트 코드가 있다면, 컴파일러는 해당 함수들을 자동으로 작성한다.
class StringTable
{
public:
StringTable() {}
// 삽입, 삭제, 조회 등을 위한 함수들은 있지만
// 복사/이동/소멸자 기능성은 없음
private:
std::map<int, std::string> values;
}
// 이런 객체들의 기본 생성과 소멸 추가한다고 하자.
class StringTable
{
public:
StringTable() // 추가됨
{
makeLogEntry("Creating StringTable object");
}
~StringTable() // 추가됨
{
makeLogEntry("Destroying StringTable object");
}
// 다른 함수들은 이전과 동일
private:
std::map<int, std::string> values;
}
- 이동 연산들이 자동으로 작성되지 않는다.
- 그러나 클래스의 복사 연산 작성에는 아무런 영향도 미치지 않는다.
- 이동 능력에 관한 검사도 통과할 것이다. 비록 이제는 클래스의 이동이 비활성화되었지만, 이동 요청들은 아무 문제 없이 컴파일 및 실행되기 때문이다.
- StringTable 객체를 '이동'하는 코드는 실제로는 바탕 std::map<int, std::string> 객체의 복사본을 생성한다.
- 그리고 std::map<int, std::string>의 복사에 걸리는 시간은 이동에 걸리는 시간에 여러 자릿수의 배수를 곱한 것(이를테면 수백, 수처 배)일 수 있다.
- 복사 연산들과 이동 연산들을 "=default"를 이용해서 명시적으로 정의했더라면 이런 문제가 없었을 것이다.
5. 특수 멤버 함수들을 관장하는 c++11의 규칙 정리
-
기본생성자
-- c++98의 규칙들과 같다. 클래스에 사용자 선언 생성자가 없는 경우에만 자동으로 작성된다. -
소멸자
-- c++98의 규칙들과 본질적으로 같다.
-- 유일한 차이는 소멸자가 기본적으로 noexcept라는 점이다.
-- c++98에서처럼, 기본적으로 작성되는 소멸자는 오직 기반 클래스 소멸자가 가상일 때에만 가상이다. -
복사 생성자
-- 실행시점 행동은 c++98의 것과 같다.
-- 즉, 비정적 자료 멤버들을 멤버별로 복사 생성한다. 클래스에 사용자 선언 복사 생성자가 없을 때에만 자동으로 작성된다.
-- 클래스에 이동 연산이 하나라도 선언되어 있으면 삭제(비활성화)된다.
-- 사용자 선언 복사 배정 연산자나 소멸자가 있는 클래스에서 이 함수가 자동 작성되는 기능은 비권장이다. -
복사 배정 연산자
-- 실행시점 행동은 c++98의 것과 같다. 즉, 비정적 자료 멤버들을 멤버별로 복사 배정한다.
-- 클래스에 사용자 선언 복사 배정 연산자가 없을 때에만 자동으로 작성된다.
-- 클래스에 이동 연산이 하나라도 선언되어 있으면 삭제(비활성화)된다.
-- 사용자 선언 복사 생성자나 소멸자가 있는 클래스에서 이 함수가 자동 작성되는 기능은 비권장이다. -
이동 생성자와 이동 배정 연산자
-- 각각 비정적 자료 멤버의 멤버별 이동을 수행한다. 클래스에 사용자 선언 복사 연산들과 이동 연산들, 소멸자가 없을 때에만 자동으로 작성된다.
기억해 둘 사항들
- 컴파일러가 스스로 작성할 수 있는 멤버 함수들, 즉 기본 생성자와 소멸자, 복사 연산들, 이동 연산들을 가리켜 특수 멤버 함수라고 부른다.
- 이동 연산들은 이동 연산들이나 복사 연산들, 소멸자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성된다.
- 복사 생성자는 복사 생성자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 복사 배정 연산자는 복사 배정 연산자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 소멸자가 명시적으로 선언된 클래스에서 복사 연산들이 자동 작성되는 기능은 비권장이다.
- 멤버 함수 템플릿 때문에 특수 멤버 함수의 자동 작성이 금지되는 경우는 전혀 없다.
추가 내용
[C++11] "default" and "delete" keyword
특수 멤버 함수
- 기본 생성자
- 소멸자
- 복사 연산자 (생성자 / 대입 연산자)
- 이동 연산자 (생성자 / 대입 연산자)
특수 멤버 함수 자동 작성 규칙
-
자동 작성되는 특수 멤버 함수들은 암묵적으로 공통적으로 아래의 속성들을 가진다.
-- 공개성 (public)
-- 인라인화 (inline)
-- 가상 소멸자가 아닌 경우 비가상 (파생 클래스의 가상 소멸자는 가상) -
그리고 특수 멤버 함수들은 아래의 조건들이 모두 충족될 때만 자동 작성된다.
-- 함수 종류별로 개별 조건들 충족
-- 명시적으로 선언되어 있지 않는 상태
-- 호출되는 코드가 존재할 경우 -
생성자
-- C++98에서부터 내려온 전통이 유지된다.
-- 기본 생성자가 아닌, 다른 형태의 생성자가 존재할 경우 자동 작성되지 않는다. -
소멸자
-- C++98의 규칙과 거의 유사하나, C++11부터는 소멸자가 기본적으로 noexcept -
복사 생성자 / 복사 대입 연산자
-- 둘 중 하나라도 명시되어 있을 경우, 나머지 하나는 자동 작성되지 않는다.
-- 이동 연산자 (생성자 or 대입 연산자)가 하나라도 명시적으로 작성되어 있을 경우, 삭제(= delete) 된다. -
이동 생성자 / 이동 대입 연산자
-- 소멸자가 명시적으로 작성되어 있을 경우, 자동 작성되지 않는다.
-- 둘 중 하나라도 명시되어 있을 경우, 나머지 하나는 자동 작성되지 않는다.
-- 복사 연산자 (생성자 or 대입 연산자)가 하나라도 명시적으로 작성되어 있을 경우, 자동 작성되지 않는다.
C++ rules for special member functions
Special member functions - wiki
[Exceptional C++ Style] 19. 파생된 클래스들에 대한 규칙 강제
Special members - cplusplus
Non-static member functions
Defaulted and Deleted Functions