일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 반복자
- exception
- UE4
- 보편 참조
- 언리얼
- effective modern c++
- effective stl
- 티스토리챌린지
- Smart Pointer
- more effective c++
- reference
- virtual function
- 참조자
- 암시적 변환
- 오블완
- c++
- 영화
- std::async
- universal reference
- 상속
- implicit conversion
- lua
- 영화 리뷰
- Effective c++
- operator new
- 스마트 포인터
- 예외
- resource management class
- iterator
- 게임
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 30. 완벽 전달(Perfect Forwarding)이 실패하는 경우 본문
Effective C++/Effective Modern C++
[Effective Modern C++] 30. 완벽 전달(Perfect Forwarding)이 실패하는 경우
김디트 2025. 3. 25. 11:32728x90
항목 30. 완벽 전달이 실패하는 경우들을 잘 알아두라
C++11의 완벽 전달(perfect forwarding)
- 이름처럼 완벽하지는 않다.
- 전달(forwarding)
- 한 함수가 자신의 인수들을 다른 함수에 전달하는 것을 의미한다.
- 완벽전달이란?
- 첫 함수(전달하는 함수)가 받은 것과 동일한 객체를 둘째 함수(전달받는 함수)가 받도록 하는 것.
- 복사본이 전달된다면 '완벽'하지 않다.
- 포인터 매개변수도 호출자에게 포인터를 넘겨주도록 강제하므로 탈락이다.
- 다시 말해 완벽 전달(perfect forwarding)은 단순히 객체만 전달하는 것이 아니라
- 특징(타입, lValue / rValue 여부, const, volatile 등)을 모두 전달하는 것을 의미한다.
- 보편 참조 매개변수가 필요할 것이다.(항목 24 참조)
template<typename T>
void fwd(T&& param)
{
f(std::forward<T>(param)); // f에 전달
}
// 제네릭함을 좀 더 연장하면 인수가 가변적이어야 할 것이다.
template<typename... Ts>
void fwd(Ts&&... params)
{
f(std::forward<Ts>(params)); // f에 전달
}
// 이 경우, 아래 두 코드가 하는 일이 다르다면 완벽 전달은 실패이다.
f( 표현식 );
fwd( 표현식 );
- 이런 실패가 발생하는 인수들이 존재하며, 아래에서 알아보도록 한다.
중괄호 초기치
void f(const std::vector<int>& v);
f({1, 2, 3}); // 컴파일 성공!
fwd({1, 2, 3}); // 컴파일 실패!
- 중괄호 초기치는 완벽 전달이 실패한다.
- f를 호출할 때
- 매개변수들의 타입들을 비교하여 호환 여부를 파악하고,
- 필요하다면 적절한 암묵적 변환을 수행한다.
- fwd를 통해서 f를 간접적으로 호출할 때
- 컴파일러가 f로 전달된 인수들과 f에 선언된 매개변수를 직접 비교할 수 없다.
- fwd에 전달되는 인수들의 타입을 추론하여 그 추론 타입과 선언된 매개변수를 비교한다.
- 이 때, 다음 두 조건 중 하나라도 만족하면 완벽 전달 실패.
- fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 타입 추론을 하지 못하는 경우.
- 컴파일 실패
- fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 타입 추론에 오류를 범한다.
- 잘못 추론한 타입에서 나올 수 있는 두 가지 결과
- 그 타입으로는 fwd의 인스턴스를 컴파일할 수 없다.
- 컴파일 된다 해도 fwd의 추론 타입을 이용해 호출한 f가 독립적으로 호출되었을 때와는 다르게 행동한다.
- 잘못 추론한 타입에서 나올 수 있는 두 가지 결과
- fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 타입 추론을 하지 못하는 경우.
- 위의 경우 타입 추론을 하지 못한 경우이다.
- 템플릿에서 중괄호 초기화는 비추론 문맥(non-deduced context)에 속한다.
- 하지만 auto 변수는 중괄호 초기치로 초기화해도 타입 추론이 잘 이루어진다.(항목 2 참조)
- 아래와 같이 처리하면 우회할 수 있다.
auto il = { 1, 2, 3 }; // std::initializer_list<int>로 타입 추론
fwd(il); // 컴파일 성공
널 포인터를 뜻하는 0 또는 NULL
- 0이나 NULL을 널 포인터로서 템플릿에 넘겨주려 하면?
- 컴파일러는 그걸 포인터가 아니라 정수 타입(보통은 int)으로 추론해버린다.
- 결국 완벽 전달할 수 없다.
- nullptr을 사용하자.(항목 8 참조)
선언만 된 정수 static const 및 constexpr 자료 멤버
- 일반적으로 static const나 static constexpr 멤버는 선언만 하면 된다.
- 컴파일러가 const 전파(const propagation)을 적용하여, 그 멤버의 메모리를 따로 마련할 필요가 없기 때문이다.
class Widget
{
public:
static constexpr std::size_t MinVals = 28; // MinVals의 선언
...
};
... // MinVals의 정의는 없다.
std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals); // 사용 가능
// 컴파일러는 MinVals가 사용된 모든 곳을 28로 치환해버리므로
// 메모리 할당이 필요 없다.
- 만일 MinVals의 주소를 취하면?
- 이 코드는 컴파일은 되지만 정의가 없어서 링크에 실패한다.
- 그렇다면 아래의 경우는 어떻게 될까?
void f(std::size_t val);
f(Widget::MinVals); // 성공. 그냥 f(28)로 처리된다.
fwd(Widget::MinVals); // 링크 실패! 보편 참조로 전달되기 때문이다.
- 컴파일러가 산출한 코드에서 참조는 포인터처럼 취급되기 때문이다.
- 표준에 따르면, static const를 참조로 전달하려면 정의해야 한다.
- 하지만 모든 구현이 이를 강제하지는 않는다.
- 따라서 컴파일러 특성에 따라 가능할 수도 있다.
- 하지만 이식성을 생각해야 할 것이다....
- 해결 방법은 그냥 정의를 제공하면 된다.
constexpr std::size_t Widget::MinVals; // cpp 파일에서 정의 제공
오버로드된 함수 이름과 템플릿 이름
- f가 함수 포인터를 받도록 선언해보자.
void f(int (*pf)(int));
- 더 간단한 비 포인터 구문으로 선언할 수 있으며 아래 코드는 위와 동일하다.
void f(int pf(int));
- 그리고 다음처럼 오버로드된 processVal 함수가 있다고 하자.
int processVal(int value);
int processVal(int value, int priority);
f(processVal); // 컴파일 성공
// 이름만 전달해도 컴파일러는 알아서 int를 받는 processVal을 선택해서 그 함수의 주소를 넘긴다.
fwd(processVal); // 컴파일 실패!
// 어떤 processVal을 선택할지에 대한 타입 정보가 전혀 없으므로 선택 불가.
- 오버로드 뿐 아니라 함수 템플릿을 전달하려 할때도 동일한 문제가 발생한다.
- 함수 템플릿은 하나의 함수가 아니라 다수의 함수를 대표한다.
- 즉 타입이 확정적이어야 한다는 뜻이다.
tmeplate<typename T>
T workOnVal(T param) { ... }
fwd(workOnVal); // 컴파일 에러!
- 해결법은 전달하고자 하는 함수나 템플릿을 명시적으로 지정하면 된다.
- 전달할 함수 포인터를 미리 만들어서 전달한다.
using ProcessFuncType = int (*)(int); // 항목 9 참조
ProcessFuncType = processValPtr = processVal; // 함수 포인터 타입 명시
fwd(processValPtr); // 컴파일 성공
fwd(static_cast<ProcessFuncType>(workOnVal)); // 역시 컴파일 성공
비트필드(bitfield)
- 다음은 IPv4 헤더를 나타내는 구조체이다.
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); // 컴파일 성공
fwd(h.totalLength); // 컴파일 실패!!
- 참조 타입에(fwd의 매개변수) 비 const 비트필드를 넘긴 것이 문제이다.
- C++ 표준에 따르면 비const 참조는 절대로 비트필드에 묶이지 않아야 한다.
- 이유?
- 비트필드들은 컴퓨터 워드의 임의의 일부분으로 구성될 수 있는데,
- 그 일부 비트를 직접적으로 지칭하는 방법이 없다.
- 즉, 그 임의의 비트들을 가리키는 포인터를 생성할 방법이 없다.
- 해결 방법
- 비트필드를 인수로 받는 임의의 함수는 그 비트필드의 값의 복사본을 받게 된다는 점을 이용한다.
- 즉, 어차피 전달받는 쪽은 항상 비트필드 값의 복사본을 받는다.
- 두 가지 방법
- 값으로 전달한다.
- 즉, 복사본을 전달한다.
- const 참조로 전달한다.
- 이 경우 비트필드 값이 복사된 보통 객체를 참조하게 된다.
- 값으로 전달한다.
- 비트필드를 인수로 받는 임의의 함수는 그 비트필드의 값의 복사본을 받게 된다는 점을 이용한다.
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length); // 복사본 전달
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 32. 람다 초기화 캡쳐(init capture) (0) | 2025.04.03 |
---|---|
[Effective Modern C++] 31. 람다 기본 캡쳐 모드 지양하기 (0) | 2025.04.01 |
[Effective Modern C++] 29. 이동 연산이 없다고 가정하기 (0) | 2025.03.24 |
[Effective Modern C++] 28. 참조 축약(reference collapsing) (0) | 2025.03.21 |
[Effective Modern C++] 27. 보편 참조 오버로드를 피하는 기법 (0) | 2025.03.18 |
Comments