← SEARCHABLE ARCHIVE
C / C++ · 2026-08-13

C++ 기초부터 마스터까지 — 19화

값 범주와 perfect forwarding으로 전달 의미 보존하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

이동 의미론을 이해한 뒤에도 템플릿 래퍼를 만들면 인자가 의도치 않게 복사되는 문제가 생길 수 있다. Perfect forwarding은 호출자가 전달한 lvalue와 rvalue의 성질을 다음 함수까지 보존한다.

이름이 있는 변수는 lvalue다

void Store(const Texture& texture); // 읽기
void Store(Texture&& texture);      // 소유권 이동 가능

Texture loaded = LoadTexture();
Store(loaded);              // lvalue 오버로드
Store(std::move(loaded));   // rvalue 오버로드
Store(LoadTexture());       // rvalue 오버로드

lvalue는 지속되는 객체의 위치를 나타내고 rvalue는 이동해도 되는 임시 값이나 명시적으로 변환된 값을 나타낼 수 있다. 타입이 Texture&&인 변수도 이름을 붙여 식에서 사용하면 그 표현식은 lvalue다.

전달 참조와 참조 축약

template <typename T>
void Register(T&& value)
{
    Store(value);
}

여기서 T가 추론되는 T&&는 전달 참조다. lvalue를 전달하면 T는 lvalue 참조로 추론되고 참조 축약 규칙에 따라 매개변수도 lvalue 참조가 된다. rvalue를 전달하면 rvalue 참조가 된다.

하지만 value는 함수 안에서 이름이 있으므로 Store(value)는 항상 lvalue 오버로드를 선택한다.

std::forward로 원래 값 범주 복원하기

#include <utility>

template <typename T>
void Register(T&& value)
{
    Store(std::forward<T>(value));
}

std::forward<T>는 원래 호출이 lvalue였다면 lvalue로, rvalue였다면 rvalue로 전달한다. std::move처럼 무조건 rvalue로 바꾸는 것이 아니다.

생성자 인자를 그대로 내부 객체에 전달하는 팩토리에서도 같은 패턴을 사용한다.

template <typename T, typename... Args>
std::unique_ptr<T> MakeOwned(Args&&... args)
{
    return std::make_unique<T>(std::forward<Args>(args)...);
}

모든 함수에 전달 참조가 필요한 것은 아니다

문자열을 저장하는 setter는 값으로 받고 한 번 이동하는 방식이 더 단순할 수 있다.

void Player::SetName(std::string name)
{
    Name = std::move(name);
}

Perfect forwarding은 오버로드 집합을 넓게 받아 예상하지 못한 타입까지 잡을 수 있고 컴파일 오류도 복잡하게 만든다. 실제로 여러 생성자 인자를 투명하게 중계해야 하는 범용 코드에서 사용하고, 애플리케이션 경계에서는 구체적인 타입과 소유권 계약을 우선하는 것이 좋다.