Effective C++/Effective Modern C++
[Effective Modern C++] 26. 보편 참조 오버로드 금지
김디트
2025. 3. 14. 11:42
728x90
항목 26. 보편 참조에 대한 오버로드를 피하라
템플릿 함수의 오버로드
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); // lValue 문자열
// 함수 내의 name이 lValue이므로 names로 복사된다.
// 전달된 값이 lValue이므로 이 복사는 피할 수 없다.
logAndAdd(std::string("Persephone")); // rValue 문자열
// 함수 내의 name이 lValue이므로 names로 복사된다.
// 전달된 값이 rValue(임시 std::string 객체)이므로 names로 이동시킬 여지가 있다.
logAndAdd("Patty Dog"); 문자열 리터럴
// 임시 생성된 std::string 객체가 name에 rValue로 묶인다.
// 함수 내의 name이 lValue이므로 names로 복사된다.
// 문자열 리터럴을 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);
// 이전과 동일하다.
logAndAdd(std::string("Persephone"));
// rValue를 이동한다.
logAndAdd("Patty Dog");
// multiset 안에 std::string을 직접 생성한다.
- logAndAdd가 int를 매개변수로 받고 싶어졌다.
- 여기서 템플릿 함수의 오버로드가 발생한다.
std::string nameFromIdx(int idx); // 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");
logAndAdd(22); // int 버전 사용
- 헌데 아래와 같이 사용하면 문제가 발생하고 만다.
short nameIdx; // short 타입
...
logAndAdd(nameIdx); // 에러!!!!
- int 버전을 사용하고 싶었지만...
- 템플릿 함수가 short&로 추론하여 실행된다.
- short로 std::string을 생성할 방법은 없으므로 컴파일 에러가 발생한다.
- 보편 참조를 받는 템플릿 함수는 C++에서 가장 욕심 많은 함수이다.
- 거의 모든 타입의 인수와 정확히 부합한다.
- 그러므로 보편 참조와 함수 오버로드를 결합하는 건 거의 항상 나쁜 선택이다.
위와 같은 상황을 피해보기?
- 보편 참조 생성자 작성으로 회피해보도록 해본다.
// 색인을 받는 전역 함수를 만드는 대신에
// 같은 일을 하는 생성자들을 가진 Person이라는 클래스를 도입한다.
class Person
{
public:
template<typename T>
explicit Person(T&& n) : name(std::Forward<T>(n)) {}
explicit Person(int idx) : name(nameFromIdx(idx)) {}
...
private:
std::string name;
};
- 여전히 short 등은 컴파일 에러를 야기한다.
- 더군다나 이 경우엔 더 큰 문제가 발생한다!!
- 복사 생성자와 이동 생성자가 자동으로 생성될 수 있으므로 더 많은 오버로드가 존재하는 셈이기 때문!
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); // 컴파일 에러!!!! 왜?
// 생성자가 아니라 보편 참조 생성자를 사용하기 때문이다!!
- 전달한 lValue 값은 const가 없기 때문에
- 원래 사용했어야 할 Person(const Person& rhs); 버전보다
- 템플릿 생성자 버전이 더 부합하기 때문이다.
const Person cp("Nancy"); // const 적용
auto cloneOfP(cp); // 이제 복사 생성자를 사용한다.
- 물론 이 경우에도 템플릿 버전이 사용될 수도 있겠지만...
- C++ 오버로드 해소 규칙 중에는 어떤 함수 호출이 템플릿, 비템플릿 둘에 똑같이 부합하면
- 비템플릿(즉 보통 함수)를 우선시한다는 규칙이 있다.
- 상속이 관여하면 더 위험해진다.
class SpecialPerson : public Person
{
public:
// 두 버전 모두 base 클래스의 것을 호출한다.
SpecialPerson(const SpecialPerson& rhs) : Person(rhs) { ... }
SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)) { ... }
};
- 이 경우 base 클래스의 보편 참조 생성자를 호출하게 된다!
- SpecialPerson을 받는 생성자가 없기 때문이다.
- 그렇다면 어떻게 해결해야 할까? 는... (항목 27 참조)
728x90