본문 바로가기
C++

[Idiom] 보편참조와 퍼펙트포워딩

by PainDiver 2025. 2. 12.

 

회사에 들어가기 전에도 알고있던 개념이었지만 이 중요한걸 설명을 안해놓다니 ㅎㅎ..

보편참조,퍼펙트포워딩,얕은복사,깊은복사 얘내들은 내가 생각하기에 하나의 세트로 묶어서 이해해야한다.

 

2022년에 얕은복사랑 깊은복사 설명 조금 해둔게 있긴하지만, 거기서 빠진 설명이 하나 있다.

얕은복사건 깊은복사건, 프로그래머가 직접 구현해줘야한다.

이동생성자에서 깊은복사해놓고 패러미터로 move썼다고 얕은복사가 되는게 아니다.

 

어떤 특정클래스에서 얕은복사와 깊은복사가 만약 구현이 된다면, 이제 이 클래스는 생성자의 패러미터가 좌측값인지 우측값인지 민감하게 반응을 하게 된다.

 


보편참조

위 내용을 기억한 채로 보편참조를 알아보자.

 

보편참조는 템플릿에서 사용되는 개념인데, 패러미터에 앰퍼선드 두개를 붙히면 좌,우측값 상관없이 모두 다 받을 수 있다.

그래서 템플릿함수는 저거 하나로 땡이다. 이 개념을 보편참조라고 한다. 별거없다 

 

원리는 레퍼런스 붕괴법칙이라는 템플릿만의 룰이 있어서 그렇다.

T&&로 받는데 A&&이 들어왔다 쳐보자, 그럼 A&& &&인데 이건 우측값으로 한다. -> A&&

T&&로 받는데 A&이 들어올 경우, A&& &인데 이건 좌측값으로 한다. -> A&

template<typename T>
void Foo(T&& value)
{
	// 
}

 

그런데 저기 들어올게 우측값인지 좌측값인지 모르는데 어떻게 하나의 함수본문으로 다 민감한 로직을 처리하겠나?

내부에서 그냥 그 변수를 쓰고 버릴거라면 별로 상관없긴하다.

 

근데 만약 저 함수안에서 좌우측값이 민감한 함수를 써야한다면???

class A
{
};

template<typename T>
void Foo(T&& value)
{
    Test(value);
}

void Test(const A& a)
{
    // 좌측값출력!
}

void Test(A&& a)
{
    // 우측값출력!
}

int main()
{
    A a;
    Foo<A>(move(a)); // 우측값을 넣었는데..

    // 좌측값출력
    
    return 0;
}

 

굉장히 억까당한 기분이다...

아니 우측값넣었는데 당연히 우측값이 출력되는게 아니야?

 

그렇다 아니다.

 

함수로 전달되면서 이 우측값이라는 녀석이 함수내부에서 이름을 가져버렸다.

당연하듯 이 자식은 좌측값이 되버렸고 엉뚱한결과가 나와버린 것이다.

 

어? 그냥 그럼 함수안에서 넘길때 move로하면안될까?

그렇게 되면 반대로 좌측값넣을때 우측값으로 바뀐다.

 

퍼펙트 포워딩

이 때 쓰이는게 포워드시맨틱이다.

class A
{
};

template<typename T>
void Foo(T&& value)
{
    Test(forward<T>(value));
}

void Test(const A& a)
{
    // 좌측값출력!
}

void Test(A&& a)
{
    // 우측값출력!
}

int main()
{
    A a;
    Foo<A>(move(a)); // 우측값을 넣었는데..

    // 좌측값출력
    
    return 0;
}

 

포워드 시맨틱이라는 녀석은 컴파일타임에 이미 들어올녀석의 타입을 알고있다.

예를들어 밖에서 move(a)로 들어오면 A&&인걸 미리 추론하고있다.

그럼 코드 자체가 A&&를 그대로 우측값으로 넘겨주는 방식으로 만들어진다.

반대로 A&면? 좌측값으로 넘겨준다.

 

이 비밀은 forward함수의 내부에 숨어져있다.

template <class S>
S&& forward(typename std::remove_reference<S>::type& a) noexcept 
{
  return static_cast<S&&>(a);
}

 

 

만약 Foo함수에 T&&가 들어온다 쳐보자

T&&&& forward<typename T&& a> noexcept
{
	return static_cast<T&&&&>(a);
}

이렇게 바뀌고 아까말한 레퍼런스 붕괴법칙에 의해서

T&& forward<typename T&& a> noexcept
{
	return static_cast<T&&>(a);
}

이런 형태로 변하면서 결국 우측값이 보존되는것이다.

 

 

반대로 Foo함수에 T&가 들어온다 쳐보자

T&&& forward<typename T& a> noexcept
{
	return static_cast<T&&&>(a);
}

레퍼런스 붕괴법칙 이후

T& forward<typename T& a> noexcept
{
	return static_cast<T&>(a);
}

좌측값이 보존된다!

 

이렇게 보편참조로 모든 타입의 값을 받음과 동시에 인자의 속성까지 그대로 가져가는 방식의 기술을 퍼펙트 포워딩이라 한다.