이것만은 잊지 말자

- 자원을 직접 접근하는 경우가 있기 때문에 RAII 클래스를 만들 때는 관리하는 자원을 얻을 수 있는 방법을 구현해야 함

- 자원 접근은 명시적 변환, 암시적 변환을 통해 가능, 안전성만 따지면 명시적 변환이 대체적으로 더 나음

RAII 객체에서 자원을 변환할 방법

명시적 변환

스마트 포인터의 get 함수 : 실제 포인터의 사본을 얻음

스마트 포인터의 operator->, operator*

 

암시적 변환

명시적 변환 함수를 호출하지 않아도 되므로 편리하지만 실수 발생할 가능성이 높음

f를 의도하지 않게 f2도 공유하여 한쪽에서 소멸시킬 경우 다른 한 쪽에서는 소멸된 객체를 가리킬 수 있는 위험이 있음

class FontHandle;
void changeFontSize(FontHandle f);

class Font
{
public:
	operator FontHandle () const { return f; } // 암시적 변환 함수
    
private:
	FontHandle f; // 실제 폰트 자원
};

Font f(getFont());
changeFontSize(f); // Font->FontHandle로 암시적 변환 수행

FontHandle f2 = f; // Font 객체를 복사하려고 했는데 f가 FontHandle로 변경되고 복사됨

 

따라서 암시적 변환보다는 명시적 변환을 제공하는 것이 바람직함

- 빈 클래스는 각 인스턴스가 고유한 주소를 가져야 하기 때문에 1바이트를 가진다.

 

가변 인자 템플릿

template<typename T, typename... Types> // ... 으로 오는 것을 템플릿 파라미터 팩
void print(T arg, Types... args) // ...으로 오는 것을 함수 파라미터 팩

 

- 가변 인자에 따라 템플릿 인스턴스를 생성

 

폴드 방식 : C++17부터 사용 가능

template<typename ...T>
class A
{
public:
    void Print(T... args)
    {
        (cout << ... << args);
    }
};

int main()
{
    A<int, int, int>* a = new A<int, int, int>();
    a->Print(5, 4, 3);
    delete a;

    return 0;
}

 

재귀 방식 : C++11부터 사용 가능

template<typename ...Types>
class A
{
public:
    template<typename T>
    void Print(T arg)
    {
        cout << arg << endl;
    }

    template<typename T, typename ...Types>
    void Print(T arg, Types... args)
    {
        cout << arg;
        Print(args...);
    }
};

int main()
{
    A<int, int, int>* a = new A<int, int, int>();
    a->Print(5, 4, 3);
    delete a;

    return 0;
}

 

sizeof...

template<typename... Args>
void printArgs(Args... args)
{
    sizeof...(args); // 가변인자의 개수 반환
}

'C++' 카테고리의 다른 글

C++ 빌드 과정  (0) 2024.08.17
C++ STL  (0) 2024.08.17
C++ 템플릿 메타 프로그래밍  (0) 2024.08.06
C++ 자료형 크기 및 범위  (0) 2024.07.25
C++ 템플릿 기초  (0) 2024.06.30

이것만은 잊지 말자!

- RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정됨

- RAII 클래스의 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해주는 선으로 마무리하자, 이외의 방법들도 가능

힙에서 생기지 않는 자원은 스마트 포인터로 처리하기에 적절하지 않으므로 힙에서 생기지 않는 자원들은 자원 관리 클래스를 만들어야 함

Lock 클래스가 복사될 경우 Mutex가 되지 않을 수 있음

class Lock
{
public:
    explicit Lock(Mutext* pm) : mutexPtr(pm)
    {
        lock(mutexptr);
    }

    ~Lock()
    {
        unLock(mutexPtr);
    }

private:
    Mutex* mutexPtr;
};

{
    Mutex m;
    
    {
        Lock locker(&m);
    }
}

 

RAII 객체가 복사될 때 수행하는 동작

복사 금지

복사 함수를 private로 만들어 복사를 실행하면 컴파일 에러가 발생

class Lock : private UnCopyable { ... };

 

