완벽 전달은 단순히 객체들을 전달하는 것만이 아니라 왼값 또는 오른값 여부, const, volatile 여부까지도 전달하는 것을 의미한다. 그리고 이를 위해서는 보편 참조 매개변수가 필요하다.

 

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)...);
}

이런 형태는 표준 컨테이너의 emplace류 함수들이나 std::make_shared, std::make_unique에서 볼 수 있다.

 

위의 예제처럼 함수 템플릿이 만들어진 상황에서

 

f(/*표현식*/);
fwd(/*표현식*/);

동일한 인수로 두 함수를 호출했을 때, 일어나는 일이 다르다면 완벽 전달은 실패한 것이다.

 

이번 항목에서 완벽 전달이 실패하는 인수의 종류를 알아보도록 하자.

 

 

중괄호 초기치

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

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

f를 직접 호출하는 경우 컴파일러가 호출 지점에서 전달된 인수들의 타입과 f에 선언된 매개변수의 타입을 비교하여 호환성을 파악하고 필요에 따라 적절한 암묵적 변환을 수행하여 호출을 성사시킨다.

 

그런데 전달 함수 템플릿 fwd를 통해 f가 간접적으로 호출된다면 fwd의 호출 지점에서 전달된 인수들과 f에 선언된 매개변수를 직접 비교할 수 없게 된다.

대신 fwd에 전달되는 인수들의 타입을 추론하고 그것들을 f의 매개변수의 타입과 비교한다.

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

 

◾ fwd이 매개변수들 중 하나 이상에 대해 컴파일러가 타입을 추론하지 못한다. 이 경우 코드는 컴파일되지 못한다.

◾ fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 타입을 잘못 추론한다. 이 경우 코드가 컴파일되지 못하거나 의도와는 다르게 행동한다.

 

이번에 언급된 상황은 auto 지역 변수를 선언하여 std::initializer_list 객체로 만든 뒤 fwd에 전달하면 문제를 우회할 수 있다.

 

 

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

0이나 NULL을 널 포인터로 템플릿에 넘겨주려고 하면 포인터 타입이 아니라 정수 타입으로 추론하기 때문에 문제가 생긴다. nullptr을 사용함으로써 간단하게 해결할 수 있다.

 

 

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

일반적으로 정수 static const 멤버 변수와 static constexpr 멤버 변수는 클래스 안에서 정의할 필요 없이 선언만 하면 된다. 컴파일러가 const 전파(constant propagation)를 적용해서 별도의 메모리를 따로 마련하지 않고 상수를 직접 사용하도록 하기 때문이다.

 

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

MinVals가 적용된 모든 곳에 상수값을 배치하게 된다.

그런데 MinVals의 주소를 참조하려는 코드를 작성하게 되면 MinVals는 메모리에 존재하지 않기 때문에 컴파일은 되지만 정의가 없어서 링크에러가 발생한다.

 

f(Widget::MinVals); // f(28)로 처리된다
fwd(Widget::MinVals); // 링크 에러 발생

이 값으로 f를 직접 호출하는 경우 f(28)로 처리되어 문제가 없지만 전달함수를 거쳐서 호출하려고 하면 링크 에러가 발생한다.

 

포인터와 참조는 본질적으로 같은 것이기 때문에 주소가 없는 MinVals를 전달 함수를 통해 참조로 전달하려고 하면 링크 에러가 발생하는 것이다.

 

근데 무조건 링크 에러가 발생하는 것은 아니고 컴파일러에 따라 다를 수도 있다.

이러한 문제를 확실히 회피하려면 정의를 제공하면 된다.

 

 

오버로딩된 함수 이름과 템플릿 이름

void f(int (*pf)(int)); // void f(int pf(int));

int processVal(int value);
int processVal(int value, int priority);

함수 포인터를 매개변수로 받는 함수와 오버로딩된 함수가 있다고 할 때,

 

f(processVal);

위의 코드는 문제가 없다. 컴파일러가 f의 선언을 보고 일치하는 함수를 골라서 f에 넘겨주게 된다.

 

fwd(processVal);

근데 전달 함수를 통해 참조 전달을 하게되면 문제가 생긴다.

f와 달리 fwd에는 필요한 타입에 대한 정보가 전혀 없기때문에 컴파일러가 어떤 함수를 선택해야 할 지 결정하지 못한다.

processVal 자체에는 타입이 없고 타입이 없으면 타입 추론도 없다. 그리고 타입 추론이 없다는 것이 바로 완벽 전달이 실패하는 또 다른 경우이다.

 

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

fwd(workOnVal); // 어떤 인스턴스인지?

함수 템플릿을 전달할때도 workOnVal의 어떤 인스턴스인지 알 수 없기 때문에 동일한 문제가 발생한다.

 

using ProcessFuncType = int(*)(int);
ProcessFuncType processValPtr = processVal;

fwd(processValPtr); // ok
fwd(static_cast<ProcessFuncType>(workOnVal)); // ok

모호함에서 발생하는 문제이기 때문에 오버로딩된 함수나 템플릿 인스턴스를 명시적으로 지정해서 넘겨주면 해결된다.

 

 

비트필드

마지막으로 비트필드가 함수 인수로 쓰일 때이다.

C++의 표준에 비const 참조는 절대로 비트필드에 묶이지 않아야 한다고 명확하게 지정되어있다.

임의의 비트들을 가리키는 포인터를 생성하는 방법이 존재하지 않기 때문이다.

 

다만 비트필드의 완벽 전달을 가능하게 하는 우회방법이 있다.

비트필드를 인수로 받는 함수는 해당 비트필드 값의 복사본을 받게 된다는 점을 알면 쉽게 이해할 수 있다.

값으로 전달하거나 const에 대한 참조로 전달하면 된다. 비트필드를 매개변수에 전달하는 단 두가지의 방법이다.

 

값으로 전달하는 것은 복사본을 받게된다는것이 확실하니 넘어가고 const 참조는 비트필드 자체에 묶이는 것이 아니라 비트필드 값이 복사된 보통 객체에 묶이게 되서 매개변수로 전달할 수 있게 된다.

 

f(h.totalLength); // 값에 의한 전달이 이루어져 복사가 됨

fwd(h.totalLength); // 에러

auto length = static_cast<std::uint16_t>(h.totalLength); // 복사
fwd(length); // 복사본 전달. ok

 

 

결론

대부분의 경우 완벽 전달은 그대로 작동한다. 완벽 전달 여부를 고민해야 하는 경우는 거의 없지만 이번 항목처럼 완벽 전달이 작동하지 않는 경우가 분명히 존재하기 때문에 알아두어야만 문제가 발생시 파악할 수 있다.

 

◾ 완벽 전달은 템플릿 타입 추론이 실패하거나 틀린 타입을 추론했을 때 실패한다.

◾ 인수가 중괄호 초기치이거나 0 또는 NULL로 표현된 널 포인터, 선언만 된 정수 static const 및 constexpr 멤버 변수, 템플릿 및 오버로딩된 함수 이름, 비트필드이면 완벽 전달이 실패한다.

+ Recent posts