KoreanFoodie's Study

Effective Modern C++ | 항목 30 : 완벽 전달이 실패하는 경우들을 잘 알아두라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 30 : 완벽 전달이 실패하는 경우들을 잘 알아두라

GoldGiver 2022. 10. 26. 10:04

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 30 : 완벽 전달이 실패하는 경우들을 잘 알아두라

핵심 :

1. 완벽 전달은 템플릿 형식 연역이 실패하거나 틀린 형식을 연역했을 때 실패한다.
2. 인수가 중괄호 초기치이거나 0 또는 NULL 로 표현된 널 포인터, 선언만 된 정수 static const 및 constexpr 자료 멤버, 템플릿 및 중복적재된 함수 이름, 비트필드이면 완벽 전달이 실패한다.


완벽 전달은 단순히 객체들을 전달하는 것만이 아니라, 그 객체들의 주요 특징, 즉 형식, 왼값/오른값 여부, const 나 volatile 여부까지도 전달해야 한다. 예시를 보자.

// 임의의 인수들을 받아 f 에 전달하는 전달 함수
template<typename... Ts>
T&& fwd(Ts&&... params)
{
  return f(std::forward<Ts>(param)...);
}

특히 표준 컨테이너의 emplace 류 함수들과 똑똑한 포인터 팩터리 함수 std::make_unique, std::make_shared 에서 이런 형태를 볼 수 있다.
대상 함수 f 와 전달 함수 fwd 가 있을 때, 만일 어떤 인수로 f 를 호출했을 때 일어나는 일과 같은 인수로 fwd 를 호출했을 때 일어나는 일이 다르면, fwd 는 표현식을 f 에 완벽하게 전달하지 못한 것이다. 이제 그 종류들을 살펴보도록 하자.

중괄호 초기치

void f(const std::vector<int>& v);

// 암묵적으로 std::vector<int> 로 변환
f({1, 2, 3});

// 오류! 컴파일되지 않음
fwd({1, 2, 3});

다음 주 조건 중 하나라도 만족되면 완벽 전달이 실패한다.

  • fwd 의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 연역하지 못한다. 이 경우 코드는 컴파일되지 않는다.
  • fwd 의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 잘못 연역한다. 잘못 연역할 경우 컴팡리이 되지 않거나, 이상하게 동작한다.

fwd 의 매개변수가 std::initializer_list 가 될 수 없는 형태로 선언되어 있어, fwd 의 경우 형식 연역이 제대로 이루어지지 않았던 것이다. 다음과 같이 우회책을 사용해 볼 수는 있다.

// auto 는 std::initializer_list<int> 로 연역
auto il = {1, 2, 3};

// OK; il 이 f 로 완벽 전달된다
fwd(il);

 

널 포인터를 뜻하는 0 또는 NULL

0 이나 NULL 을 널 포인터로서 템플릿에 넘겨주면, 컴파일러가 이를 정수 형식(보통은 int) 로 연역하므로 문제가 생긴다. 자세한 내용은 항목 8을 참고하자.

선언만 된 정수 static const 및 constexpr 자료 멤버

class Widget {
public:
  // MinVals 선언
  static constexpr std::size_t MinVals = 28;
  ...
};

std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals);

위의 예시에서, MinVals 의 정의가 없어도 코드는 잘 작동하지만, 만일 어떤 코드가 MinVals 의 주소를 취한다면(포인터 등) MinVals 를 가리킬 저장소가 필요해진다. 그럼 이 코드는 컴파일은 되지만 MinVals 의 정의가 없어서 링크에 실패한다.

// OK; 그냥 f(28) 로 처리됨
f(Widget::MinVals);

// 오류! 링크에 실패
fwd(Widget::MinVals);

보편 참조의 경우, 참조는 포인터로 취급되는 것이 보통이므로 fwd 는 링크에 실패한다. 아래와 같이 정의를 해 주면 코드가 정상 작동할 것이다(물론 컴파일러에 따라, 선언만으로 충분하다고 여길 수도 있지만, 이식성을 기대해서는 안된다).

// Widget.cpp 파일에서
constexpr std::size_t Widget::MinVals;

 

중복적재된 함수 이름과 템플릿 이름

함수 f 의 행동을 커스텀화하기 위해, f 가 하나의 함수를 받아서 그 함수를 호출한다고 하고, 그 함수가 int 를 받고 int 를 돌려준다고 하자. 그런 f 는 다음과 같이 표현해 볼 수 있다.

// 포인터 버전
void f(int (*pf)(int));

// 비포인터 버전
void f(int pf(int));

// pf 예시
int processVal(int value);
int processVal(int value, int priority);

// f 에 적용
f(processVal);

// 오류! 어떤 processVal 인지?
fwd(processVal);

일반 함수의 경우, 매개변수 형식을 보고 어떤 함수를 오버로딩할 지 선택할 수 있지만, fwd 는 그러한 호출에 관한 정보가 전혀 없어 컴파일이 실패한다. processVal 자체에는 형식이 없으며, 형식이 없으면 형식 연역도 없기 때문이다.

함수 템플릿도 마찬가지의 문제가 발생한다.

template<typename T>
T workOnVal(T param)
{ ... }

// 오류! workOnVal 의 어떤 인스턴스인지?
fwd(workOnVal);


우회책은, f 의 매개변수와 같은 형식의 함수 포인터를 만들어서 processVal 이나 workOnVal 로 초기화하고, 그 포인터를 fwd 에 넘겨주면 된다. 캐스팅도 가능하다.

using ProcessFuncType =
  int (*)(int);

ProcessFuncType processValPtr = processVal;

fwd(processValPtr);

fwd(static_cast<ProcessFuncType>(workOnVal));

 

비트필드

완벽 전달이 실패하는 마지막 경우는 비트필드(bitfield) 가 함수 인수로 쓰일 때이다.

// IPv4 헤더 구조체의 일부
struct {
  std::uint32_t version:4,
    IHL:4,
    DCSP:6,
    ECN:2, 
    totalLength:16;
  ...
};

// 호출할 함수
void f(std::size_t sz);

IPv4Header h;

// OK
f(h.totalLength);

// 오류!
fwd(h.totalLength);

C++ 표준은 둘의 조합에 대해 "비const 참조는 절대로 비트필드에 묶이지 않아야 한다(shall not)" 라고 선고한다. 일부 비트를 직접적으로 지칭하는 방법은 없다. 임의의 비트들을 가리키는 포인터를 생성하는 방법은 없으며(C++ 에서 직접 가리킬 수 있는 가장 작은 것은 char이다), 따라서 참조를 임의의 비트들에 묶는 방법도 없다.
우회책은 비트필드 값을 복사하여 사용하는 것이다.

auto length = static_cast<std::uint16_t>(h.totalLength);

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


완벽 전달은 대부분의 경우 잘 동작한다. 다만 위에서 언급된 완벽 전달이 실패하는 경우를 고려하여 우회책을 적절히 사용하는 것도 중요하다!

Comments