개인적으로 강사님이 c++11에서 제일 좋아하는 문법.
[] (인자) { }
[] (Item& item) {return item.rarity == Rarity::Unique;}
람다 는 struct 해서 operator() 오버로딩 한 함수 객체랑 똑같다고 보면 된다.
다만 struct 이름을 명시적으로 해주지 않았는데 컴파일러가 대충 구별할 수 있게끔 자기만의 이름을 지어준다 ( 무명 )
또한 return 형식도 표현하지 않았는데 컴파일러가 return 타입을 추론해준다.
물론 lamda 표현식도 auto로 지정해줄 수 있다.
closure : 람다에 의해 만들어진 실행시점 객체.
capture : 함수 객체 내부에 변수를 저장하는 개념과 유사
기본 캡쳐 모드 : 복사 방색(=) , 참조방식 (&)
중요한것은 class Knight 내부에서 auto ResetHpJob() 이란 public 함수를 만들었고
그 안에서 람다함수를 만들었을때 캡쳐모드로 무엇을 할것인가?
일단 캡쳐모드에 아무것도 하지 않는 다면 오류가 뜬다.
그럼 캡쳐모드에 기본 캡쳐모드인 = (복사) 방식을 하면 오류가 없어지는데
이렇게 대수롭지 않게 넘어갈 수 있다.
그러나 애당초 클래스 내부에서 멤버 변수를 바꿔주는 코드는 this-> 가 항상 생략됐다고 생각하면 되고
이 경우에도 캡쳐로 = 했다는건 knight의 주소값을 가지고 있는 this 를 =(복사) 했다는거다.
문제는 외부에서 Knight 가 delete 되게 되면 문제가 생긴다.
( 이 부분은 강의를 통해 복습하는게 빠를것 같다. )
스마트 포인터
멀티 쓰레드 환경에서도 잘 돌아가야 되기 때문에
서버 강의에서 자세하게 살펴보는 시간을 가질거고, 이번 시간에는 훑어 보는 시간을 가져보자.
여려명에서 작업하는 프로젝트에서 메모리 오염을 일으키는 사람은 정말 역적이 된다.
요즘 현대적인 c++ 에서는 포인터를 직접적으로 사용하지 않는다.
반드시 스마트 포인터로 간접적으로 사용하는게 일반적이다.
스마트 포인터 : 포인터를 래핑해서 사용하는 느낌.
사용 이유는 간단하다 : RAII ( 자원의 획득은 Initialize 이다 )
* RAII 는 구글링만 해도 자료가 수두룩 하다.
Modern C++ 에서는 세가지 스마트 포인터를 알아야된다.
( 언리얼 엔진에서는 표준 방식의 스마트 포인터를 사용하지 않고, 자기들이 다른 클래스르 만들어서 사용하지만 비슷하다. 그리고 표준에서 사용하는 방식을 알아야한다. )
- unique_ptr ,
- shared_ptr ,
- weak_ptr
# 22.08.07 추가
Unique_ptr
unique_ptr 은 이름처럼 해당 포인터에 대한 exclusive ownership을 보장해준다.
특별할건 없고, 조심해야 될 부분이랑 언제 자주 쓰이는지 보자.
다음과 같은 코드는 오류가 난다.
std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
std::unique_ptr<Cat> catPtr2 = catPtr;
unique_ptr 은 copy constructor가 delete 되어 있다.
생각해보면 당연한거다.
해당 포인터에 대한 소유권이 unique 해야되는데 누군가 copy constructor 로 복사를 하면 두명이 소유하게 되는거니깐,
대신에, std::move() 를 통해 move constuctor 나 move assignment 는 가능하다.
말 그대로, unique 하게 가지고 있던 소유권을 넘겨준다고 보면 된다.
unique_ptr은
class 에서 member variable 로 포인터를 가져야만 할 경우에 많이 사용한다.
Animal 클래스가 있다고 가정하고, Dog 클래스와 Cat 클래스는 Animal 클래스를 상속받는다고 가정해보자.
이때 Zoo 라는 동물원 클래스에서 멤버변수로 Animal* (포인터) 가지고 있을때
생성자에서 어떤 상황에 때라 Dog 를 만들지 Cat 을 만들지 선택 할 수 있다.
Zoo 클래스는 멤버 변수로 mAnimal을 가지고 있다.
이 말은, rule of three (생성자, 복사 생성자, move 생성자 ) + ~(소멸자) , copy / move assignment 를 직접 만들어줘야 된다는 소리이다.
또한, 이 멤버 포인터가 가리키는 오브젝트는 클래스 밖에 있는 다른 Animal 포인터가 가리킬 수 있는 상황이 발생할 수도 있다.
이를 원천적으로 막기 위해서 unique_ptr 을 쓰면은
더 이상 개발자가 소멸자를 통해 delete로 오브젝트를 해제해준다거나, move construct 나 move assignment 를 생각할 필요가 없어지는거다.
일종의 wrapper 클래스로 개발자가 따로 코딩할 필요 없이 편하게 쓰는거다.( copy constructor나 copy assignment 는 unique_ptr 에서 애시당초 delete 되어 있기 때문에 못 쓴다.)
# 22.08.07 추가
Shared_ptr
unique_ptr 가 다르게 shared ownership이 보장된다.
shared_ptr Ref conunt 를 가지고 있는데, 이 Ref count를 통해 언제 객체를 delete 해야되는지 알고 있는거다.
그리고 Ref count 는 shared_ptr 을 첫번째로 만들때, Ref count block이 힙영역에 할당되는데 이후 shard_ptr을 share 하는 모든 포인터들은 같은 공간의 Ref Count block을 공유하기 때문에 모두 같은 Ref count 를 공유 할 수 있다.
shared_ptr 의 user_count() 함수를 통해 count 를 확 인 할 수 있다.
그럼 조심할 부분을 알아보자.
mPtr 은 Cat 클래스의 Shared_ptr인데, Cat 클래스 안에 멤버 변수로 shared_ptr 을 하나 더 가지고 있고,
mPtr-> mVar = mPtr ;
을 통해 자기 자신을 가리키게 했다.
그리고 main 함수가 끝나면서 mPtr 없어지면서, Ref conunt 가 1 줄게 되지만,
ref cnt가 0이 아니기 때문에 Heap 영역에 메모리 leak 이 일어나게 된거다.
하지만, 위 경우는 memory leak을 보여주기 위해 일부로 만들어낸 케이스이고,
실제 개발자들이 가장 많이 하는 실수는 아래와 같은 코드이다.( Circular Refernce )
각 객체가 가지고 있는 mFriend 포인터가 서로를 가리키고 있기 때문에
스택에 있던 포인터 변수가 없어질때 ref cnt가 1 줄어들지만 ref cnt가 0이 되지 않아서 메모리 릭이 발생하는 경우이다.
[ 참고 ]
https://www.youtube.com/watch?v=tg34hwP0P0M&list=PLDV-cCQnUlIbOBiPvBaRPezOLArubgZbQ&index=4
# 22.08.07 추가
weak_ptr
weak_ptr 이란 shared_ptr 처럼 가리킬 순 있지만, Ref cnt에는 아무런 영향을 주지 않는 스마트 포인터이다.
예를 들어 어떤 objcect 를 두개의 shared_ptr 이 가리키고 있다면 Ref cnt 는 2일 것이다.
이때 weak_ptr가 같은 object 를 가리키게 된다 해도, Ref cnt 가 1 증가하지 않는것이다.
때문에 object의 Life Cycle 에는 아무런 영향도 미치지 않게 된다.
대신에 Weak_ptr을 사용하기 위해서는 이를 shared_ptr로 전환한 후에 사용해야된다.
shared_ptr로 전환 할 때에는 Ref Cnt 가 1증가하게 될 것이다.
weak_ptr 이 shared_ptr(sPtr) 이 가리키는 똑같은 오브젝트를 가리킴에도 불구하고, sPtr.use_count() 함수의 결과는 1이 나오는 것을 확인 할 수 있다.
그리고 weak_ptr을 사용하기 위해서는 weak_ptr 의 lock() 함수를 이용해서
if state 문 안에서 spt(shared_ptr) 을 만들고 그 shared_ptr을 통해서 speak() 함수를 사용해야 한다.
하지만 아래와 같이 shared_ptr가 scope 안에서 만들어지고 없어 졌을때는
없어진 Cat 객체를 가리키고 있는 weak_ptr (wPtr) 은
if( const auto sPtr = wPtr.lock())
에서 false 를 리턴하기 때문에 else 문으로 분기를 타게 된다.
그러면 weak_ptr이 vaild 한지 아는 방법은 무엇이 있을까
weak_ptr 이 valid 한 오브젝트를 가리키고 있는지 알기 위해서는
- 위에서 사용한 것 처럼 lock() 을 사용하던
- use_count() 가 1 이상일 때 이던
- expired() 가 false 를 리턴해줄때
로 확인 할 수 있다.
그러나 weak_ptr 을 사용하기 위해서는
언제나 shared_ptr 로 받아서 사용해야 하기 때문에
shared_ptr을 반환하는 lock() 함수를 사용하는게 좋다.
나머지 방법들은 그저 valid 한 오브젝트를 가리키고 있는지 아닌지만 알 수 있다.
그리고 weak_ptr 을 사용하는 가장 큰 이유 중 하나는
shared_ptr 의 circular Refernce 문제를 해결 할 수 있어서이다.
아래의 문제 있는 코드를
weak_ptr을 사용하게 된다면
서로를 가리키게 해도 Ref Cnt 가 증가하지 않으니 정상적으로 main() 함수의 끝에서 sPtr1, sPtr2가 없어지게 된다.
# 22.08.07 추가 끝 --
shared_ptr 은 포인터를 관리하는데 ref_count를 관리해서 몇명이나 참조를 하고 있는지 관리를 해준다.
그리고 누군가 한명도 기억하지 않을때 delete 해준다.
main 함수에서 k1이 scope를 벗어나서 없어지는데도 k2는 sharedPtr 덕분에 옳바른 값을 포인팅 할 수 있게 된다.
이제는 우리가 만든 SharedPtr 말고 표준 shared_ptr을 사용해보자. ( 비슷한 방식으로 구현되어 있다고 생각하면 된다. )
서버쪽에서는 그냥 쌩포인터를 사용하는것은 Modern C++ 이후부터는 없다고 보면 된다.
그럼 weak_ptr은 무엇? ( shared_ptr 과 세트라고 보면 된다. )
shared_ptr 이 refCount를 관리해줘서 아무도 기억하지 않을때 자신을 소멸시키지 않는다는 점은 좋은데
전통적인 사이클 문제를 해결 할 수 없다.
k1과 k2가 서로의 존재를 서로 바라보고 있으면 메모리가 어떠한 상황이 일어나도 소멸이 되지 않는 상황이 발생한다.
너는 나를 주시하고 나는 너를 주시하고!
그래서 shared_ptr을 사용할때는 이 사이클 문제에 대해 조심해야한다.
k1->_target = nullptr; 이렇게 쓰지 않는것은 nullptr로 밀어주는 부분이 들어가야된다.
애를 nullptr로 밀어주면 간접적으로 refCount 1을 소멸시켜주는 부분이 코드에서 들어가게 된다.
참고로 강사님이 운영했던 서버 프로젝트에서는 weak_ptr을 쓰지않고 shared_ptr 하나로만 사용했다.
그래서 누군가가 실수로 어떤 플레이어가 몬스터를 바라보고 있고 몬스터도 플레이어를 바라보고 있어서 순환구조로 인해 메모리가 영영 해제되지 않는 부분이 일어났다. 그럼 메모리 사용량이 쭉 늘어나면서 서버가 3-4일 후에 메모리 고갈로 크러쉬가 나온다.
물론 그나마 다행인건 이런 버그들은 찾기가 쉽다.
또 다른 방법은 weak_ptr을 활용하는 방법이다.
shared_ptr에서는 관리하는 포인터와 더불어서 refCount도 관리하고 있었는데 weak_ptr 랑 같이 등장하게 되면은
refCountBlock 에는 WeakCount라는 변수가 하나 더 등장한다.
weak_count는 이런
'C++과 언리얼로 만드는 게임 개발 > Part1. C++ 문법' 카테고리의 다른 글
Day13 ( 84.82% ) (0) | 2022.04.26 |
---|---|
Day12 ( 82.14% ) (0) | 2022.04.20 |
Day11 ( 80.36% ) (0) | 2022.04.17 |
Day10 ( 79.46% ) (0) | 2022.04.17 |
Day9 ( 71.42% ) (0) | 2022.04.04 |