KoreanFoodie's Study

[언리얼] TOptional 사용하기 + 예제 본문

Game Dev/Unreal C++ : Study

[언리얼] TOptional 사용하기 + 예제

GoldGiver 2023. 3. 21. 23:45

TOptional

핵심 :

1. TOptional 은 인자로 들어간 녀석이 생성되었는지 아닌지 여부를 간단하게 확인할 수 있는 Wrapper 클래스이다.
2. Optional.h 에 보면 Value 와 bIsSet 이 있는데, Value 가 실제 넘기는 데이터이고, bIsSet 이 해당 데이터의 생성자가 호출되었는지 여부를 판단하는 녀석이다. IsSet, Emplace, GetValue 등을 사용하면 해당 클래스를 유용하게 활용할 수 있다.
3. TOptional 을 사용하는 장점 중 하나는, 특정 변수가 초기화되었는지를 판단하기 위해 'Magic Number' 를 쓸 필요가 없어진다는 것이다!

 


TOptional 구조체

일단,  Optional.h 에 정의된 TOptional 구조체의 대략적인 구조를 보자.

/**
 * When we have an optional value IsSet() returns true, and GetValue() is meaningful.
 * Otherwise GetValue() is not meaningful.
 */
template<typename OptionalType>
struct TOptional
{
public:
	/* 
	* Construct an OptionaType with a valid value.
	* Contructor두 개만 예시로 남겨놨다
	*/
	TOptional(OptionalType&& InValue)
	{
		new(&Value) OptionalType(MoveTempIfPossible(InValue));
		bIsSet = true;
	}
	template <typename... ArgTypes>
	explicit TOptional(EInPlace, ArgTypes&&... Args)
	{
		new(&Value) OptionalType(Forward<ArgTypes>(Args)...);
		bIsSet = true;
	}

	/** Construct an OptionalType with no value; i.e. unset */
	TOptional()
		: bIsSet(false)
	{
	}

	~TOptional()
	{
		Reset();
	}

	/** Copy/Move construction, Operator Overloading, etc */

private:
	TTypeCompatibleBytes<OptionalType> Value;
	bool bIsSet;
};

설명을 읽어보면, IsSet 이 true 면 Optional 안의 Value 가 제대로 설정되었다는 것을 확인하고, 그 이후 GetValue 를 통해 실제 저장된 값을 불러올 수 있다는 것을 알 수 있다.

생성자에서 Optional 에 유효한 값을 넣어 생성할 경우, bIsSet 값이 True 가 되고, 기본 생성자를 넣었을 경우 bIsSet 값이 false 가 됨을 알 수 있다.

Value 의 타입이 TTypeCompatibleBytes<OptionalType> 으로 되어 있는데, OptionalType 은 그냥 템플릿 타입 인자이다.

TTypeCompatibleBytes 는 해당 OptionalType 을 이용해 Align 이 가능한 자료구조를 나타내는 것인데... 자세한 내용은 이 글을 참고하자.


 
IsSet, Emplace, GetValue 등의 함수를 써서 TOptional 함수를 사용해 보자. 정의를 보면...

/** @return true when the value is meaningful; false if calling GetValue() is undefined. */
bool IsSet() const { return bIsSet; }
FORCEINLINE explicit operator bool() const { return bIsSet; }

/** Emplace 는 bIsSet 을 다시 true 로 설정해 준다 */
template <typename... ArgsType>
OptionalType& Emplace(ArgsType&&... Args)
{
    Reset();
    OptionalType* Result = new(&Value) OptionalType(Forward<ArgsType>(Args)...);
    bIsSet = true;
    return *Result;
}

/** @return The optional value; undefined when IsSet() returns false. */
const OptionalType& GetValue() const { checkf(IsSet(), TEXT("It is an error to call GetValue() on an unset TOptional. Please either check IsSet() or use Get(DefaultValue) instead.")); return *(OptionalType*)&Value; }
      OptionalType& GetValue()		 { checkf(IsSet(), TEXT("It is an error to call GetValue() on an unset TOptional. Please either check IsSet() or use Get(DefaultValue) instead.")); return *(OptionalType*)&Value; }

 

