KoreanFoodie's Study
[언리얼] TOptional 사용하기 + 예제 본문
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);
}
참고 : 언리얼 공식 문서, 블로그
'Game Dev > Unreal C++ : Study' 카테고리의 다른 글
[언리얼] TTypeCompatibleBytes 타입 (0) | 2023.03.22 |
---|---|
언리얼 스마트 포인터(Unreal Smart Pointer) 정리 2 : 구현 세부사항과 팁 (0) | 2023.03.22 |
[언리얼] 언리얼의 Cast 동작 원리 (2) | 2023.01.13 |
[언리얼] 위젯 리플렉터 - 언리얼 위젯 디버깅 (0) | 2022.11.16 |
Unreal 에서 Actor 와 ActorComponent 의 개념 (vs. Unity 에서의 GameObject 와 비교) (0) | 2022.10.20 |