본문 바로가기
프로그래밍/Unreal 부트캠프

TIL 2024.12.31 기록

by Rozentea 2024. 12. 31.

0. 개요


오늘은 어제 진행했던 SimpleVector에 대한 설계를 올바르게 한건가에 대한 고민을 하다가 여러가지 실험도 해보고, 더 찾아보면서 공부하며 시간을 보냈다.

직접 실험을 해보면서 이전에 이해한다고 생각했던 것들에 대해 다시한번 더 복습을 하고, 추가적인 공부를 할 수 있어서 굉장히 의미가 깊은 시간이었던 것 같아 만족스럽다.

 

오늘 정리는 강의를 듣고, 궁금했던 부분이나 알게된 부분에 대해 정리를 하고

이후에는 과제에 대한 어떤 고민을 했었고, 그 고민으로 부터 어떤 공부를 했는지에 대해서 간단하게 정리할 것이다.

 

1. 템플릿(Template)


템플릿은 일반화된 코드를 작성할 수 있는 문법이다. 또한 템플릿을 활용한 프로그래밍을 Generic Programming이라고 한다.

=> 템플릿과 제네릭이라는 단어자체는 다른 의미인것 같은데, 두가지를 결합해 사용한 프로그래밍을 저렇게 부르는것 같다.

 

제네릭 및 템플릿(C++/CLI)

자세한 정보: 제네릭 및 템플릿(C++/CLI)

learn.microsoft.com

 

템플릿을 활용하면 코드 재사용성이 대폭 증가하고, 다형성도 자연스럽게 달성할 수 있다는 장점도 있다.

 

템플릿을 이용하면, 어떤 타입이든 대응할 수 있도록 만들 수 있다.

이런게 가능한 이유는 컴파일 타임에 템플릿을 기반으로 실제 코드를 생성해주기 때문이다.

ㄴ> 템플릿 자체는 컴파일되지 않고, 컴파일 타임에 템플릿을 사용하는 부분을 확인하면, 구체적인 타입에 대해 코드가 인스턴스화되어 실제로 생성된다.

ㄴ> 이러한 이유 때문에 코드 재사용성이 대폭 증가한다.

 

템플릿의 단점

가독성이 굉장히 나빠진다.

ㄴ> 어떤 타입에 대응되어 템플릿이 진행될지도 빠르게 파악하기 힘들 뿐더러 가변 템플릿으로 만들게 되면 훨씬 복잡해지기 때문이다.

빌드 시간이 증가한다.

ㄴ> 빌드시간이 많이 증가하기는 하지만, 실제 업무에서 템플릿을 제외하고라도 프로그램이 굉장히 무거워져 빌드 시간 자체가 길 수 밖에 없다고 한다. 때문에 빌드시간이 템플릿을 지양할 이유가 되지는 않는다고 한다.

ㄴ> 이외의 장점이나 활용처가 많기 때문이라 판단된다.

 

템플릿은 함수뿐 아니라 클래스를 설계할 때도 사용할 수 있다.

 

템플릿 특수화(Template Specialization)

일부의 경우에 대해서 따로 처리하는 것을 의미하는데, 템플릿이라고 무조건 모든 타입이 T일 필요는 없다.

즉, 중간 중간 우리가 원하는 타입을 명시함으로써 해당 멤버 변수나, 인자 등등을 지정해줄 수 있다.

 

 

지금까지 템플릿을 크게 활용해본적이 없어서 템플릿은 기초적인 부분은 알지만, 잘 모르는 영역이라해도 될 것 같다.

때문에 추가적인 공부를 해서 따로 포스팅을 해야할 것 같다.

 

또, 이번기회에 실제 게임 개발을 할때는 템플릿을 어떻게 활용될까?에 대한 고민을 많이해봤다.

답을 듣기로는 퀘스트 기능을 만들 때, 어떤 것들은 물건 자체를 가져와 개수를 세기 때문에 int를 사용하는데, 어떤 퀘스트는 %로 진행 상황을 나타낸다면 float을 사용하는 등의 대응되어야 할 타입이 다양할 때 등의 이야기를 들을 수 있었다.

ㄴ> 하지만 아직 가변 템플릿도 몰라서 그런걸까..? 잘 이해가 안되었다.

ㄴ> 때문에 다시 조금 더 깊게 공부해보고 질문을 드려보려 한다.

 

2. decltype


처음 공부하는 decltype이니 만큼 더 자새히 공부하고 정리하고 글을 적어두고 싶다.

 

