[Modern C++] 항목 16: const 멤버 함수를 스레드에 안전하게 작성하라
카테고리: Cpp
이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역
가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩
📦 3. 현대적 C++에 적응하기
👉🏻 항목 17: 특수 멤버 함수들의 자동 작성 조건을 숙지하라
🔍 특수 멤버 함수란?
- C++이 스스로 기꺼이 작성하는 멤버 함수들을 특수 멤버 함수(special member function)라고 부른다.
- 이 함수들은 꼭 필요한 경우에만, 즉 클래스에 명시적으로 선언되어 있지는 않지만 이 함수들을 사용하는 클라이언트 코드가 존재할 때에만 작성된다.
- 작성된 특수 멤버 함수들은 암묵적으로
public이자inline이며, 가상 소멸자가 있는 부모 클래스를 상속하는 자식 클래스의 소멸자를 제외하고는 비가상(nonvirtual)이다.
C++98의 특수 멤버 함수:
- 기본 생성자
- 소멸자
- 복사 생성자
- 복사 대입 연산자
C++11에서 추가된 특수 멤버 함수:
class Widget {
public:
…
Widget(Widget&& rhs); // 이동 생성자
Widget& operator=(Widget&& rhs); // 이동 대입 연산자
…
};
🔄 이동 연산의 자동 작성 조건
이동 연산들은 필요할 때에만 작성되며, 작성되는 경우에는 클래스의 비정적 멤버들에 대해 멤버별 이동을 수행한다.
클래스에 대한 이동 연산들은 다음 세 조건이 모두 만족될 때에만 자동으로 작성된다.
- 클래스에 그 어떤 복사 연산도 선언되어 있지 않다.
- 클래스에 그 어떤 이동 연산도 선언되어 있지 않다.
- 클래스에 소멸자가 선언되어 있지 않다.
이동 생성자가 선언되어 있으면 컴파일러가 이동 대입 연산자를 작성하지 않고, 이동 대입 연산자가 선언되어 있으면 컴파일러가 이동 생성자를 작성하지 않는다. 두 이동 연산은 독립적이지 않다.
📋 복사 연산의 자동 작성 조건
두 복사 연산은 서로 독립적이다. 하나를 선언한다고 해서 다른 하나의 작성이 방지되지는 않는다.
그러나 이동 연산이 하나라도 선언되어 있으면 복사 연산들이 삭제(비활성화)된다.
또한, 소멸자가 명시적으로 선언된 클래스에서 복사 연산들이 자동 작성되는 기능은 비권장(deprecated)이다. 소멸자를 직접 선언하면 단순한 멤버별 복사가 그 클래스에 적합하지 않을 가능성이 크기 때문이다.
🏛️ 3의 법칙 (Rule of Three)
만일 복사 생성자와 복사 대입 연산자, 소멸자 중 하나라도 선언했다면 나머지 둘도 선언해야 한다.
- 이 지침의 근원은, 클래스의 복사 대입 연산의 의미를 프로그래머가 직접 지정해야 할 필요성은 거의 항상 그 클래스가 자원 관리를 수행하기 때문에 생긴다는 것이다.
- 3의 법칙에 기초해서 추론하자면, 소멸자가 있는 클래스는 복사 연산들에 단순한 멤버별 복사가 적합하지 않을 가능성이 크다.
C++11은 3의 법칙의 추론과 “복사 연산 선언 시 이동 연산 암묵적 작성 배제” 규칙의 결합에 의해, 사용자 선언 소멸자가 있는 클래스에 대해서는 이동 연산들을 작성하지 않는다.
✅ 특수 멤버 함수 자동 작성 규칙 정리
| 특수 멤버 함수 | 자동 작성 조건 |
|---|---|
| 기본 생성자 | 클래스에 사용자 선언 생성자가 없을 때 |
| 소멸자 | C++98 규칙과 거의 같음. 기본적으로 noexcept |
| 복사 생성자 | 사용자 선언 복사 생성자 없을 때. 이동 연산이 하나라도 선언되어 있으면 삭제 |
| 복사 대입 연산자 | 사용자 선언 복사 대입 연산자 없을 때. 이동 연산이 하나라도 선언되어 있으면 삭제 |
| 이동 생성자 / 이동 대입 연산자 | 복사 연산, 이동 연산, 소멸자가 모두 선언되어 있지 않을 때 |
🛠️ = default 활용
- C++11에서는 기본 행동을 사용하겠다는 의사를
= default를 이용해서 명시적으로 표현할 수 있다.
class Widget {
public:
…
~Widget(); // 사용자 선언 소멸자
Widget(const Widget&) = default; // 기본 복사 생성자, 기본 행동 OK
Widget& operator=(const Widget&) = default; // 기본 복사 대입, 기본 행동 OK
…
};
- 다형적 부모 클래스에서의 활용 예시:
class Base {
public:
virtual ~Base() = default; // 소멸자를 가상으로
Base(Base&&) = default; // 이동 지원
Base& operator=(Base&&) = default;
Base(const Base&) = default; // 복사 지원
Base& operator=(const Base&) = default;
…
};
= default의 중요성:
class StringTable {
public:
StringTable() {}
…
~StringTable() // 추가됨
{ makeLogEntry("Destroying StringTable object"); }
private:
std::map<int, std::string> values;
};
- 소멸자를 선언하면 이동 연산들이 자동으로 작성되지 않는다.
StringTable객체를 ‘이동’하는 코드는 실제로는 바탕std::map<int, std::string>객체의 복사본을 생성한다.- 그냥 클래스에 소멸자를 하나 추가했을 뿐인데 엄청난 성능 문제가 발생하게 된다.
- 복사 연산들과 이동 연산들을
= default를 이용해서 명시적으로 정의했더라면 이런 문제가 없었을 것이다.
⚠️ 멤버 함수 템플릿과 특수 멤버 함수
- 멤버 함수 템플릿 때문에 특수 멤버 함수의 자동 작성이 금지되는 경우는 전혀 없다.
class Widget {
…
template<typename T> // 그 어떤 것으로도
Widget(const T& rhs); // Widget을 생성
template<typename T> // 그 어떤 것으로도
Widget& operator=(const T& rhs); // Widget을 대입
…
};
- 이 템플릿들이 복사 생성자나 복사 대입 연산자의 시그니처와 일치하는 함수들로 인스턴스화될 가능성이 있지만(T가
Widget이면 그런 일이 발생한다), 컴파일러는 여전히Widget의 복사 연산들과 이동 연산들을 작성한다.
🧐 정리
- 컴파일러가 스스로 작성할 수 있는 멤버 함수들, 즉 기본 생성자와 소멸자, 복사 연산들, 이동 연산들을 가리켜 특수 멤버 함수라고 부른다.
- 이동 연산들은 이동 연산들이나 복사 연산들, 소멸자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성된다.
- 복사 생성자는 복사 생성자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 복사 대입 연산자도 마찬가지다. 소멸자가 명시적으로 선언된 클래스에서 복사 연산들이 자동 작성되는 기능은 비권장이다.
- 멤버 함수 템플릿 때문에 특수 멤버 함수의 자동 작성이 금지되는 경우는 전혀 없다.
댓글남기기