관리하고 있는 자원에 대해 참조 카운팅을 수행

자원을 사용하고 있는 마지막 객체가 소멸될 때까지 자원을 해제하지 않아야 하는 경우

shared_ptr 사용

 

깊은 복사

자원을 다 썼을 때 각각의 사본을 확실히 해제해야 함

string은 문자열을 구성하는 원소들을 힙 메모리에 저장하고 메모리에 대한 포인터를 멤버로 갖고 있어서 복사하면 사본이 새로운 힙 메모리를 갖게 됨

 

객체가 가진 모든 데이터를 새로 복사하여 독립된 객체를 만드는 방법

포인터가 있으면 포인터가 가리키는 데이터도 새로 할당하여 복사

장점 : 데이터 독립성, 안전한 메모리 관리

단점 : 느린 복사, 메모리 증가

 

관리하고 있는 자원의 소유권을 옮김 

unique_ptr은 자원을 참조하는 객체를 딱 하나만 존재하도록 복사 될때 그 자원의 소유권을 사본 쪽으로 옮김

 

얕은 복사

객체의 필드값만 복사하는 방법, 포인터가 있으면 포인터 값이 복사되기 때문에 데이터가 공유됨

장점 : 메모리 주소만 복사하므로 속도가 빠름

단점 : 데이터 공유로 인한 문제, 이중 해제 문제(복사된 객체와 원본 객체가 같은 메모리를 해제하려고 시도하는 문제) 가능

유지보수와 유연함, 확장성 향상을 위한 객체지향 프로그래밍 원칙(SOLID)

S(Single Responsibility) : 하나의 클래스는 하나의 책임만 가져야 함

O(Open/Closed) : 클래스 설계를 변경하지 않고 동작을 확장할 수 있어야 함

L(Liskov Subtitution) : 자식 클래스는 부모 클래스를 대체 사용할 수 있어야 함

I(Interface Segregation) : 작고 명확한 인터페이스들로 분리해 관리해야 함

D(Dependency Inversion) : 구체적인 클래스가 아닌 인터페이스에 의존해야 함

 

C#, JAVA부터 보완한 새로운 기능

Interface : 객체 설계의 틀을 제공하는 추상 클래스

Reflection : 런타임에서 객체의 구조를 파악하고 객체에 메타 데이터를 부여

Delegate : 프로그램에서 발생한 이벤트를 다수의 객체에 효과적으로 전달하는데 활용

 

성능과 유지보수를 모두 갖기 위해 언리얼은 C++에 매크로 기능을 활용하여 모던 객체 지향 언어들이 가지고 있는 기능을 사용

C++ 오브젝트 : 저수준의 빠른 처리를 위한 기능 구현에 사용

언리얼 오브젝트 : 콘텐츠 제작에 관련된 복잡한 설계 구현에 사용

 

언리얼 오브젝트

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/objects-in-unreal-engine?application_version=5.1

 

클래스가 언리얼 오브젝트인지 지정하기 위해 UCLASS 매크로 사용
언리얼 오브젝트 생성할 때 NewObject<class>() 사용

 

특징

클래스 기본 객체(CDO) : 클래스의 기본 값과 타입 정보 제공

리플렉션 : 런타임에서 클래스 정보 참조 기능

가비지 컬렉션 : 자동 메모리 관리

직렬화 : 객체 정보를 바이트 스트림으로 저장, 전송, 로드하는 기능

네트워크 리플리케이션

 

언리얼 헤더 툴

ProjectName_API : 다른 DLL(모듈)에서 언리얼 오브젝트 클래스를 사용할 수 있도록 하는 키워드

GENERATED_BODY : 객체지향 설계를 위해서 제공되는 여러가지 기능들을 언리얼 엔진이 자동으로 생성

 

언리얼 헤더 툴에 의해서 언리얼 오브젝트 기능을 제공하기 위해 컴파일 과정에서 매크로들을 참조해 genreated.h를 자동으로 생성하고 최종 빌드 됨

 

언리얼 오브젝트 리플렉션(프로퍼티) 시스템

https://www.unrealengine.com/ko/blog/unreal-property-system-reflection

 

