보편참조를 받는 템플릿을 제한한다.
std::enable_if는 컴파일러가 특정 템플릿을 존재하지 않는 것처럼 할 수 있다. 그런 템플릿을 비활성화된 템플릿이라고 한다. 기본적으로 모든 템플릿은 활성화 상태이지만 std::enable_if를 사용하는 경우, 특정 조건이 만족될 때에만 활성화된다.
일단 기존의 예제에 std::enable_if만 추가해보자
class Person{
public:
template<typename T, typename = typename std::enable_if<조건>::type>
explicit Person(T&& n);
...
};
// std::enable_if와 그 작동원리인 SFINAE는 나중에 따로 정리
여기서 우리가 지정하려고 하는 조건은 T가 Person이 아니라는 것이다. 이것을 판별할 유용한 형식특질로 두 형식이 같은지를 판별하는 std::is_same이 있다. 그래서 조건식을 !std::is_same<Person, T>::value라고 두면 될 것이다. 하지만 왼값으로 초기화되는 보편참조는 항상 왼값 참조로 연역되기 때문에 수정이 필요하다(std::is_same은 Person형식과 &Person형식을 서로 다르다고 판정). 이를 제대로 판별하기 위해서는 참조 여부와 const, volatile성을 모두 제거해 줘야 한다. 그러기 위해 std::decay를 사용한다.
class Person{
public:
template<typename T,
typename = typename std::enable_if<
!std::is_same<Person, typename std::decay<T>::type>::value>::type>
explicit Person(T&& n);
...
};
이렇게 수정된 예제는 Person을 어떤Person으로 생성하는 경우 왼값이든 오른값이든, const성/volatile성과 관계없이 보편참조를 받는 생성자는 절대 호출하지 않는다.
하지만 파생된 클래스의경우 통상적인 복사연산들과 이동연산들은 문제가 생긴다. 그 연산들은 모두 base 클래스의 완벽전달 생성자를 호출하게 된다. 그래서 이 또한 바로잡아줘야 한다. 즉, Person도 아니고 Person의 파생 클래스로된 형식도 아니라는 조건이 필요하다.
파생된 형식인지 확인하기 위한 형식 특질은 std::is_base_of이다. std::is_base_of<T1, T2>::value는 만일 T2가 T1에 파생된 형식이면 참이다. 사용자 정의 형식은 자기 자신으로 부터 파생된 것으로 간주된다. 즉 std::is_base_of<T, T>::value는 만일 T가 사용자 정의 형식이면 참이다(T가 내장형식일 경우는 거짓). 이것을 사용해 위의 예제를 수정해보자
class Person{ // C++11 버전
public:
template<
typename T,
typename = typename std::enable_if<
!std::is_base_of<Person, typename std::decay<T>::type
>::value
>::type
>
explicit Person(T&& n);
...
};
class Person{ // C++14 버전
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>
>::value
>
>
explicit Person(T&& n);
...
};
하지만 여전히 정수/비정수 인수를 구분하는 방법이 남았다.
정수 인수들을 처리하는 Person 생성자 중복적재를 추가하고 그런 인수들에 대해 템플릿화된 생성자가 비활성화 되도록 조건을 추가한다. 이를 예제에 다시 추가해보자
class Person{ // C++14 버전
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value &&
!std::is_integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n) // std::string 혹은 std::string으로 변환되는 인수를 위한 생성자
: name(std::forward<T>(n))
{ ... }
explicit Person(int idx) // 정수 인수를 위한 생성자
: name(std::nameFromIdx(idx))
{ ... }
...
private:
std::string name;
};
절충점들
완벽 전달이 더 효율적이라는 것은 하나의 규칙이다. 선언된 매개변수 형식을 만족하기 위해 임시 객체를 생성하는 비효율성이 없기 때문이다.
하지만 완벽 전달에도 단점이 있다. 그 중 하나는 완벽전달이 불가능한 인수들이 있다는 점이고, 클라이언트가 유효하지 않은 인수를 전달했을 때이다. 예를들어 char가 아닌 char16_t(16비트 문자를 표현하는 형식)들로 이루어진 문자열 리터럴을 전달해 Person 객체를 생성한다고 해보자
Person p(u"Konrad Zuse");
완벽전달에 기초한 접근방식에서는 char16_t 배열을 생성자에 매개변수에 묶고 생성자는 Person의 std::string 자료멤버에 전달하게 되며, 이때서야 형식의 불일치를 발견하게 된다.
Person의 경우 전달 함수의 보편 참조 매개변수가 std::string에 대한 초기치로 쓰일 것을 알고 있기 때문에 static_assert를 이용해 점검 할 수 있다. std::is_constructible이라는 형식 특질은 한 형식이 객체를 다른 한 형식의 객체로부터 생성할 수 있는지 컴파일 시점에서 판정한다.
위의 예제를 가지고 또다시 수정 해보자
class Person{ // C++14 버전
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value &&
!std::is_integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n) // std::string 혹은 std::string으로 변환되는 인수를 위한 생성자
: name(std::forward<T>(n))
{
static_assert(std::is_constructible<std::string, T>:: value,
"Parameter n can't be used to construct a std::string"
);
...
}
explicit Person(int idx) // 정수 인수를 위한 생성자
: name(std::nameFromIdx(idx))
{ ... }
...
private:
std::string name;
};
Conclusion
- 보편 참조와 중복적재의 조합에 대한 대안으로는 구별되는 함수 이름 사용, 매개변수를 const에 대한 왼값 참조로 전달, 매개변수를 값으로 전달, 꼬리표 배분 사용 등이 있다.
- std::enable_if를 이용해 템플릿의 인스턴스화를 제한함으로써 보편참조와 중복적재를 함께 사용할 수 있다. std::enable_if는 컴파일러가 보편 참조 중복적재를 사용하는 조건으로 프로그래머가 직접 제어하는 용도로 쓰인다.
- 보편 참조 매개변수는 효율성면에서는 장점인 경우가 많지만 사용성 면에서는 단점인 경우가 많다.
Reference
Effective Modern C++ 항목 27: 보편참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아두라
'C++ > Effective Modern C++' 카테고리의 다른 글
C++11에서 이동 의미론이 항상 도움이 될까 (0) | 2020.07.28 |
---|---|
참조축약을 숙지하라 (0) | 2020.07.27 |
보편참조에 대한 중복적재 대신 사용할수 있는 기법들 1 (0) | 2020.07.22 |
보편 참조에 대한 중복적재를 피하라 (0) | 2020.07.21 |
std::move, std::forward (0) | 2020.07.17 |
댓글