때문에 해당 글에는 어떤 것인지에 대해 간략하게만 정리해두고, c++카테고리 쪽에 본격적으로 정리해 올릴 생각이다.

 

아무튼

auto와 decltype은 다르다.

자료형을 추론해 프로그래머가 직접 작성하지 않아도, 내부적으로 추론한 자료형이 auto나 decltype()의 자리에 들어가게 된다.

ㄴ> 물론 이게 컴파일 타임에 추론되어 해당 자리에 실질적인 자료형으로 치환되어서 바이너리가 완성되는지는 아직 잘 모르겠다.

 

auto의 경우 추론은 하지만 정확한 타입을 전달해주지 않는다.

ㄴ> const나 레퍼런스와 같은 것은 반영해주지 않기 때문에 추가적으로 명시해주어야 한다.

ㄴ> auto의 경우 기본적으로 복사를 통해 값을 할당한다.

 

decltype의 경우 정확한 자료형을 추론해 전달해준다.

ㄴ> 정확한!! 타입을 추론하기 위해 사용된다.

 

또 내부적으로 동작하는 매커니즘도 다른데, auto가 내부적으로 decltype을 사용하지는 않는다.

ㄴ> auto는 초기화된 변수의 타입을 추론하는 방식, decltpye은 표현식의 정확한 타입을 추론하는데 사용한다.

ㄴ> 즉, auto는 값 기반으로 타입을 추론하며, decltype은 표현식을 기반으로 정확한 타입을 추론하는데 집중한다.

 

c++14부터 추가된 문법이 있는데, 이를 활용하면 함수의 리턴 타입을 정확하게 추론해 사용할 수 있다.

template <typename T, typename U>
auto add(T t, U u) -> decltype(t+u)
{
	retirm t + u;
}

이렇게 작성하면, 리턴 타입을 정확히 추론해준다.

template <typename T, typename U>
decltype(t + u) add (T t, U u)
{
	return t + u;
}

가 오류가 발생하는 이유는 아직 t와 u가 무엇인지도 모르는 상태에서 decltype을 사용하기 때문이다.

음.. 코드를 진행하는 순서에 있는데, 위에서 아래로, 왼쪽에서 오른쪽으로 진행하기 때문에 decltype(t + u)를 확인하는 시점에서 아직 t와 u가 없기 때문에 알 수 없는 것이다.

때문에 auto와 함께 사용하는 문법이 새로 추가된 것이다.

 

decltype과 다르게 std::declval라는 것이 있는데, decltype은 키워드이고, declval는 <utilty>에 정의된 함수이다.

 

decltype을 공부하면서 decltype 키워드는 우리가 원하는 식의 타입을 알 수 있고, 해당 식이 단순한 식별자 표현식(identifier expression)이라면, 그냥 그 식의 타입으로 치환된다는 것을 알았다.

그 이외의 경우라면 해당 식의 값 카테고리가 무엇인지에 따라 decltype의 타입이 정해진다.

c++의 모든 식에는 두 가지 꼬리표가 따라다니는데, 하나는 타입이고, 다른 하나는 값 카테고리이다.

값 카테고리는 크게 3가지 lvalue, pvalue, xvalue로 나뉘고, 조금 넓은 범위에서는 rvalue, gvalue로 나뉜다.

이러한 카테고리 덕분에 decltype과 declval함수를 이용해 타입을 추론하고 치환해 사용할 수 있다는 것을 공부했다.

 

다만 템플릿과 마찬가지로 큰 질문이 남아있는데, 그래서 이러한 기능들을 어디에 사용하느냐이다.

실제 게임을 개발할 때 어떤 부분에서 사용하면 좋을지가 아직 감이 잘 안와서 프로젝트 경험을 추가적으로 쌓거나, 좀 더 공부를 하면서 고민을 해봐야하는 문제인것 같다.

 

3. 과제에 대한 고민


어제 가변 배열을 템플릿으로 구현하면서, 클래스를 동적할당한 포인터들을 가변 배열로 관리한다고 할 때, 배열이 삭제되면서 해당 요소들의 메모리를 해제해주지 않아 메모리 누수가 발생하는게 문제라 생각했다.

 

떄문에 SimpleVector의 소멸자에서 포인터 타입일 경우 배열 요소들을 순회하면서 동적할당한 메모리들을 해제해주도록 만들었다.

이는 pop_back()도 동일했었다.

 

하지만 이렇게만 해둘 경우 문제가 있는데, int*나 float*처럼 일반 타입의 주소를 배열이 관리할 때, 동적할당을 받은게 아니기 때문에 메모리 해제를 하게될 경우 오류가 발생한다.

 

