오른값 참조 2
오른값 참조 타입을 인자로 받는 Test_RValueRef() 함수가 있다고 가정했을 때,
k4의 경우 왼값이기에 인자로 들어갈 수없는건 이해가 간다.
하지만, k5의 경우 std::move() 함수를 사용하여 오른값 참조 타입으로 만들어 변수에 저장하였는데 에러가 발생한다.
오른값 참조 타입인것과 오른값 인것은 별개의 문제이다.
- 오른값 : 왼값을 제외한 나머지 = 단일식에서 벗어나면 사용 x
- 오른값 참조 : 오른값만 참조할 수 있는 타입
위의 정의를 보면 오른값은 재사용이 가능하면 안된다. 그런데 k5는 위에서 선언한 후 인자로 넣어주면서 재사용이 가능하게 되어 버린것을 알 수 있다.
때문에 정상적으로 사용하기 위해선 인자로 넣어줄 때, 다시 std::move() 함수를 사용해서 오른값으로 변환해줘야한다.
람다 (lambda)
함수 객체를 빠르게 만들어주는 문법
기본 구조 :
람다 식을 "람다 표현식 (Lambda Expression)" 이라고 하며,
람다에 의해 만들어진 실행시점의 객체를 "클로저 (Closure)" 라고 한다.
// 람다 표현식 (Lambda Expression)
[]() {}
// [] : 캡쳐(capture)
// () : 인자값
// {} : 구현부
기본 사용법 :
// 준비
enum class ItemType
{
None,
Armor,
Weapon,
Jewelry,
Consumable, // 소비템
};
enum class Rarity
{
Common,
Rare,
Unique,
};
class Item
{
public:
// 기본생성자
Item() {}
// 생성자1
Item(int itemId, Rarity rarity, ItemType type)
: _itemId(itemId), _rarity(rarity), _type(type)
{
}
public:
int _itemId = 0;
Rarity _rarity = Rarity::Common;
ItemType _type = ItemType::None;
};
std::vector<Item> v;
// 임시 객체를 넣어줬기에 "이동 대입 연산자"에 의해 값이 들어간다.
v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
v.push_back(Item(2, Rarity::Common, ItemType::Armor));
v.push_back(Item(3, Rarity::Rare, ItemType::Jewelry));
v.push_back(Item(4, Rarity::Unique, ItemType::Weapon));
struct IsUniqueItem
{
bool operator()(Item& item)
{
return item._rarity == Rarity::Unique;
}
};
// 클로저 (closure) : 람다에 의해 만들어진 실행시점 객체
auto isUniqueLambda = [](Item& item) { return item._rarity == Rarity::Unique; };
// case1 : 함수 객체 활용
auto findIt1 = std::find_if(v.begin(), v.end(), IsUniqueItem());
// case2 : 람다 활용
auto findIt2 = std::find_if(v.begin(), v.end(), isUniqueLambda);
// case3 : 재상용성 없는 일회성 람다
auto findIt3 = std::find_if(v.begin(), v.end(), [](Item& item) { return item._rarity == Rarity::Unique; });
람다는 타입을 추론한다.
일회성으로 사용하는 경우엔 case3.번과 같이 람다 표현식을 함수의 인자로 바로 넣어줘도 된다.
값(복사) 방식 vs 참조 방식 :
일반적인 경우 구조체나 클래스는 멤버변수를 가지고 있을 것이다.
해당 경우에 대한 함수 객체는 람다에서 어떻게 구현 할 수 있는지 알아보자.
struct FindItemById
{
FindItemById(int itemId) : _itemId(itemId) {}
bool operator()(Item& item)
{
return item._itemId == _itemId;
}
public:
int _itemId;
};
int itemId = 4;
// case1 : 함수 객체
auto findIt2 = std::find_if(v.begin(), v.end(), FindItemById(itemId));
if (findIt2 != v.end())
std::cout << "아이템ID : " << findIt2->_itemId << std::endl;
else
std::cout << "찾을 수 없음" << std::endl;
// case2 : lambda "값(복사) 방식"
//auto findItemByIdLambda = [=](Item& item) { return item._itemId == itemId; };
// case3 : lambda "참조 방식"
auto findItemByIdLambda = [&](Item& item) { return item._itemId == itemId; };
// 참조 방식은 캡쳐에서 참조값을 저장하고 있음을 의미한다.
// 때문에 해당 함수에 값을 전달한 다음 뒤에서 값을 바꾸면 바꾼 값이 적용된다.
itemId = 10;
auto findIt1 = std::find_if(v.begin(), v.end(), findItemByIdLambda);
if (findIt1 != v.end())
std::cout << "아이템ID : " << findIt1->_itemId << std::endl;
else
std::cout << "찾을 수 없음" << std::endl;
람다의 "캡쳐" 영역에 "=" or "&" 기호를 넣어주면 되는데, 각각 값, 참조 방식을 뜻한다.
람다는 외부에 존재하는 변수를 사용할 수 있다는 특징이 있는데, 이때 해당 값을 복사하여 사용할지, 참조하여 사용할지를 캡쳐부분의 기호를 통해 나타내준다.
"=" 기호는 "값(복사) 방식" 으로 말 그대로 값을 복사하여 사용한다. 별다른 특징은 없다.
"&" 기호는 "참조 방식" 인데, 특징으로는 람다식이 선언/정의 된 이후 사용하는 변수의 값이 변경되면 변경된 값을 활용하여 람다식의 결과값을 반환한다는 특징이 있다. -> 어찌보면 참조기 때문에 당연한 결과인것 같다.
일반적인 경우의 람다 활용 예제 :
멤버 변수를 여러개 가지는 경우에 대한 예제코드를 보자
struct FindItem
{
FindItem(int itemId, Rarity rarity, ItemType type)
: _itemId(itemId), _rarity(rarity), _type(type)
{
}
bool operator()(Item& item)
{
return item._itemId == _itemId && item._rarity == _rarity && item._type == _type;
}
public:
int _itemId;
Rarity _rarity;
ItemType _type;
};
int itemId = 4;
Rarity rarity = Rarity::Unique;
ItemType type = ItemType::Weapon;
// 람다 값 복사 방식
auto FindItemLambda = [=](Item& item) {return item._itemId == itemId && item._rarity == rarity && item._type == type; };
auto findIt2 = std::find_if(v.begin(), v.end(), FindItemLambda);
if (findIt2 != v.end())
std::cout << "아이템ID : " << findIt2->_itemId << std::endl;
else
std::cout << "못 찾음\n";
// 함수 객체 방식
auto findIt = std::find_if(v.begin(), v.end(), FindItem(itemId, rarity, type));
if (findIt != v.end())
std::cout << "아이템ID : " << findIt->_itemId << std::endl;
else
std::cout << "못 찾음\n";
람다식에서 외부 변수를 활용하는 경우 실수방지를 위해서 "캡쳐" 부분에 넣어주는 기호(=, &)들 대신에 사용하는 인자를 직접 지정하여 넣어줄 수 있다.
auto FindItemLambda = [=](Item& item) {return item._itemId == itemId && item._rarity == rarity && item._type == type; };
// 값(복사) 방식
auto FindItemLambda = [itemId, rarity, type](Item& item) {return item._itemId == itemId && item._rarity == rarity && item._type == type; };
// 참조 방식
auto FindItemLambda = [&itemId, &rarity, &type](Item& item) {return item._itemId == itemId && item._rarity == rarity && item._type == type; };
각 인자들의 사용방식을 변수마다 다르게 부여할 수 있다.
실수 방지 :
위에서 람다식에서 외부 변수를 사용하는 경우 실수 방지를 위해 캡쳐 영역에 기호 대신에 변수를 직접 선언해준다고 하였는데 그 예제를 보자
// 실수방지
{
class Knight
{
public:
auto ResetHpJob()
{
//auto f = [=]() {_hp = 200; };
auto f = [this]() { this->_hp = 200; };
return f;
}
public:
int _hp = 100;
};
Knight* k = new Knight();
auto job = k->ResetHpJob();
// Knight 객체의 메모리를 해제했기에
delete k;
job();
}
해당 예제를 보면 람다식의 반환값을 저장하는 job 객체가 있다.
람다식을 저장해준 후 아래에서 람다식이 들어있는 Knight 타입 객체 k의 메모리를 해제함으로 인해 job이 가리키고 있는 메모리 영역도 값이 비워지게된다.
즉, 해제하면 안되는 메모리 영역을 해제해버린것....
이러한 실수를 줄이기 위해서 캡쳐 영역에 "=" 대신에 "this" 라고 직접 어떤 값을 사용하고 있는지 알려줌으로써 문제 발생시 좀 더 빨리 확인할 수 있도록 해준다.
스마트 포인터
필요한 이유 :
C++의 장점이자 단점인 양날의 검 "포인터" 메모리 영역을 직접 건들 수 있기에 장점이면서도 단점이 된다.
또한 메모리를 직접 할당하고 해제해줘야 하는데 어느 타이밍에 메모리를 해제해줘야하는지 관리하기 힘든경우가 있다.
이 때문에 "댕글링 포인 (Dangling Pointer)" 와 같이 이미 메모리가 해제되어 해당 위치에 어떤 값도 없지만 그걸 모르고 가리키고 있는 경우도 발생하게된다.
이러한 문제들을 스마트 포인터를 사용하면 해결해줄 수 있다.
최근... :
포인터의 현대적인 사용법은 직접적인 접근보단 스마트 포인터를 활용한 간접적인 접근법이다.
약간의 성능상의 손해를 보더라도 안정성이 더 중요하다.
당장 Unreal 만 보아도 스마트 포인터를 사용한다.
스마트 포인터의 종류 :
- shared_ptr (핵심)
- weak_ptr
- unique_ptr
shared_ptr<T> :
"참조 카운트 (Reference Count)" 를 관리한다.
shared_ptr이 관리하는 포인터가 현재 몇번 참조되고 있는지 계속 추적하며, 더 이상 참조되지 않아 참조 카운트가 0이 되는 순간 메모리 해제한다.
메모리 관리의 부담이 줄어든다는 장점이 생긴다.
make_shared<T>() :
std::shared_ptr<Knight2> k1(new Knight2);
std::shared_ptr<Knight2> k1 = std::make_shared<Knight2>();
new 연산자를 통해 shared_ptr 객체를 생성해도 되지만 make_shared<T>() 함수를 사용하여 생성하는게 성능상 더 좋다. ( 자세한 이유는 잘 모르겠다... )
weak_ptr<T> :
weak_ptr 과 shared_ptr 은 세트관계라고 생각하면 된다.
shared_ptr 는 순환참조가 발생할 수 있는데, 두 객체가 서로를 참조함으로 인해 둘다 메모리 해제를 할 수 없게된다는 문제가 발생한다.
weak_ptr 는 일반 포인터와 shared_ptr 사이에 위치한 스마트 포인터로, 스마트 포인터 처럼 객체를 안전하게 참조할 수 있게 해주지만, shared_ptr 와는 다르게 참조 개수를 늘리지는 않는다. 이름 그대로 약한 포인터
때문에 어떤 객체를 weak_ptr 가 가리키고 있다고 하더라도, 다른 shared_ptr 들이 가리키고 있지 않다면 메모리에서 소멸될 수 있다.
weak_ptr 자체로는 원본 객체를 참조할 수 없고, 반드시 shared_ptr 로 변환해서 사용해야 한다.
이 때 가리키고 있는 객체가 이미 소멸되었다면 빈 shared_ptr 을 변환하고, 아닐경우 해당 객체를 가리키는 shared_ptr 로 변환한다.
class Knight3
{
public:
Knight3() { std::cout << "Knight3 생성" << std::endl; }
~Knight3() { std::cout << "Knight3 소멸" << std::endl; }
void Attack()
{
// expired() : 원본 객체 메모리 해제유무 확인
if (false == _target.expired())
{
// lock() : shared_ptr 객체 얻어오는 함수
std::shared_ptr<Knight3> sptr = _target.lock();
sptr->_hp -= _damage;
std::cout << "HP : " << sptr->_hp << std::endl;
}
}
public:
int _hp = 100;
int _damage = 10;
std::weak_ptr<Knight3> _target;
};
std::shared_ptr<Knight3> k1 = std::make_shared<Knight3>();
std::shared_ptr<Knight3> k2 = std::make_shared<Knight3>();
k1->_target = k2;
k2->_target = k1;
k1->Attack();
// weak_ptr<T> 사용안하려면 아래 작업을 해줘야한다.
//k1->_target = nullptr;
//k2->_target = nullptr;
위의 코드에서 확인할 수 있듯이 weak_ptr 로 원복 객체를 참조하기 위해선 expired() 함수로 원본 객체의 메모리 해제유무를 확인하고, lock() 함수로 shared_ptr 객체를 얻어와야 한다는 번거로움이 있다.
'DirectX11 > Rookiss' 카테고리의 다른 글
함수 객체 (Function Object) (0) | 2024.03.16 |
---|---|
함수 포인터 (0) | 2024.03.16 |
비트 연산 (0) | 2024.03.16 |
Mordern C++ (2) | 2024.03.15 |
[게임 프로그래머 도약반] DirectX11 입문_강의노트 (0) | 2024.03.12 |