1. 중복적재를 포기한다
중복 적재 버전에 각자 다른 이름을 붙여 보편 참조에 대한 단점을 피할 수 있다. 하지만 생성자의 이름은 언어에 의해 고정되기 때문에 중복적재가 필요할 수 있다.
2. const T& 매개변수를 사용한다.
보편 참조 매개변수 대신 const에 대한 왼값 참조 매개변수를 사용한다. 이런 설계는 원하는 만큼 효율적이지 않다.
3. 값 전달 방식의 매개변수를 사용한다
참조 전달 매개변수 대신 값 전달 매개변수를 사용한다. 복사될 것이 확실한 객체는 값으로 전달하는 것을 고려하는 것이 좋다. 앞에 설명했던 예제의 Person을 사용해 결과를 보도록 하자
class Person {
public:
explicit Person(std::string n) // T&& 생성자를 대체한다
: name(std::move(n)) {}
explicit Person(int idx)
: name(nameFromIdx(idx)) {}
...
private:
std::string name;
};
정수 하나만 받는 std::string 생성자는 없으므로 int 형식 / int류 형식의 인수로 Person 생성자를 호출하면 int를 받는 중복적재가 선택되며 std::string 형식의 인수에 대해서는 std::string을 받는 생성자가 선택된다.
4. 꼬리표 배분을 사용한다
const 왼값 참조 전달이나 값 전달은 완벽 전달을 지원하지 않는다. 보편 참조를 사용하려는 이유가 완벽 전달이면 보편 참조 말고는 다른 대안이 없다. 중복 적재된 함수에 대해 컴파일러는 중복적재된 버전의 모든 가능한 조합을 평가해 가장 잘 부합하는 것을 선택한다. 일반적으로 보편 참조 매개변수는 전달된 인수에 대해 정확히 부합하지만 매개변수 목록에 보편참조가 아닌 매개변수들도 포함되어 있으면 나쁜 부합이 선택될 수 있다.
std::multiset<std::string> name;
template<typename T>
void logAndAdd(T&& name)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
전에 설명했듯이 위의 예제에 int를 받는 중복적재를 추가하면 문제가 생기게 된다.
하지만 그 대신 logAndAdd가 호출을 다른 두 함수로 위임하게 한다.
실제 작업은 logAndAddImpl에서 하고 이 함수는 중복적재와 보편참조를 모두 사용한다. 그리고 전달된 인수가 정수형식인지 아닌지를 뜻하는 또 다른 보편참조를 받는다. 그로인해 int를 받는 중복적재를 추가했을때의 문제를 피할 수 있다.
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(std::forward<T>(name), std::is_integral<T>());
}
이 함수는 자신의 매개변수를 logAndAddImpl에 전달하며 매개변수의 형식(T)이 정수인지를 뜻하는 인수도 전달한다. 오른값 정수 인수들도 잘 작동한다. 하지만 보편 참조 name으로 전달된 인수가 왼값이면 T는 왼값 참조 형식으로 연역되어 int 형식이 logAndAdd에 전달되면 T는 int&로 연역된다. 참조는 정수 형식이 아니므로 std::is_integral<T>는 임의의 왼값 인수에 대해 거짓이 된다.
이 문제는 std::remove_reference라는 형식 특질을 사용해 모든 참조 한정사를 제거하는 것으로 해결 할 수있다.
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(std::forward<T>(name), std::is_integral<typename std::remove_reference<T>:type>());
// logAndAddImpl(std::forward<T>(name), std::is_integral<std::remove_reference_t<T>>()); C++14버전
};
그렇다면 여기서 logAndAddImpl 함수를 살펴보도록 하자
template<typename T>
void logAndAddImpl(T&& name, std::false_type) // 비정수 인수를 받는 버전
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
개념적으로 logAndAdd는 자신에게 전달된 인수가 정수 형식인지 아닌지를 뜻하는 bool값을 logAndAddImpl에 넘겨준다. true와 false는 실행 시점 값이다. 컴파일 시점에서 중복 적재 해소 과정에 필요한 형식이 필요한데 true에 해당하는 std::true_type, false에 해당하는 std::false_type이라는 형식을 제공한다. 만일 T가 정수 형식이면 std::true_type을 상속하는 어떤 형식의 객체가 되고, 정수 형식이 아니면 std::false_type을 상속하는 어떤 형식의 객체가 된다. 결과적으로 logAndAddImpl 중복적재는 정수 형식이 아닌 T로 logAndAdd를 호출했을 때에만 적법한 후보가 된다
std::string nameFromIdx(int idx);
void logAndAddImpl(int idx, std::true_type) // 정수 인수를 받는 버전
{
logAndAdd(nameFromIdex(idx));
}
여기서는 주어진 색인으로 이름을 조회해서 logAndAdd를 호출한다. 그러면 그 이름은 std::forward를 통해 다른 logAndAddImpl 중복 적재로 전달된다.
이러한 설계에서 std::true_type과 std::false_type형식은 중복적재 해소를 하는데 있어 일종의 '꼬리표'이다. logAndAdd 안에서 중복적재된 구현 함수들을 호출하는 것은 주어진 작업을 꼬리표에 기초해 dispatch 하는것에 해당한다.
여기서 사실 logAndAdd는 제한 없는 보편 참조 매개변수를 받지만 이 함수 자체는 중복적재되지 않는다. 중복 적재되는 것은 구현함수인 logAndAddImpl이며, 이 중복적재 또한 꼬리표 매개변수에 의존한다. 그리고 그 꼬리표는 주어진 인수에 대해 하나의 중복적재만 유효한 부합이 되도록 설계되었다.
Reference
Effective Modern C++ 항목 27: 보편참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아두라
'C++ > Effective Modern C++' 카테고리의 다른 글
참조축약을 숙지하라 (0) | 2020.07.27 |
---|---|
보편참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아두라 2 (0) | 2020.07.23 |
보편 참조에 대한 중복적재를 피하라 (0) | 2020.07.21 |
std::move, std::forward (0) | 2020.07.17 |
Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정리하라 (0) | 2020.07.16 |
댓글