// 사람 이름 하나를 매개변수로 받고, 현재 날짜와 시간을 기록하여 전역 자료구조에 추가하는 예제
std::multiset<std::string> names; // 전역 자료구조
void logAndAdd(const std::string& name)
{
auto now = std::chrono::system_clock::now(); // 현재 시간을 얻고
log(now, "logAndAdd"); // 로그에 기록
names.emplace(name); // 이름을 전역 자료구조에 추가
}
std::string petName("Darla");
logAndAdd(petName); // 왼값 std::string을 넘겨줌
logAndAdd(std::string("Persephone")); // 오른값 std::string을 넘겨줌
logAndAdd("Patty Dog"); // 문자열 리터럴을 넘겨줌
logAndAdd 첫 호출에서 매개변수 name은 petName에 묶여 names.emplace로 전달된다. name이 왼값이므로 emplace에서 names로 복사한다.
두번째 호출에서 매개변수 name이 오른값("Persephon"로부터 명시적으로 생성된 임시 std::string객체)에 묶인다. name 자체는 왼값이므로 names에 복사되지만 name의 값을 names로 이동이 가능하기 때문에 복사를 피할 여지가 있다.
세번째 호출에서도 매개변수 name은 오른값에 묶이지만 암묵적으로 생성된 임시 객체 std::string에 묶였다. 하지만 문자열 리터럴이기 때문에 emplace에 직접 전달시 std::multiset안에서 std::string객체를 직접 생성한다.
logAndAdd가 보편 참조를 받게 하면, 즉 std::forward를 이용해서 emplace에 전달하면 둘째 호출과 셋째 호출의 비효율성을 제거할 수 있다.
template<typename T>
void logAndAdd(T&& name)
{
auto now = std::chrono::system_clock::now(); // 현재 시간을 얻고
log(now, "logAndAdd"); // 로그에 기록
names.emplace(std::forward<T>(name)); // 이름을 전역 자료구조에 추가
}
std::string petName("Darla");
logAndAdd(petName); // 왼값 std::string을 넘겨줌, 이전과 동일하게 multiset으로 복사됨
logAndAdd(std::string("Persephone")); // 오른값을 이동함
logAndAdd("Patty Dog"); // 임시 std::string을 복사하는 대신 multiset안에 std::string생성
만약 클라이언트가 logAndAdd가 요구하는 문자열 형태의 이름을 직접 얻지 못하고, 색인만 알고 있으며 그 색인을 통해 이름을 조회해야 한다. 이를 지원하기 위해 logAndAdd를 중복적재 한다고 하자.
std::string nameFromIdx(int idx);
void logAndAdd(int idx)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(nameFromIdx(idx));
}
std::string petName("Darla");
logAndAdd(petName);
logAndAdd(std::string("Persephone"));
logAndAdd("Patty Dog"); // 이전과 동일하며 모두 T&&를 받는 중복적재 버전 호출
logAndAdd(22); // int 버전 호출
short nameIdx;
logAndAdd(nameIdx); // 오류
logAndAdd의 중복적재는 보편참조를 받는 버전과 int를 받는 버전의 2개의 중복적재가 있다. short는 보편참조를 받는 버전의 T를 short&로 연역했을 때 정확히 부합하는 형태가 되며, int 버전의 경우 short인수를 int로 승격해야 호출에 부합하게 된다. 즉 보편참조 중복적재가 호출하게 된다.
보편참조를 받는 중복적재에서 매개변수 name은 short에 묶여서 names(std::multiset<std::string> 객체)의 멤버 함수 emplace에 전달된다. 하지만 std::string의 생성자 중에 short를 받는 버전이 없기 때문에 multiset::emplace호출 안의 std::string생성자 호출이 실패한다.
보편참조를 받는 템플릿 함수는 거의 모든 형식의 인수와 정확히 부합한다. 그렇기 때문에 보편참조와 중복 적재를 결합하는것은 거의 항상 나쁜 선택이라 할 수 있다.
이러한 문제는 완벽 전달 생성자를 작성해서 피할 수 있다. std::string 또는 자유 색인을 받는 자유 함수 대신 그와 같은 생성자를 가진 Person이라는 클래스를 도입해보자
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 coneOfP(p); // p로부터 새 Person 생성, 컴파일 실패!
함수는 복사 생성자를 호출하지 않고 Person의 std::string 자료 멤버를 Person 객체(p)로 생성하려 한다. 하지만 std::string에는 Person을 받는 생성자가 없어서 컴파일 에러가 발생한다. 이 예에서 컴파일러는 cloneOfP를 const가 아닌 왼값(p)로 초기화하며, 템플릿화 된 생성자를 Person 형식의 비 const 왼값을 받는 형태로 인스턴스화 할 수 있다.
// 인스턴스화 된 Person 클래스
class Person{
public:
explicit Person(Person& n) // 완벽 전달 템플릿에서 인스턴스화 됨
: name(std::forward<Person&>(n)) {}
...
};
auto cloneOfP(p); 에서 복사 생성자를 호출하려면 p에 const를 추가해서 인수를 복사 생성자의 매개변수 형식과 부합시켜야 한다. 그러나 인스턴스화된 템플릿은 이미 부합하기 때문에 템플릿에서 인스턴스화된 중복적재를 호출하게 된다. 즉, Person의 비 const 왼값의 '복사'를 복사 생성자가 아닌 완벽 생성 생성자가 처리하게 된다.
하지만 복사할 객체가 const가 되도록 바꾸면 정상적으로 복사 생성자를 호출하게 된다.
const Person cp("Nancy"); // 객체가 const
auto cloneOfP(p); // 복사 생성자 호출
// 이 경우에 생성되는 인스턴스
class Person{
public:
explicit Person(const Person& n); // 템플릿에서 인스턴스화 됨
Person(const Person& rhs); // 자동 생성된 복사 생성자
...
};
이때 생성되는 인스턴스도 함수 호출에서 템플릿 인스턴스와 비템플릿 함수에 똑같이 부합한다면 비템플릿 함수를 우선시하기 때문에 같은 같은 서명을 갖게되더라도 복사 생성자가 선택된다.
상속이 관여할 경우 좀 더 복잡해진다.
class SpecialPerson: public Person{
public:
SpecialPerson(const SpecialPerson& rhs) // 복사 생성자, base class의 완벽 전달 생성자 호출
: Person(rhs)
{...}
SpecialPerson(SpecialPerson&& rhs) // 이동 생성자, base class의 완벽 전달 생성자 호출
: Person(std::move(rhs))
{...}
};
파생 클래스 함수들이 SpecialPerson 형식의 인수를 base class에 넘겨주게 되고, SpecialPerson을 받는 std::string 생성자가 없어 코드가 컴파일이 되지 않는다.
Conclusion
- 보편 참조에 대한 중복적재는 거의 항상 보편 참조 중복 적재 버전이 예상보다 자주 호출되는 상황으로 이어진다.
- 완벽 전달 생성자들은 문제가 더 많다. 그런 생성자들은 대체로 비const 왼값에 대한 복사 생성자보다 더 나은 부합이며, 기반 클래스 복사 및 이동 생성자들에 대한 파생 클래스의 호출들을 가로챌 수 있다.
Reference
Effective Modern C++ 항목 26 : 보편 참조에 대한 중복 적재를 피하라
'C++ > Effective Modern C++' 카테고리의 다른 글
보편참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아두라 2 (0) | 2020.07.23 |
---|---|
보편참조에 대한 중복적재 대신 사용할수 있는 기법들 1 (0) | 2020.07.22 |
std::move, std::forward (0) | 2020.07.17 |
Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정리하라 (0) | 2020.07.16 |
new 보다 std::make_shared와 make_unique를 선호하라 (0) | 2020.07.15 |
댓글