이 문제는 is_interfral이나 decltype으로 예외처리를 해줄 수는 있지만, 근본적으로 문제라 생각했다.

 

왜냐하면, SimpleVector 외부에서 동적할당 받은 요소들을 SimpleVector 내부에서 해제해주는게 이상한 설계라 생각했다.

이런 이유에서인지 C++ STL에서 지원하는 vector도 내부적으로 처리를 해주지는 않는다.

(이전 프로젝트들을 진행하면서 STL을 굉장히 많이 사용했는데, 경험상 그랬고, 실제로도 위와 같은 처리를 해주지 않는다.)

 

때문에 구조 자체를 외부에서 동적할당 받은 것들은 외부에서 처리하도록 변경해주었다.

 

어제 해당 고민을 하고 내부에서 처리하게 만들면서 constexpr이나 is_pointer에 대해 공부한 것은 의미 있는 공부였다.

 

4. delete와 delete[]의 차이


delete : 단일 객체의 소멸자를 호출한다.

단일 객체에 대해 정의된 동작을 수행하며, 배열에 대해 사용하면 정의되지 않은 동작이 발생할 수 있다.

 

delete[] : 배열의 각 요소에 대해 소멸자를 호출한다.

배열의 메모리를 해제하는 데 정의된 동작을 수행한다.

 

어제 소멸자에서 delete를 사용했는데, 큰 차이가 없었던 이유가 무엇일까?

생각해보니 어제 테스트를 진행할 때 배열이 지닌 요소의 타입이 일반 타입이거나 포인터 타입이었다.

이 경우 해당 타입에 대한 소멸자가 호출되면서(?) delete가 delete[]처럼 동작해 정상적으로 메모리가 해제된 것 처럼 보인 것이다.

 

이를 실험해보니,

delete를 사용해 진행하면, Animal의 소멸자가 잘 호출은 되지만, 이후 해제가 진행되면서 오류가 발생하게 된다.

ㄴ> 정확히 어떤 오류인지는 모르지만, 메모리 해제를 잘못 시도했다는 것은 알겠다.

ㄴ> 즉, 우리가 생각하지 않은 오류가 발생한 것이다.

ㄴ> 물론 잘못된 문법을 사용했기 때문에 생각하지 못한 오류라고 하기엔 좀 이상하지만..

 

반면에 delete[]를 사용하면, 우리가 의도한 대로 배열의 요소 하나하나에 소멸자가 호출된다.

 

왜 소멸자 호출이 기본적으로 4번이 발생하는가?

이유는 Animal a1, a2, a3, a4로 선언을 해주고 전달하기 때문에 해당 객체가 생성될 때 생성자가 호출이된 것이고, 프로그램이 종료됨에 따라 a1부터 a4까지 소멸자가 호출되어 삭제되는 것이다.

 

그러면 왜 push_back()을 4번만 했는데 소멸자가 10번이 호출되는가?

이유는 Sim_vect를 선언했을 때, SimpleVector 생성자에서 메모리 공간을 Animal 10만큼을 할당해주도록 구현했다. 또 malloc이 아닌 new를 사용했기 때문에 Animal의 생성자를 호출해 객체까지 생성해준다.

때문에 push_back()을 4번 했어도 소멸자가 10번 호출되는 것이다.

 

이런 이유 때문에 사실상 push_back()을 해주지 않고 배열만 만들어도

Sim_vect[0].a = 4; 와 같은 작업을 진행하며 사용해도 된다.

 

5. new와 malloc의 차이


 

 

C++ new와 delete/ malloc()과 free()

해당 글은 공부를 하면서 적은 글이기 때문에 틀릴 수 있습니다. 참고용으로만 봐주세요~ 해당 글을 보기 전에 메모리 구조에 대해 먼저 살펴 보면 도움이 될겁니다. C++ 메모리 구조 해당 글은

rozentea.tistory.com

이전에도 공부해서 정리해 두었는데, 이때는 이해만하고 직접 실험해보지는 않았다.

그래서 인지 오늘도 실험을 진행하면서 왜 이렇게 되는지가 바로 떠오르지 않고 고민을 해보게 되었다.

 

오늘 확인한 것은 배열만 생성하고, push_back()을 하지 않았어도 왜 생성자가 호출되는가? 였다.

이유는 간단한데, malloc은 메모리 공간만 할당해주고, new는 생성자를 호출해 객체까지 만들어 그 주소를 반환해주기 때문에 차이가 있다.