Operator -> 가 오버로딩되어 있어서, 실제로 사용할 때는 '.' 아니라 '->' 를 사용하면 된다.

const OptionalType* operator->() const { return &GetValue(); }
      OptionalType* operator->()	   { return &GetValue(); }

const OptionalType& operator*() const { return GetValue(); }
      OptionalType& operator*()		  { return GetValue(); }

 

그렇다면 TOptional 은 왜 사용할까? 장점 중 하나로, 해당 인자가 초기화되었는지 여부를 판단하기 위해 'Magic Number' 를 사용할 필요가 없다는 것을 꼽을 수 있다. 예제를 보자.

struct Wage
{
  // UnSet 되었을 경우 -1 이라는 'Magic Number' 사용
  int minWage = -1;
  int maxWage = -1;
  
  // 0 을 사용
  TPair<int, int> company = {0, 0};
  
  // bool
  bool isEmployed = false;
  
  // 이름
  FString name = Text("");
  
  int myWage = -1;
};

위처럼, 월급을 담는 구조체가 있고, 최소/최대치 등의 여러 변수가 함께 있다고 하자.

 

실제로 월급을 체크할 때는, 아래와 같이 할 것이다 :

Wage GetMyWage(struct Wage wage)
{
  Wage newWage;
  
  // 각종 Magic Number Check
  if (-1 == wage.minWage || -1 == wage.maxWage) { /* minWage, maxWage 필터링 */ }
  if (-1 == wage.myWage) { /* myWage 필터링 */ }
  if (0 == wage.company.Key && 0 == wage.company.Value) { /* company 필터링 */  }
  if (false == wage.isEmployed) { /* myWage 필터링 */ }
  if (TEXT("") == wage.name) { /* name 필터링 */ }

  return newWage;
}

TOptinal을 사용하지 않을 때에는 각 타입별로 정의된 'Magic Number' 를 알맞게 체크해 주어야 한다. 또한 해당 작업은 특정 구조체가 사용되는 곳이 많을수록 실수의 가능성이 점점 커질 것이다.


 
만약 Wage 구조체를 TOptional 로 사용하여 정의하면 어떻게 될까?

struct Wage
{
  // UnSet 되었을 경우 -1 이라는 'Magic Number' 사용
  TOptional<int> minWage = -1;
  TOptional<int> maxWage = -1;
  
  // 0 을 사용
  TOptional<TPair<int, int>> company = {0, 0};
  
  // bool
  TOptional<bool> isEmployed = false;
  
  // 이름
  TOptional<FString> name = Text("");
  
  TOptional<int> myWage = -1;
};

TOptional 을 사용하면, 타입에 따라 특정 'Magic Number' 에 의존할 필요 없이, IsSet 함수를 호출해 주기만 하면 된다!

Wage GetMyWage(struct Wage wage)
{
  Wage newWage;
  
  // 각종 Magic Number Check
  if (wage.minWage.IsSet && wage.maxWage.Isset*()) { /* minWage, maxWage 필터링 */ }
  if (wage.myWage.IsSet()) { /* myWage 필터링 */ }
  if (wage.company.IsSet()) { /* company 필터링 */  }
  if (wage.isEmployed.IsSet()) { /* myWage 필터링 */ }
  if (wage.name.IsSet()) { /* name 필터링 */ }

  return newWage;
}

 

이제 간단한 예제 하나를 더 보면서 글을 마무리하자.

TOptional<TArray<TWeakPtr<MyOption>>> _options;

/* 옵션값 설정... */
void AddOption(TSharedPtr<MyOption> InOption)
{
    // 옵션값 있는지 체크 (없으면 bIsSet 을 Set 해준다)
    if (!_options.IsSet())
    {
        _options.Emplace();
    }
    
    _options->Add(InOption);
}

 

참고 : 언리얼 공식 문서, 블로그 
Comments