std::multiset<std::string> names;
void logAndAdd(const std::string& name) {
...
names.emplace(name);
}
std::string petName("Darla");
logAndAdd(petName); // 왼값
logAndAdd(std::string("Persephone")); // 오른값
logAndAdd("Patty Dog"); // 문자열 리터럴
인수가 왼값인 경우 emplace의 복사는 피할 수 없다.
인수가 오른값인 경우 매개변수 name은 오른값에 묶이지만 name 자체는 왼값이기 때문에 복사가 이루어진다. 하지만 인수가 오른값이기 때문에 원칙적으로는 복사 대신 이동을 적용할 여지가 있다.
인수가 리터럴 문자열인 경우 두번째 경우와 마찬가지로 매개변수 name은 오른값에 묶이지만 name 자체는 왼값이기 때문에 복사가 이루어지며 이동을 적용할 여지가 있다. 약간의 차이가 있다면 두번째 경우는 명시적인 임시 string 객체를 생성하고 매개변수가 거기에 묶였지만, 리터럴 문자열은 암묵적으로 생성된 임시 string 객체에 name이 묶인다.
만약 문자열 리터럴을 직접 emplace에 전달했다면 multiset 안에서 직접 string 객체를 생성했을 것이다. 애초에 복사나 이동 비용을 치를 필요가 없던 것이다.
std::multiset<std::string> names;
template<typename T>
void logAndAdd(T&& name) {
...
names.emplace(std::forward<T>(name));
}
std::string petName("Darla");
logAndAdd(petName); // 왼값 복사
logAndAdd(std::string("Persephone")); // 오른값 이동
logAndAdd("Patty Dog"); // 임시객체 복사 대신 multiset 안에 string 생성
보편 전달을 적용함으로써 최적화를 이뤄낼 수 있다.
이번에는 인수가 문자열이 아니라 정수타입 변수를 통해 인덱스를 전달하고 함수 내에서 해당 인덱스를 통해 이름을 가져오는 함수를 오버로딩 한다고 해보자.
std::string nameFromIdx(int idx); // idx에 해당하는 이름 반환
void logAndAdd(int idx) {
...
names.emplace(nameFromIdx(idx));
}
logAndAdd(22);
잘 해소된것처럼 보이지만 short 타입을 전달하면 에러가 발생한다.
short타입을 캐스팅 등을 통해 int타입으로 변환하지 않는 이상 보편 참조를 받는 버전의 함수가 호출된다.
이 때, T&&는 short&로 추론되며 name에 묶이고 emplace 함수를 호출할 때, name을 string 생성자에 전달하지만 short타입을 받는 생성자는 존재하지 않기 때문에 에러가 발생하는 것이다.
보편 참조를 받는 함수 템플릿은 생각보다 많은 타입들을 빨아들이기 때문에 오버로딩과 결합하는 것은 웬만해서는 나쁜 선택이 된다.
이런 문제를 쉽게 피하는 방법 중 하나는 완벽 전달(Perfect Forwarding) 생성자를 작성하는 것이지만 더 심각한 문제를 야기할수도 있다.
class Person {
public:
template<typename T>
explicit Person(T&& n) : name(std::forward<T>(n)) {} // 완벽 전달 생성자
explicit Person(int idx) : name(nameFromIdx(idx)) {}
Person(const Person& rhs); // 복사 생성자
Person(Person&& rhs); // 이동 생성자
private:
std::string name;
};
Person p("Nancy");
auto cloneOfP(p); // 복사 생성자 호출?
완벽 전달 생성자, 복사 생성자, 이동 생성자 모두 정의되어있는 클래스이다.
cloneOfP는 복사 생성자를 호출함으로써 객체를 생성하려고 했지만 실제로 호출되는 것은 완벽 전달 생성자이다.
class Person {
public:
explicit Person(Person& n) : name(std::forward<Person&>(n)) {}
...
};
cloneOfP의 생성자는 Person& 타입의 매개변수를 받는 형태로 인스턴스화 된다. p가 비const 객체이기 때문에 복사 생성자보다는 완벽 전달 생성자의 매개변수 타입에 더 부합하기 때문에 완벽 전달 생성자가 호출된다.
만약 p가 const 객체였다면 복사 생성자의 매개변수 타입에 부합하므로 복사 생성자가 호출될 것이다.
class Person {
public:
explicit Person(const Person& n);
Person(const Person& rhs);
...
};
그런데 p가 const객체로 전달되는 경우 생성자 템플릿의 매개변수가 복사 생성자와 똑같아진다. 이런 경우 템플릿 함수보다 비템플릿 함수(일반적인 함수)의 호출을 우선시한다는 규칙에 의해 자동으로 작성된 복사 생성자가 호출된다.
class SpecialPerson : public Person {
public:
SpecialPerson(const SpecialPerson& rhs) : Person(rhs) {}
SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)) {}
...
};
상속이 관여하는 클래스에서는 완벽 전달 생성자와 컴파일러가 작성한 복사 및 이동 연산들 사이의 상호작용이 미치는 여파가 더욱 크다.
파생 클래스의 복사, 이동 생성자가 기반 클래스의 복사, 이동 생성자를 호출하는 것 처럼 보이지만 실제로는 둘 다 완벽 전달 생성자를 호출하고 있다.
보편 참조에 대한 함수 오버로딩에 대한 해결책은 다음 항목에서 다루도록 한다.
◾ 보편 참조에 대한 오버로딩은 거의 항상 보편 참조 오버로딩 버전이 예상보다 자주 호출되는 상황으로 이어진다.
◾ 완벽 전달 생성자들은 특히나 문제가 많다. 그런 생성자는 대체로 비const 왼값에 대한 복사 생성자보다 더 나은 부합이며, 기반 클래스 복사 및 이동 생성자들에 대한 파생 클래스의 호출들을 가로챌 수 있기 때문이다.
'도서 > Effective Modern C++' 카테고리의 다른 글
[5장] 항목 28 : 참조 축약을 숙지하라 (0) | 2023.01.25 |
---|---|
[5장] 항목 27 : 보편 참조에 대한 오버로딩 대신 사용할 수 있는 기법들을 알아두라 (0) | 2023.01.25 |
[5장] 항목 25 : 오른값 참조에는 std::move를, 보편 참조에는 std::forward를 사용하라 (0) | 2023.01.25 |
[5장] 항목 24 : 보편 참조와 오른값 참조를 구별하라 (0) | 2023.01.24 |
[5장] 항목 23 : std::move와 std::forward를 숙지하라 (0) | 2023.01.24 |