[C++][UE5] universal reference (std::move, std::forward Forward, std::remove_reference_t)

ChangJin·2025년 4월 12일
0

Unreal Engine5

목록 보기
120/122
post-thumbnail

글에 사용된 모든 그림과 내용은 직접 만들고 작성한 것입니다.

글의 목적

언리얼 엔진에서 universal reference를 어떻게 다루는지, C++에선 어떻게 다루는지에 대해 정리하기 위함. 그리고 template을 사용해 중복된 함수를 하나의 함수로 만드는 방법에 대해 정리하기 위함.

알게 된 점

  1. universal reference는 rvalue, lvalue 둘 다 의미하는 용어이다.
  2. 언리얼 엔진에서는 std::remove_reference_t를 사용해 universal reference를 다룬다.

참고한 문서

https://learn.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=msvc-170
https://learn.microsoft.com/en-us/cpp/cpp/lvalue-reference-declarator-amp?view=msvc-170
https://learn.microsoft.com/en-us/cpp/cpp/references-cpp?view=msvc-170
https://learn.microsoft.com/ko-kr/cpp/standard-library/remove-reference-class?view=msvc-170
https://learn.microsoft.com/ko-kr/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=msvc-170

내용

🎯 universal reference에 대해

  • microsoft 공식 홈페이지에서는 reference을 다음처럼 소개하고 있습니다.

    "A reference, like a pointer, stores the address of an object that is located elsewhere in memory. Unlike a pointer, a reference after it's initialized can't be made to refer to a different object or set to null. The & operator signifies an lvalue reference and the && operator signifies either an rvalue reference, or a universal reference (either rvalue or lvalue) depending on the context. "

    레퍼런스는 포인터와 같이 메모리의 주소 값을 저장합니다. 포인터는 초기화가 되고 난 이후에도 다른 주소값을 가지거나 nullptr로 설정할 수 있는 반면에 레퍼런스는 다른 메모리 주소값을 가리키거나 null로 설정할 수 없습니다. 설정을 보면 포인터가 만능인데 왜 레퍼런스를 쓸까요? C++의 Move Semantic 때문입니다. 사용자가 지정한 operator+ 같은 연산자 오버로딩을 잘 만들어두면 연산의 결과를 담아두는 임시 객체가 생겨나지 않게 됩니다. 결과 값을 바로 다음 연산에 사용할 수 있습니다.

#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s = string("h") + "e" + "ll" + "o";
   cout << s << endl;
}

& 는 lvalue, &&는 rvalue, &, && 둘다 될 수 있는 것이 universal reference입니다.

// rvalue 참조
void function(Cat&& object) 

// rvalue 참조
Cat&& p1 = Cat("Persian cat", 2010); 

// universal 참조
// var1는 auto를 사용해 명시적으로 나타낼 수 없습니다.
auto && var1 = var2		

// rvalue 참조
// param은 std::vector를 사용해 명시적으로 나타냈고, vector의 내부 객체인 T만 문맥에 따라 구분됩니다. 
template<typename T>
void VectorFunction(std::vector<T>&& param);

// universal 참조
// param 자체를 T로 선언했기 때문에 명시적으로 타입을 알 수 없습니다.
template<typename T>
void UniversalFunction(T&& param);

위의 예시를 보면 알 수 있듯이 여러 타입을 사용할 수 있는 template 함수를 만들 때 universal 참조의 예를 많이 만나게 됩니다. 바로 이럴때 std::forward를 사용해 값을 전달합니다. std::move는 rvalue에 사용합니다.

std::move

std::move를 사용하는 예시를 먼저 보겠습니다.


#include <iostream>
#include <vector>
#include <string>
#include <utility> // std::move를 사용하기 위함

class Cat {
public:
    std::string name;
    int birthYear;

    Cat(const std::string& n, int y) : name(n), birthYear(y) {
        std::cout << "Constructor: " << name << ", " << birthYear << std::endl;
    }

    // Move Constructor
    Cat(Cat&& other) noexcept
        : name(std::move(other.name)), birthYear(other.birthYear) {
        std::cout << "Move Constructor called for " << name << std::endl;
    }

