KoreanFoodie's Study
Effective Modern C++ | 항목 26 : 보편 참조에 대한 중복적재를 피하라 본문
Effective Modern C++ | 항목 26 : 보편 참조에 대한 중복적재를 피하라
GoldGiver 2022. 10. 26. 09:59
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 26 : 보편 참조에 대한 중복적재를 피하라
핵심 :
1. 보편 참조에 대한 중복적재는 거의 항상 보편 참조 중복적재 버전이 예상보다 자주 호출되는 상황으로 이어진다.
2. 완벽 전달 생성자들은 특히나 문제가 많다. 그런 생성자는 대체로 비const 왼값에 대한 복사 생성자보다 더 나은 부합이며, 기반 클래스 복사 및 이동 생성자들에 대한 파생 클래스들의 호출들을 가로챌 수 있기 때문이다.
다음과 같은 코드가 있다고 가정하자.
std::multiset<std::string> names;
void logAndAdd(const std::string& name)
{
auto now =
std::chrono::system_clock::now();
log(now, "logAnddAdd");
names.emplace(name);
}
...
std::string petName("Darla");
logAndAdd(petName); // 왼값
logAndAdd(std::string("Perephone")); // 오른값
logAndAdd("Patty Dog"); // 문자열 리터럴
첫 호출에서 매개변수 name 은 petName 에 묶이며, name 이 왼값이므로, emplace 는 그것을 names 에 복사한다.
둘째 호출에서는 매개변수 name 이 오른값에 묶인다. name 자체는 왼값이므로 names 에 복사된다. 그러나 원칙적으로 name 의 값을 names 로 이동하는 것이 가능하다. 따라서 복사 1회의 비용이 발생하지만 대신 한 번의 이동으로 작업을 완수할 여지가 있다.
셋째 호출에서도 매개변수 name 은 오른값에 묶여 std::string 복사 1회의 비용(문자열 리터럴에서 암묵적으로 복사 생성자 호출)을 치르지만, 원칙적으로는 복사는 커녕 이동 비용을 치를 필요도 없다.
logAndAdd 가 보편 참조를 받게 하고 std::forward 를 사용하면, 둘째 호출과 셋째 호출의 비효율성을 제거할 수 있다.
template<typename T>
void logAndAdd(T&& name)
{
auto now =
std::chrono::system_clock::now();
log(now, "logAnddAdd");
names.emplace(std::forward<T>(name));
}
그런데 이제는 std::string 타입이 아니라, 정수 인자를 통해 std::string 색인을 찾는 오버로드 함수를 정의했다고 하자.
void logAndAdd(int idx)
{
auto now =
std::chrono::system_clock::now();
log(now, "logAnddAdd");
names.emplace(nameFromIdx(idx));
}
logAndAdd(22);
하지만 다음과 같은 코드는 오류를 발생시킨다.
short nameIdx = 26;
logAndAdd(nameIdx);
logAndAdd 의 중복적재는 두 가지인데, 보편 참조를 받는 버번은 T 를 short& 로 연역할 수 있으며, 그러면 주어진 인수와 정확히 부합하는 형태가 된다. int 를 받는 버전은 short 인수를 int 로 승겨(promotion) 해야 호출과 부합한다. 보통의 중복적재 해소 규칙에 따라, 정확한 부합이 승격을 통한 부합보다 우선시된다. 따라서 보편 참조 중복적재가 호출된다.
그런데 보편 참조 중복 적재는 전달받은 인자로 std::string 객체를 생성하려고 시도한다. 이 부분에서 에러가 발생하게 된다! 보편 참조 중복 적재는 자동으로 생성하는 복사 생성자와 이동 생성자와도 경쟁을 해서 이김(?) 으로써, 이상한 동작을 하도록 만들 수 있다!
class Person {
public:
template<typename T>
explicit Person(T&& n)
: name(std::forward<T>(n)) {}
explicit Person(int idx);
// 복사 생성자(컴파일러가 생성)
Person(const Person& rhs);
// 이동 생성자(컴파일러가 생성)
Person(Person&& rhs);
};
...
Person p("Nancy");
// 컴파일 에러!
auto cloneOfP(p);
위의 코드에서 cloneOfP 는 복사 생성자를 호출하지 않고, Person 을 받는 완벽 전달 생성자를 호출한다(인스턴스화가 이루어짐)! 그 이유는, p 는 Person& 타입(왼값)이지 const Person& 이 아니기 때문이다(const 가 없어서 복사 생성자가 호출이 안됨)!
만약 코드를 다음과 같이 바꾸면 컴파일이 될 것이다.
const Person p("Nancy");
// 컴파일 에러!
auto cloneOfP(p);
C++ 는 오버로딩 해소 중 어떤 함수 호출이 템플릿 인스턴스와 비템플릿 함수(즉, '보통' 함수)에 똑같이 부합한다면 보통 함수를 우선시 한다는 규칙이 있다.
마지막으로, 상속이 관여할 경우, 문제가 더 커질 수 있다.
class SpecialPerson: public Person {
public:
SpecialPerson(const SpecialPerson& rhs)
: Person(rhs) // 기반 클래스의 완벽 전달 생성자 호출!
{ ... }
SpecialPerson(const SpecialPerson& rhs)
: Person(std::move(rhs)) // 기반 클래스의 완벽 전달 생성자 호출!
{ ... }
};
위의 코드에서, 기반 클래스의 복사, 이동 생성자들을 호출하지 않는 이유는 무엇일까? 일단, 인자로 SpecialPerson 을 전달하고 있는데 기반 클래스에서는 파생클래스가 존재하는지 아닌지조차 모른다. 따라서 애초에 기반 클래스에서 파생 클래스를 인수로 받은 생성자를 생성한다는 것이 논리적으로 말이 되지 않는다. 그러므로 자연스럽게, 인자 형식을 SpecialPerson 이 되는 완벽 전달 생성자가 인스턴스화 되어, 복사나 이동 생성자 대신 호출되는 것이다!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective Modern C++ | 항목 28 : 참조 축약을 숙지하라 (0) | 2022.10.26 |
---|---|
Effective Modern C++ | 항목 27 : 보편 참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아 두라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 25 : 오른값 참조에는 std::move 를, 보편 참조에는 std::forward 를 사용하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 24 : 보편 참조와 오른값 참조를 구별하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 23 : std::move 와 std::forward 를 숙지하라 (0) | 2022.10.26 |