런타임에 자기 자신을 조사하는 기능을 지원

C++은 리플렉션을 지원하지 않아 언리얼에서 Unreal Build Tool(UBT)와 Unreal Header Tool(UHT)을 사용하여 자체적으로 구축

언리얼 오브젝트에만 적용되며 옵션이므로 사용할지 안할지 선택할 수 있음

 

"FileName.genterated.h"를 include하고 UENUM(), UCLASS(), USTRUCT(), UFUNCTION(), UPROPERTY() 매크로를 사용하여 리플렉션으로 등록

언리얼 오브젝트를 선언할 때 항상 generated.h 파일이 가장 마지막에 include 해야 함

cpp 파일에서 언리얼 오브젝트가 선언된 헤더 include 순서가 가장 위에 있어야 함

 

함수 본문에 GENERATED_BODY() 매크로가 필수적

매크로 안에 메타 데이터를 지정하여 게임 컨텐츠를 제작할 때 에디터와 연동하여 여러가지 기능을 만들 수 있음

 

멤버를 UPROPERTY로 선언하면 언리얼에서 자동으로 초기화 및 메모리 관리(가비지 컬렉션), 선언하지 않았다면 직접 메모리를 관리해야 함

 

언리얼 오브젝트 계층 구조

위로 갈수록 상위 클래스

UStruct는 컴포지션으로 UField를 소유

 

언리얼 오브젝트 클래스는 UTypeName::StaticClass()나 Instance->GetClass()를 사용하여 UHT가 자동으로 생성한 리플렉션 정보를 보관한 객체에 접근할 수 있음

for( TFieldIterator<UProperty> PropIt(GetClass()); PropIt; ++PropIt)
{
	...
}

 

각 유형에 플래그나 메타 데이터 등이 있어서 필요한 것만 필터링해서 얻을 수 있음

런타임에 어떤 UClass인지 알 수 있으며 형변환이 가능해짐

 

UPROPERTY를 사용해야 하는 경우

메모리를 GC를 통해 관리해야 하는 변수

블루프린트나 리플렉션을 통해서 참조하는 변수

// Outer를 설정하여 컴포지션 관계 설정 및 객체의 생명 주기를 Outer와 동일하게
CourseInfo = NewObject<UCourseInfo>(this);

// 객체의 생명 주기를 유지할 필요가 없기 때문에 Outer 설정 X
UStudent* Student1 = NewObject<UStudent>();
// AddUObject를 사용하여 오브젝트의 멤버 함수를 바인딩
CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);

 

예제를 통해 실습한 결과 Delegate 호출할 때 바인딩이 늦게 된 순서대로 호출되는 것으로 보임

 

언리얼 오브젝트 처리

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/unreal-object-handling-in-unreal-engine?application_version=5.1

 

UPROPERTY로 선언한 멤버는 자동으로 0으로 초기화됨

 

언리얼 오브젝트 객체의 UPROPERTY로 지정한 것만 사용자가 지정한 포맷에 맞게 I/O해줌

    - Serialization, 네트워크 통신에 사용

 

CDO(Class Default Object)

언리얼 객체가 가진 기본 값을 보관하는 템플릿 객체

한 클래스로부터 다수의 물체를 생성할 때 같은 기본 값을 조정하는 데 사용됨

GetDefaultObject()를 통해 얻을 수 있음

엔진 초기화 이후 사용할 수 있으므로 CDO의 기본값을 변경하는 경우에 에디터를 끄고 다시 컴파일해야 함

 

형변환할 때 Cast를 사용하여 실패하면 nullptr을 보장해주기 때문에 안전하게 형변환 가능

 

리플렉션을 활용하면 접근 지시자와 무관하게 값을 설정할 수 있으므로 상황에 따라서는 필요할 수도 있음

UStudent* Student = NewObject<UStudent>();
UTeacher* Teacher = NewObject<UTeacher>();