    // Copy Constructor
    Cat(const Cat& other)
        : name(other.name), birthYear(other.birthYear) {
        std::cout << "Copy Constructor called for " << name << std::endl;
    }

    void Info() const {
        std::cout << "Cat: " << name << ", born in " << birthYear << std::endl;
    }
};

// rvalue 참조를 받는 함수
void function(Cat&& object) {
    std::cout << "[function] received an rvalue reference:\n";
    object.Info();
}

// rvalue 참조를 받는 템플릿 함수
template<typename T>
void VectorFunction(std::vector<T>&& param) {
    std::cout << "[VectorFunction] received vector of size " << param.size() << std::endl;
    for (auto& item : param) {
        item.Info();
    }
}

int main() {
    // std::move를 이용해서 rvalue로 캐스팅
    Cat a("Siberian", 2015);
    function(std::move(a));  // std::move로 rvalue로 바꿔 전달

    // rvalue 참조 변수에 rvalue를 대입
    Cat&& p1 = Cat("Persian cat", 2010);  // 임시 객체를 rvalue 참조에 바인딩
    p1.Info();

    // std::vector를 rvalue로 전달
    std::vector<Cat> catVec;
    catVec.emplace_back("Bengal", 2018);
    catVec.emplace_back("Maine Coon", 2016);

    VectorFunction(std::move(catVec));  // std::move로 rvalue로 전달
}

다음의 Cat&& p1 = Cat("Persian cat", 2010); 이 부분을 보면 임시 객체를 그대로 바로 rvalue 참조에 직접 넘겨주고 있는 것을 알 수 있습니다.
VectorFunction(std::move(catVec)); 같은 경우도 rvalue로 vector&& param에 catVec을 전달하고 있습니다. 위 코드의 실행 결과는 다음과 같습니다.

std::forward

#include <iostream>
#include <string>
#include <utility> // std::move, std::forward

class Cat {
public:
  std::string name;

  Cat(const std::string& n) : name(n) {
      std::cout << "Constructor: " << name << std::endl;
  }

  // Copy Constructor
  Cat(const Cat& other) : name(other.name) {
      std::cout << "Copy Constructor for " << name << std::endl;
  }

  // Move Constructor
  Cat(Cat&& other) noexcept : name(std::move(other.name)) {
      std::cout << "Move Constructor for " << name << std::endl;
  }

  void Info() const {
      std::cout << "Cat: " << name << std::endl;
  }
};

// 이 함수는 오직 rvalue만 받습니다
void AcceptRvalue(Cat&& c) {
  std::cout << "[AcceptRvalue] ";
  c.Info();
}

// 이 함수는 오직 lvalue만 받습니다
void AcceptLvalue(const Cat& c) {
  std::cout << "[AcceptLvalue] ";
  c.Info();
}

// universal reference + std::forward
template<typename T>
void UniversalFunction(T&& param) {
  std::cout << "[UniversalFunction] forwarding to the correct function...\n";
  AcceptOverload(std::forward<T>(param));
}

// 내부에서 lvalue/rvalue에 따라 호출될 함수
void AcceptOverload(Cat&& c) {
  AcceptRvalue(std::move(c));
}

void AcceptOverload(const Cat& c) {
  AcceptLvalue(c);
}

int main() {
  Cat kitty("Kitty");

  std::cout << "\n>> Call with lvalue\n";
  UniversalFunction(kitty); // lvalue → lvalue로 전달

  std::cout << "\n>> Call with rvalue\n";
  UniversalFunction(Cat("Tom")); // rvalue → rvalue로 전달

  return 0;
}

위의 코드를 보면 std::move를 사용하여 lvalue로 전달받은 매개변수는 그대로 lvalue로 사용하고 rvalue로 전달받은 매개변수는 rvalue로 사용하고 있습니다. template 함수를 자세히 보면 AcceptOverload에 T 타입을 사용하는 매개변수를 전달하는데 문제 없이 컴파일이 됩니다. 실행 결과는 다음과 같습니다.



🎯 Unreal Engine에서

