티스토리 뷰
[Effective Modern C++] 항목 27. 보편 참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아 두라
pppgod 2019. 12. 8. 16:59항목 26에서 나온 문제에 대하여 해결 방법을 알아보자.
2019/12/01 - [C++/Effective Modern C++] - [Effective Modern C++] 항목 26. 보편 참조에 대한 중복적재를 피하라
중복적재를 포기한다
항목 26에서 사용한 logAndAdd 함수를 중복적재를 사용하다보니 문제가 발생하였다. 문제가 발생하는 함수에 대하여 함수 명을 logAnddAddNameIdx 와 같이 변경하면 문제를 해결할 수 있다. 그렇지만 생성자의 경우에는 함수명을 변경할 수 없기 때문에 해결할 수 없는 경우도 있다.
const T& 매개변수를 사용한다.
보편 참조 매개변수 대신에 const에 대한 왼값 참조 매개변수를 사용할 수 있다. 이는 항목 26에서 사용한 방법으로 효율성의 문제가 생길 수 있지만 예상치 않은 문제를 피할 수 있다.
값 전달 방식의 매개변수를 사용한다
참조 전달 매개변수 대신 값 전달 매개변수를 사용하는 것이다. 아래와 같이 사용할 수 있다.
class Person {
public:
explicit Person(std::string n)
: name(std::move(n)) {}
explicit Person(int idx)
: name(nameFromIdx(idx)) {}
...
private:
std::string name;
};
꼬리표 배분을 사용한다
const 왼값 참조 전달이나 값 전달은 완벽 전달을 지원하지 않는다. 완벽 전달을 위하여 보편 참조를 사용하려는 것이라면 보편 참조 외에는 대안이 존재하지 않는다. 이 문제를 해결하기 위해서는 꼬리표 배분이라는 방식을 사용할 수 있다. 먼저 문제 코드를 다시 보자.
std::multiset<std::string> names;
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 함수를 추가하여 실제 동작을 하도록 하는 방법이다.
template<typename T>
void logAndAdd(T&& name)
{
logAnddAddImpl(std::forward<T>(name),
std::is_integral<T>());
}
위 코드에서 사용한 std::is_integral 함수는 타입을 보고 정수 타입인지 확인하는 함수로 정수 타입의 경우에 true 를 리턴한다. 위 코드에서 문제는 int& 형태의 타입이 전달되면 이 타입이 정수 타입이라고 판단하지 않는다는 것이다. 이 문제를 없애기 위하여 아래와 같이 참조들을 제거해줘야 한다.
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>::type>()
);
}
이제 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));
}
파라미터가 하나 추가된 것 외에는 차이가 없다. 위 std::false_type 은 컴파일러가 적절한 함수를 찾기위해 사용하는 것으로 인자가 false 에 해당하는 경우에만 위 함수가 호출되게 된다. true 인 경우에는 다음과 같다.
std::string nameFromIdx(int idx);
void logAndAddImpl(int idx, std::true_type)
{
logAndAdd(nameFromIdx(idx));
}
true 인 경우에는 위 함수가 호출되게 되고 idx 에 해당하는 string 을 찾아 다시 logAndAdd 함수를 호출하도록 하였다. 위와 같은 방식을 꼬리표 배분이라고 부르며 탬플릿 메타프로그래밍의 표준적인 구축 요소이다.
보편 참조를 받는 템플릿을 제한한다.
std::enable_if 라는 것이 존재하는데 이는 컴파일러에게 특정 템플릿이 존재하지 않는 것처럼 행동하게 만들 수 있다.
class Person {
public:
template<typename T,
typename = typename std::enable_if<조건>::type>
explicit Person(T&& n);
...
};
위 코드에서 조건에 들어가야하는 것은 Person 타입이 아닌 경우에만 인스턴스를 생성하도록 만들어야하는 것이다. 여기서 먼저 알아야할 것이 있다. 바로 참조 여부와 const, volatile 여부이다. 모두 같은 Person 타입이지만 이 같이 특별한 경우에는 같은 타입이라고 인지하지 못한다. 이와 같은 수식어들을 없애는 방법은 바로 std::ecay 함수를 이용하는 것이다. 아래와 같이 상요할 수 있다.
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);
...
};
여기서 끝난것 같지만 그러허지 않다. 항목 26에서 이야기한 파생 클래스에서의 문제들에 대해서 아직 해결하지 못한다. 파생 클래스의 경우에도 인스턴스를 생성하지 못하도록 해야한다. 우리는 운이 좋게도 표준 라이브러리에서 이 함수를 찾을 수 있다. std::is_base_of 는 파생 형식인 경우에 true 를 리턴하도록 되어있다. 이제 간단하다 위 코드에서 is_same 대신에 is_base_of 를 사용하기만 하면 된다.
class Person {
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);
...
};
이제 거의 완벽하다. 정수 타입의 문제만 해결하면 된다. 앞서 사용한 is_integral 함수를 사용하면 쉽게 해결이 가능하다.
class Person {
public:
template<
typename T,
typename = std::enable_if<
!std::is_base_of<Person, std::decay_t<T>>::value
&&
!std::is_Integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n)
: name(std:;forward<T>(n))
{ ... }
...
private:
std::string name;
};
절충점들
지금까지 완벽전달의 장점들을 활요하기 위해 여러가지 해결 방법을 알아보았다. 그러나 완벽 전달에도 단점들이 있다. 하나는 완벽 전달이 불가능한 인수들이 존재한다는 것이고, 또 하나는 오류 메시지의 문제이다. 예를 들어 char 형이 아닌 char16_t 를 사용한다고 하자. 이를 Person 객체에 적용하면 난해한 오류 메시지들이 출력된다. 에러가 발생하는 이유는 사실 간단하다. string 형태로 변경할 수 없어 발생하는 문제이다. static_assert 를 이용하여 오류 메시지를 쉽게 확인할 수 있다.
class Person {
public:
template<
typename T,
typename = std::enable_if<
!std::is_base_of<Person, std::decay_t<T>>::value
&&
!std::is_Integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n)
: 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"
);
}
...
private:
std::string name;
};
참고 서적
스콧 마이어스, Effective Modern C++
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 항목 29. 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정하라 (0) | 2019.12.14 |
---|---|
[Effective Modern C++] 항목 28. 참조 축약을 숙지하라 (0) | 2019.12.08 |
[Effective Modern C++] 항목 26. 보편 참조에 대한 중복적재를 피하라 (0) | 2019.12.01 |
[Effective Modern C++] 항목 25. 오른값 참조에는 std::move를, 보편 참조에는 std::forward를 사용하라 (0) | 2019.11.30 |
[Effective Modern C++] 항목 24. 보편 참조와 오른값 참조를 구별하라 (2) | 2019.11.23 |
- Total
- Today
- Yesterday
- MOVE
- thread
- CPP
- forward
- async
- C++
- Forwarding
- 보편참조
- detach
- C++14
- Effective
- std::move
- Override
- const
- std::forward
- 다이소
- auto
- C++11
- Future
- 람다
- Perfect
- Join
- 발아시기
- Effective Modern C++
- 포인터
- Unreal
- Modern
- Overloading
- 보편 참조
- C
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |