본문 바로가기
C++/Effective Modern C++

완벽 전달이 실패하는 경우들 1

by COCO1337 2020. 7. 29.

우선 완벽 전달에 대해 알아보도록 하자

전달(forwarding)이란 말 그대로 한 함수가 자신의 인수들을 다른 함수에 넘겨주는(전달하는) 것을 뜻한다. 이때 목표는 전달받는 함수가 애초에 전달하는 함수가 받았던 것과 동일한 객체들을 받게 하는 것이다. 하지만 값 전달 방식의 매개변수로는 불가능하다. 매개변수는 원래 호출자가 넘겨준 인수의 복사본이기 때문이다. 그래서 범용적인 전달을 위해 참조 매개변수들을 사용해야 한다.

완벽 전달은 단순히 객체들을 전달하는 것 뿐만 아니라 그 객체의 특징과 형식(왼값/오른값 여부, const/volatile 여부)도 전달하는 것을 말한다.

f라는 함수에 인수를 전달하는 함수를 만들어 보자. 전달 함수는 임의의 형식과 개수의 인수들을 받는 템플릿 이어야 하기 때문에 가변 인수를 받을 수 있도록 만든다.

template<typename... Ts>
void fwd(Ts&&... params)		// 임의의 인수를 받고
{
  f(std::forward<Ts>(params)...);	// f에 전달
}

만일 이런 대상 함수 f와 전달 함수 fwd가 있다고 할때 어떤 인수로 f를 호출했을 때 일어나는 일과 같은 인수로 fwd를 호출했을 때 일어나는 일이 다르다면 완벽 전달은 실패한 것이다.

그러면 완벽하게 전달할 수 없는 인수들의 각 종류들을 살펴보자


중괄호 초기치

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

f({1, 2, 3});	// "{1, 2, 3}"은 암묵적으로 std::vector<int>로 변환된다.
fwd({1, 2, 3});	// 컴파일 되지 않음

f는 컴파일러가 f에 선언된 매개변수들이 형식을 비교해서 호환 여부를 파악한 뒤, 필요하다면 적절한 암묵적 변환을 수행해 호출을 성사시킨다. 위의 예에서는 {1, 2, 3}으로부터 임시 std::vector<int>객체를 생성해 f의 매개변수 v에 묶는다.

전달 함수 템플릿 fwd를 통해 f를 간접 호출할 때에는 fwd 호출 지점에서 전달된 인수들과 f에 선언된 매개변수를 직접 지교할 수 없다. 대신 컴파일러는 fwd에 전달되는 인수 형식을 연역하고 그 형식들을 f의 매개변수 선언들과 비교한다. 이 때 두 조건을 하나라도 만족시키면 완벽 전달에 실패하게 된다.

첫째로, fwd 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 연역하지 못한다.

둘째로, fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 잘못 연역한다. 잘못 연역한 형식으로 fwd의 인스턴스를 컴파일 할 수 없거나 fwd의 연역된 형식들을 이용해서 호출한 f가 fwd에 전달된 인수들로 f를 직접 호출했을 때와 다르게 행동하는 것이다.

앞에 나온 fwd({1, 2, 3}) 호출에서 문제는 std::initializer_list가 될 수 없는 형태로 선언된 함수 템플릿 매개변수에 중괄호 초기치를 넘겨준 것이다. 표준에서는 비연역 문맥(non-deduced context)라고 부른다.

하지만 auto를 사용하면 형식이 잘 연역된다.

auto il = {1, 2, 3};
fwd(il);	// 잘 연역됨

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

0이나 NULL을 널포인터로서 템플릿에 넘겨주려 하면 컴파일러는 정수 형식으로 연역하기 때문에 문제가 생기는데 nullptr을 사용하면 해결할 수 있다.


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

일반적으로 정수 static const 자료 멤버와 정수 static constexpr 자료 멤버는 클래스 안에서 정의할 필요 없이 선언만 하면 된다. 이는 그런 멤버의 값에 대한 컴파일러가 const propagation을 적용해서 그런 멤버의 값을 위한 메모리를 따로 마련할 필요가 없어지기 때문이다.

class Widget{
public:
  static constexpr std::size_t MinVals = 28;
  ...
};		// MinVals 정의 없음
...

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

Widget::MinVals에 대한 정의가 없어도 컴파일러는 Widget::MinVals가 언급된 모든 곳에 28이라는 값을 배치함으로써 누락된 정의를 처리한다.

void f(std::size_t val);
f(Widget::MinVals);		// f(28)로 처리됨
fwd(Widget::MinVals);	// 오류, 링크에 실패 (정의 되어 있지 않아 저장소를 따로 두지 않았기 때문에 주소가 없음)

fwd의 매개변수는 보편 참조이며, 컴파일러가 산출한 코드에서 참조는 포인터처럼 취급되기 때문에 정의되지 않은 MinVals는 참조 하려하기 때문에 완벽 전달이 실패한다.

이 문제는 해당 정수 static const 또는 constexpr 자료 멤버의 정의를 제공하면 해결 할 수 있다.

constexpr std::size_t Widget::MinVals;	// 초기치를 지정하지 않음

Reference

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

반응형

댓글