언리얼 엔진에서는 UnrealTemplate.h 파일을 참고해보면 다음과 같이 선언되어 있습니다. std::forward를 언리얼 엔진에서 비슷하게 만들었다고 주석에 나와있습니다. 참고로 std::remove_reference_t는 type으로부터 non reference type을 만들어 냅니다.

// output : remove_reference_t<int&> == int
#include <type_traits>
#include <iostream>

int main()
{
	int *p = (std::remove_reference_t<int&> *)0;

	p = p;  // to quiet "unused" warning
    std::cout << "remove_reference_t<int&> == "
        << typeid(*p).name() << std::endl;

    return (0);
}

위의 코드에서 (std::remove_reference_t<int&> *)0;가 생소할 수 있는데요. 0, 즉 널포인터를 std::remove_reference_t<int&> 타입의 포인터로 캐스팅하는 것입니다. 즉 int&에서 reference를 제거했으니 int가 되고 여기에 *를 붙였으니 int* 가 됩니다. 이것만 이해했다면 잘 다룰 수 있습니다.

/**
* Forward will cast a reference to an rvalue reference.
* This is UE's equivalent of std::forward.
*/
template <typename T>
UE_INTRINSIC_CAST FORCEINLINE constexpr T&& Forward(std::remove_reference_t<T>& Obj) noexcept
{
	return (T&&)Obj;
}

template <typename T>
UE_INTRINSIC_CAST FORCEINLINE constexpr T&& Forward(std::remove_reference_t<T>&& Obj) noexcept
{
	return (T&&)Obj;
}

즉 언리얼 엔진에서의 Forward는 T 타입의 매개변수에 reference를 제거하고 T타입의 Rvalue로 캐스팅하여 리턴합니다. std::forward과 같습니다.



💡 이제 언리얼 엔진에서 제가 사용했던 코드를 보여드리겠습니다. 다음처럼 hud에서 ui 메시지를 띄우는 기능인데요. 상당히 많은 부분에 같은 로직이 사용되고 있었습니다. 이를 template 함수로 사용하기 편하게 만들고자 했습니다.

if (AMyHUD* myHUD = Cast<AMyHUD>(GetHUD()))
{
	if (myHUD->m_wHUDPanelWidget)
	{
		myHUD->m_wHUDPanelWidget->PopUp(FText::FromString(FString::Printf(TEXT("Remain Time : %.0f"), _time)));
	}
}

if (AMyHUD* myHUD = Cast<AMyHUD>(GetHUD()))
{
	if (myHUD->m_wHUDPanelWidget)
	{
		myHUD->m_wHUDPanelWidget->PopUp(FText::FromString(FString::Printf(TEXT("Move Next Level..."))));
	}
}

if (AMyHUD* myHUD = Cast<AMyHUD>(GetHUD()))
{
	if (myHUD->m_wHUDPanelWidget)
	{
		myHUD->m_wHUDPanelWidget->PopUp(FText::FromString(FString::Printf(TEXT("You Attacked! %s %f"), _other, _damage)));
	}
}

구조를 잘 보면 FText로 전달하는 저 부분만 바뀌고 있습니다. 따라서 FmtType으로 rvalue를 받고 이 rvalue를 Forward 함수를 사용해 그대로 PopUp함수에 rvalue를 전달하게끔 만들었습니다. 물론 매개변수를 여러 개 받을 수 있도록 typename...도 사용했습니다.

template<typename FmtType, typename... Args>
void PopUp(FmtType&& Format, Args&&... args)
{
	const FString Text = FString::Printf(Forward<FmtType>(Format), Forward<Args>(args)...);
	
	if (AMyHUD* myHUD = Cast<AMyHUD>(GetHUD()))
	{
		if (myHUD->m_wHUDPanelWidget)
		{
			myHUD->m_wHUDPanelWidget->PopUp(FText::FromString(Text));
		}
	}
}

이제 다음처럼 간단하게 사용할 수 있습니다.

PopUp(TEXT("Remain Time : %.0f"), _time);
PopUp(TEXT("Move Next Level..."), _time);
PopUp(TEXT("You Attacked! %s %f"), _other, _damage);
profile
게임 프로그래머

0개의 댓글