27. Rule of Three/Five/Zero: 사용자 정의 소멸자/복사가 이동 생성자에 미치는 영향
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C++
// 문제 27 — Rule of Three/Five/Zero: 사용자 정의 소멸자/복사생성자가
// 이동 생성자의 암시적 생성에 미치는 영향
//
// 아래는 게임 서버에서 인벤토리 아이템 목록을 담는 세 가지 버전의 클래스다.
// 셋 다 겉보기엔 비슷하지만, 특수 멤버 함수를 선언한 방식이 다르다.
//
// (A) ItemBagLoud : 사용자 정의 소멸자만 선언(로그를 남기기 위해).
// (B) ItemBagCopyOnly : 사용자 정의 복사 생성자 + 복사 대입 연산자만 선언
// (소멸자·이동은 선언하지 않음).
// (C) ItemBagExplicit : 소멸자를 선언하되, 이동 생성자/이동 대입 연산자를
// "= default"로 명시 선언.
//
// 세 클래스 모두 std::vector<ItemBagX>에 push_back을 반복해 재할당이 여러 번
// 일어나게 만든 뒤, 각 클래스의 "복사 생성자 호출 횟수"와 "이동 생성자 호출
// 횟수"를 카운터로 센다.
//
// 과제:
// 1. ItemBagLoud, ItemBagCopyOnly, ItemBagExplicit 각각에 대해 컴파일러가
// 이동 생성자를 암시적으로 생성하는지(생성한다면 그것이 실제로 호출되는지),
// 아니면 이동이 필요한 자리에서 복사로 폴백(fallback)되는지 예측하라.
// 2. std::vector<T>::push_back으로 인한 재할당 시 원소들이 이동되는지 복사되는지는
// 이동 생성자의 존재 여부뿐 아니라 그 이동 생성자가 noexcept인지에도 좌우된다.
// 이 세 클래스에 대해 vector 재할당 시 실제로 어느 연산(복사/이동)이 몇 번
// 호출되는지 카운터 값을 예측하라.
// 3. "사용자 정의 소멸자가 하나라도 있으면 이동 생성자가 암시적으로 삭제/미생성된다"는
// 규칙이 정확히 어떤 조건에서 적용되는지, ItemBagExplicit처럼 이동을
// "= default"로 명시하면 왜 다시 정상적으로 이동이 일어나는지 설명하라.
//
// 아래 코드는 그대로 컴파일·실행 가능하다 (g++ -std=c++17).
#include <iostream>
#include <vector>
#include <string>
#include <type_traits>
// ---------- (A) 소멸자만 사용자 정의 ----------
struct ItemBagLoud {
std::string name;
static inline int copyCount = 0;
static inline int moveCount = 0;
explicit ItemBagLoud(std::string n) : name(std::move(n)) {}
~ItemBagLoud() {
// 소멸 로그(실제 학습 목적상 카운트는 세지 않음)
}
ItemBagLoud(const ItemBagLoud& other) : name(other.name) {
++copyCount;
}
ItemBagLoud& operator=(const ItemBagLoud& other) {
name = other.name;
++copyCount;
return *this;
}
// 이동 생성자/이동 대입 연산자는 선언하지 않음.
};
// ---------- (B) 복사 생성자/복사 대입만 사용자 정의 ----------
struct ItemBagCopyOnly {
std::string name;
static inline int copyCount = 0;
static inline int moveCount = 0;
explicit ItemBagCopyOnly(std::string n) : name(std::move(n)) {}
ItemBagCopyOnly(const ItemBagCopyOnly& other) : name(other.name) {
++copyCount;
}
ItemBagCopyOnly& operator=(const ItemBagCopyOnly& other) {
name = other.name;
++copyCount;
return *this;
}
// 소멸자, 이동 생성자/이동 대입 연산자 모두 선언하지 않음.
};
// ---------- (C) 소멸자 + 이동을 명시적으로 default ----------
struct ItemBagExplicit {
std::string name;
static inline int copyCount = 0;
static inline int moveCount = 0;
explicit ItemBagExplicit(std::string n) : name(std::move(n)) {}
~ItemBagExplicit() {}
ItemBagExplicit(const ItemBagExplicit& other) : name(other.name) {
++copyCount;
}
ItemBagExplicit& operator=(const ItemBagExplicit& other) {
name = other.name;
++copyCount;
return *this;
}
ItemBagExplicit(ItemBagExplicit&& other) noexcept : name(std::move(other.name)) {
++moveCount;
}
ItemBagExplicit& operator=(ItemBagExplicit&& other) noexcept {
name = std::move(other.name);
++moveCount;
return *this;
}
};
template <typename T>
void FillAndReport(const char* label, int n) {
std::vector<T> bag;
for (int i = 0; i < n; ++i) {
bag.push_back(T(std::to_string(i)));
}
std::cout << label
<< " is_move_constructible=" << std::is_move_constructible<T>::value
<< " copyCount=" << T::copyCount
<< " moveCount=" << T::moveCount
<< "\n";
}
int main() {
std::cout << "[static-check] ItemBagLoud: is_nothrow_move_constructible="
<< std::is_nothrow_move_constructible<ItemBagLoud>::value << "\n";
std::cout << "[static-check] ItemBagCopyOnly: is_nothrow_move_constructible="
<< std::is_nothrow_move_constructible<ItemBagCopyOnly>::value << "\n";
std::cout << "[static-check] ItemBagExplicit: is_nothrow_move_constructible="
<< std::is_nothrow_move_constructible<ItemBagExplicit>::value << "\n";
FillAndReport<ItemBagLoud>("[ItemBagLoud] ", 5);
FillAndReport<ItemBagCopyOnly>("[ItemBagCopyOnly]", 5);
FillAndReport<ItemBagExplicit>("[ItemBagExplicit]", 5);
return 0;
} 내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.