스토리텔링 개발자

[Effective Modern C++] 30. 완벽 전달(Perfect Forwarding)이 실패하는 경우 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 30. 완벽 전달(Perfect Forwarding)이 실패하는 경우

김디트 2025. 3. 25. 11:32
728x90

항목 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에 전달되는 인수들의 타입을 추론하여 그 추론 타입과 선언된 매개변수를 비교한다.
    • 이 때, 다음 두 조건 중 하나라도 만족하면 완벽 전달 실패.
      1. fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 타입 추론을 하지 못하는 경우.
        • 컴파일 실패
      2. fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 타입 추론에 오류를 범한다.
        • 잘못 추론한 타입에서 나올 수 있는 두 가지 결과
          • 그 타입으로는 fwd의 인스턴스를 컴파일할 수 없다.
          • 컴파일 된다 해도 fwd의 추론 타입을 이용해 호출한 f가 독립적으로 호출되었을 때와는 다르게 행동한다.
  • 위의 경우 타입 추론을 하지 못한 경우이다.
    • 템플릿에서 중괄호 초기화는 비추론 문맥(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 참조는 절대로 비트필드에 묶이지 않아야 한다.
  • 이유?
    • 비트필드들은 컴퓨터 워드의 임의의 일부분으로 구성될 수 있는데,
    • 그 일부 비트를 직접적으로 지칭하는 방법이 없다.
    • 즉, 그 임의의 비트들을 가리키는 포인터를 생성할 방법이 없다.
  • 해결 방법
    • 비트필드를 인수로 받는 임의의 함수는 그 비트필드의 값의 복사본을 받게 된다는 점을 이용한다.
      • 즉, 어차피 전달받는 쪽은 항상 비트필드 값의 복사본을 받는다.
    • 두 가지 방법
      1. 값으로 전달한다.
        • 즉, 복사본을 전달한다.
      2. const 참조로 전달한다.
        • 이 경우 비트필드 값이 복사된 보통 객체를 참조하게 된다.
auto length = static_cast<std::uint16_t>(h.totalLength);

fwd(length); // 복사본 전달

 

728x90
Comments