중복적재된 함수 이름과 템플릿 이름
1편에서의 함수 f와 마찬가지로 fwd를 거쳐 인수를 전달하려는 대상 함수의 행동 방식을 커스텀 하기위해 f가 하나의 함수를 받아서 그 함수를 호출한다고 하자. 그함수가 int를 받고 int를 돌려준다고 하자
void f(int (*pf)(int)); // pf는 processing function
void f(int pf(int)); // 더 간단한 비 포인터 구문
여기에 중복 적재된 processVal 함수가 있다고 하자
int processVal(int value);
int processVal(int value, int priority);
f(processVal); // 가능하다
f는 함수 포인터를 기대하지만 processVal은 함수 포인터가 아니다. 함수또한 아니다. 단지 두 함수가 공유하는 이름일 뿐이다. 하지만 컴파일러는 두 함수중 f의 매개변수 형식과 일치하는 버전을 받아 넘겨준다. 즉, 컴파일러는 int를 받는 processVal을 선택한다. 컴파일러는 processVal의 버전 중 f의 호출에 필요한것이 무엇인지 컴파일러가 f의 선언을 보고 알아맞출수 있기 때문에 가능한 일이다. 하지만 함수 템플릿인 fwd에는 그런 호출에 필요한 형식에 관한 정보가 없기 때문에 어떤 중복적재를 선택해야 할지 알 수 없다.
fwd(processVal); // 오류
processVal 자체는 형식도 없고, 형식이 없기 때문에 형식 연역도 없다. 그렇기 때문에 완벽 전달이 실패하게 된다. 이 문제는 함수 뿐만이 아닌 함수 템플릿을 사용했을때도 문제가 된다.
완벽 전달 함수가 중복적재된 함수 이름이나 템플릿 이름을 받아들이게 하려면 전달하고자 하는 중복적재나 템플릿 인스턴스를 명시적으로 지정하면 된다.
// f의 매개변수와 같은 형식의 함수 포인터를 만들어 초기화 하여 그 포인터를 넘겨준다
using ProcessFuncType = int(*)(int); // typedef들을 만든다
ProcessFuncType processValPtr = processVal; // processVal에 필요한 서명을 명시한다
fwd(processValPtr);
fwd(static_cast<ProcessFuncType>(workOnVal));
물론 이렇게 하기 위해서는 FWD가 전달하는 함수 포인터의 형식을 알고 있어야 한다.
비트 필드
완벽전달이 실패하는 마지막 경우는 비트필드가 함수 인수로 쓰일 때이다.
여기서는 이전에 사용하던 f와 IPv4 헤더를 보자. f는 이번에 std::size_t 형식의 매개변수를 받는다.
struct IPv4Header{
std::uint32_t version:4,
IHL:4,
DSCP:6,
ECN:2,
totalLength:16,
...
};
void f(std::size_t sz);
IPv4Header h;
...
f(h.totalLength); // OK
fwd(h.totalLength); // 오류
문제는 fwd는 매개변수가 참조이고 h.totalLength는 비const 비트 필드라는 점이다. C++ 표준에서는 비const 참조는 절대 비트필드에 묶이지 않아야 한다. 라고 명확하게 선고한다. 하드웨어 수준에서 포인터와 참조는 같다. 하지만 임의의 비트를 가리키는 포인터를 생성하는 방법은 없으며, 따라서 참조를 임의의 비트들에 묶는 방법도 없다.
다만 비트필드를 인수로 받는 임의의 함수는 그 비트필드의 값의 복사본을 받게 되는데 이 점을 이용하면 된다. 어차피 참조를 비트필드에 묶을수 있는 함수는 없으며, 비트필드를 가리키는 포인터를 받을수 있는 함수도 없다. 비트필드를 매개변수에 전달하는 방법은 두가지로 첫째는 값으로 전달하는 것이고 다른 하나는 const에 대한 참조로 전달하는 것이다. 첫번째 경우 호출된 함수가 비트필드 값의 복사본을 받는것은 명확하다. 두번째 경우 표준에 따르면 참조는 실제로 어떤 표준 정수 형식의 객체에 저장된 비트필드값의 복사본에 묶여야 한다. 즉, const 참조는 비트필드 자체에 묶이는 것이 아니라 비트필드 값이 복사된 보통 객체에 묶인다.
비트필드를 완벽 전달 함수에 넘겨주는 기법의 핵심은 전달 대상 함수가 항상 복사본을 받게 된다는 점이다. 즉, 복사본을 직접 생성해서 복사본으로 전달함수를 호출하면 된다.
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length);
Conclusion
- 완벽 전달은 템플릿 형식 연역이 실패하거나 틀린 형식을 연역했을 때 실패한다.
- 인수가 중괄호 초기치거나 0 또는 NULL로 표현된 널포인터, 선언된 정수 static const 및 constexpr 자료 멤버, 템플릿 및 중복적재된 함수 이름, 비트필드이면 완벽 전달이 실패한다
Reference
Effective Modern C++ 항목 30: 완벽 전달이 실패하느 경우들을 잘 알아두라
'C++ > Effective Modern C++' 카테고리의 다른 글
객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라 (0) | 2020.08.06 |
---|---|
람다표현식에서 기본 갈무리 모드는 피하자 (0) | 2020.08.03 |
완벽 전달이 실패하는 경우들 1 (0) | 2020.07.29 |
C++11에서 이동 의미론이 항상 도움이 될까 (0) | 2020.07.28 |
참조축약을 숙지하라 (0) | 2020.07.27 |
댓글