즉, new를 사용했었기 때문에 객체가 생성된 것이다.

 

malloc으로 진행해 실험해보았을 때, 메모리 공간만 할당 받아서 타입 케스트를 진행하니까 쓰레기 값들이 들어있었다. (값을 넣은게 없이 공간만 할당 받았기 때문.)

 

이번 실험을 진행하면서 new와 malloc의 차이를 확실히 알게된 것 같았고, 이전보다 기억이 더 선명하게 남을 것 같다.

 

6. 임시 객체 및 복사 대입


만들어둔 객체를 전달하는게 아니라 Animal()처럼 생성자를 호출해 객체를 전달할 경우

임시 객체가 만들어진다.

 

또한, push_back()을 진행하면서 data[currentSize] = value;로 대입하게 되는데, 이때는 복사 대입을 하게 된다.

 

임시 객체와 복사 대입에 대해 너무 단편적인 실험이고, 정리이긴 한데

기존에 알고 있던 지식을 더 채운 느낌이고, 몸소 경험하면서 더 깊게 이해한 것 같았다.

 

7.함수에 임시 객체 전달할 때 진행 순서


int main()
{
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	//test
	SimpleVector<Animal> Sim_vect;

	Sim_vect.push_back(Animal());
	Sim_vect.push_back(Animal());
	Sim_vect.push_back(Animal());

	return 0;
}

// .....

template<typename T>
inline void SimpleVector<T>::push_back(const T& value)
{
	// 배열이 가득 차서 원소를 추가할 수 없을 경우, 배열 허용량을 5늘려 재할당해 원소를 추가해준다.
	if (currentCapacity <= currentSize)
	{
		resize(currentCapacity + 5);
	}
	data[currentSize++] = value;
}

 

1. Animal 클래스의 생성자가 호출되고, 임시객체가 생성된다.

2. push_back() 함수가 호출되고, 진행된다. (이때는 value를 대입해주기 때문에 복사 대입이 발생한다.)

3. push_back() 함수가 종료된다.

4. Animal 클래스의 소멸자가 호출되어 임시객체가 삭제된다.

 

이는 함수 인자로 전달할떄, 어떻게 전달이 되고, 어떻게 함수가 진행되고 동작되는지에 대한 내용이다.

 

이전에도 복사 대입이나 임시 객체에 대한 내용을 공부하고, 유의했다고 생각했었는데, 직접 실험을 해보니 유의하면서 코드를 작성하지는 않았던것 같다.

 

이번 실험을 통해 좀 더 확실히 알게되었고, 유의하며 코드를 작성하게된 것 같다.

 

8. 함수 오버로딩 주의사항


한줄로 끝날거 같아서 일단 가장 뒤로 미루었다..

 

디폴트 매개변수의 모호성 주의할 것.

ㄴ> 다른 것들은 어느정도 바로바로 생각났는데, 해당 내용만 좀 나중에 떠올랐다.

 

9. 마무리


올해의 가장 마지막 날이다..

지금까지 이전에 다니던 회사를 퇴사하고.. 코딩 공부를 시작한지도 2년째가되어 간다..

힘들 때도 있었고, 프로젝트도 몇 번 진행하면서 의미 있는 시간들을 보냈던 것 같다.

하지만 실력이 크게 늘었을까에 대한 고민이나 앞으로 어떤 것들을 채워나가야 취업에 성공할까 등.. 해결하지 못한 고민들도 많다..

 

우선 지금하고 있는 공부에 집중하고, 또 지금까지의 열정과 흥미를 잃지않고 더 노력한다면.. 내년에는 분명 좋은 결과가 있겠지..!

 

이렇게 오늘 하루 공부를 마무리하고, 내일 새로운 해의 나도 화이팅..!

 

강의를 들으면서 부족한 부분을 채워나간 점이나, 과제를 할때 어떻게 설계하는게 맞는건가에 대한 고민도 해보고, 또 고민한 결과를 바탕으로 더 깊게 찾아보고, 알아보고, 실험해보며 공부할 수 있어서 좋은 하루였다.

'프로그래밍 > Unreal 부트캠프' 카테고리의 다른 글

TIL 2025.01.03 기록  (3) 2025.01.03
TIL 2025.01.02 기록  (0) 2025.01.02
TIL 2024.12.30 기록  (0) 2024.12.30
TIL 2024.12.27 기록  (0) 2024.12.27
TIL 2024.12.26 기록  (0) 2024.12.26