UTeacher::StaticClass()->FindPropertyByName(TEXT("Name")); // 프로퍼티 정보를 가져올 수 있음
if (NameProp) 
{
    NameProp->GetValue_InContainer(Teacher, &CurrentTeacherName); // 프로퍼티의 값을 가져옴
    NameProp->SetValue_InContainer(Teacher, &NewTeacherName); // 프로퍼티의 값을 변경함
}

UFunction* DoLessonFunc = Teacher->GetClass()->FindFunctionByName(TEXT("DoLesson")); // 함수에 대한 객체 얻음
if(DoLessonFunc)
{
    Teacher->ProcessEvent(DoLessonFunc, nullptr); // 리플렉션을 활용한 함수 실행
}

이것만은 잊지 말자!

- 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자

- 일반적으로 널리 쓰이는 RAII 클래스는 unique_ptr, shared_ptr
  unique_ptr은 복사될 때 소유권이 이동, shared_ptr은 복사될 때 소유권을 공유

파생 클래스의 객체를 얻는 팩토리 함수를 사용했을 때 로우 포인터를 반환하면 사용자가 직접 소멸시켜야 함

void f()
{
    MyClass* pMyClass = createMyClass();
    ...
    delete pMyClass;
}

 

하지만 delete가 실행되지 않는 코드가 있을 수 있음

  • delete 전에 return 되는 경우
  • 생성과 소멸이 루프 안에 있는데 continue / goto 쓰는 경우
  • delete 전에 예외가 발생하는 경우

따라서 메모리 누수 방지를 위해 자원을 객체(스마트 포인터)에 넣고 소멸자에서 자원을 해제하여 자동으로 자원이 소멸되도록 해야 함

void f()
{
	shared_ptr<MyClass> pMyClass = createMyClass();
}

 

RAII(Resource Acquisition Is Initialization)

자원을 획득하고 나서 바로 자원 관리 객체에 넘겨주는 방법으로 보통 한 문장에 가능

자신의 소멸자를 사용해서 자원을 해제함

 

unique_ptr

자신이 소멸될 때 가리키고 있는 대상을 자동으로 delete함

객체를 가리키는 unique_ptr이 두 개 이상이면 미정의 동작이 발생하므로 unique_ptr 객체를 복사하면 원본 객체는 null로 만듦
(복사하는 객체만이 자원의 유일한 소유권을 갖는다는 의미)

unique_ptr<MyClass> pMyClass1(createMyClass());
unique_ptr<MyClass> pMyClass2(pMyClass1); // pMyClass1은 가리키는 객체가 null

 

RCSP(Reference-Counting Smart Pointer)

소유권을 공유하는 외부 객체 수를 유지하다가 0이 되면 해당 자원을 삭제하는 스마트 포인터

소유권을 공유하기 때문에 복사가 자유로움

shared_ptr

 

unique_ptr 및 shared_ptr은 소멸자 내부에서 delete 연산자를 사용

동적으로 할당된 배열은 vector나 string으로 대체

이것만은 잊지말자!

- 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 함
- 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 말자
  대신 공통된 동작을 제 3의 함수에 분리하고 양쪽에서 이것을 호출하자

복사 함수

복사 생성자, 복사 대입 연산자

복사 함수에 멤버 복사를 빠뜨렸을 경우 컴파일러가 알려주지 않으므로 멤버를 추가하면 복사 함수에 추가해야 함

MyClass::MyClass(const MyClass& rhs) {...}
MyClass& MyClass::operator=(const MyClass& rhs) {...}

 

복사 생성자에서 파생 클래스의 경우 기본 클래스의 멤버를 복사하지 않을 경우 기본 클래스의 기본 생성자에 의해 초기화됨

파생 클래스에서 기본 클래스의 멤버를 복사할 때는 파생 클래스의 복사 함수에서 기본 클래스의 복사 함수를 호출하도록 구현

AddClass::AddClass(const AddClass& rhs) 
    : MyClass(rhs) 
{...}
    
AddClass& AddClass::operator=(const AddClass& rhs) 
{ 
    MyClass::operator=(rhs);
    ...
}

 

복사 대입 연산자와 복사 생성자에서 중복 코드가 있을 경우 private, Init으로 시작하는 함수를 만들자

+ Recent posts

목차