본문 바로가기
C++/Effective Modern C++

보편참조에 대한 중복적재 대신 사용할수 있는 기법들 1

by COCO1337 2020. 7. 22.

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: 보편참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아두라

반응형

댓글