함수 객체를 이용한 기존 방식과 람다를 사용하는 방식을
비교하며 차이점과 람다의 장점을 알아보자
기존에 함수객체를 이용하는 방식은 아래와 같았다
enum class Rarity
{
Common,
Rare,
Unique,
};
enum class ItemType
{
None,
Armor,
Weapon,
Jewerly,
Consumable,
};
class Item
{
public:
Item()
{
}
Item(int itemId, Rarity rarity, ItemType type)
: mItemId(itemId)
, mRarity(rarity)
, mType(type)
{
}
public:
int mItemId = 0;
Rarity mRarity = Rarity::Common;
ItemType mType = ItemType::None;
};
int main()
{
std::vector<Item> v;
v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
v.push_back(Item(1, Rarity::Common, ItemType::Armor));
v.push_back(Item(1, Rarity::Rare, ItemType::Jewerly));
v.push_back(Item(1, Rarity::Unique, ItemType::Weapon));
{
struct IsUniqueItem
{
bool operator()(Item& item)
{
return item.mRarity == Rarity::Unique;
}
};
auto findIt = std::find_if(v.begin(), v.end(), IsUniqueItem());
if (findIt != v.end())
std::cout << "아이템 ID: " << findIt->mItemId << std::endl;
}
}
lambda를 사용하는 방식은 아래와 같다
enum class Rarity
{
Common,
Rare,
Unique,
};
enum class ItemType
{
None,
Armor,
Weapon,
Jewerly,
Consumable,
};
class Item
{
public:
Item()
{
}
Item(int itemId, Rarity rarity, ItemType type)
: mItemId(itemId)
, mRarity(rarity)
, mType(type)
{
}
public:
int mItemId = 0;
Rarity mRarity = Rarity::Common;
ItemType mType = ItemType::None;
};
int main()
{
std::vector<Item> v;
v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
v.push_back(Item(1, Rarity::Common, ItemType::Armor));
v.push_back(Item(1, Rarity::Rare, ItemType::Jewerly));
v.push_back(Item(1, Rarity::Unique, ItemType::Weapon));
{
// 비교를 위해
// 함수 객체를 이용한 예전 방식을 작성해두었다
struct IsUniqueItem
{
bool operator()(Item& item)
{
return item.mRarity == Rarity::Unique;
}
};
// 람다 표현식을 이용한 클로저(closure)
// 클로저(closure): 람다에 의해 만들어진 실행시점 객체
auto IsUniqueLambda = [](Item& item)
{
return item.mRarity == Rarity::Unique;
};
auto findIt1 = std::find_if(v.begin(), v.end(), IsUniqueLambda);// 한번 거치는 버전
auto findIt2 = std::find_if(v.begin(), v.end(), [](Item& item) { return item.mRarity == Rarity::Unique;});// 바로 사용하는 버전
if (findIt1 != v.end())
std::cout << "아이템 ID: " << findIt1->mItemId << std::endl;
}
}
참고로 return type을 지정하려면 아래와 같이
"-> int"와 같은 형식으로 작성하면 된다
auto IsUniqueLambda = [](Item& item) -> int
{
return item.mRarity == Rarity::Unique;
};
auto findIt1 = std::find_if(v.begin(), v.end(), IsUniqueLambda);// 한번 거치는 버전
auto findIt2 = std::find_if(v.begin(), v.end(), [](Item& item) -> int { return item.mRarity == Rarity::Unique;});// 바로 사용하는 버전
함수객체에 데이터가 있는 경우에도 람다를 사용할 수 있다
기존에 함수객체에서는 아래와 같이 사용했었다
enum class Rarity
{
Common,
Rare,
Unique,
};
enum class ItemType
{
None,
Armor,
Weapon,
Jewerly,
Consumable,
};
class Item
{
public:
Item()
{
}
Item(int itemId, Rarity rarity, ItemType type)
: mItemId(itemId)
, mRarity(rarity)
, mType(type)
{
}
public:
int mItemId = 0;
Rarity mRarity = Rarity::Common;
ItemType mType = ItemType::None;
};
int main()
{
std::vector<Item> v;
v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
v.push_back(Item(1, Rarity::Common, ItemType::Armor));
v.push_back(Item(1, Rarity::Rare, ItemType::Jewerly));
v.push_back(Item(1, Rarity::Unique, ItemType::Weapon));
{
struct FindItemByItemId
{
FindItemByItemId(int itemId)
: mItemId(itemId)
{
}
bool operator()(Item& item)
{
return item.mItemId == mItemId;
}
int mItemId;
};
int itemId = 4;
auto findIt = std::find_if(v.begin(), v.end(), FindItemByItemId(itemId));
if (findIt != v.end())
std::cout << "아이템 ID: " << findIt->mItemId << std::endl;
}
}
위의 상황을 람다를 사용해서 만든다면 아래와 같다
enum class Rarity
{
Common,
Rare,
Unique,
};
enum class ItemType
{
None,
Armor,
Weapon,
Jewerly,
Consumable,
};
class Item
{
public:
Item()
{
}
Item(int itemId, Rarity rarity, ItemType type)
: mItemId(itemId)
, mRarity(rarity)
, mType(type)
{
}
public:
int mItemId = 0;
Rarity mRarity = Rarity::Common;
ItemType mType = ItemType::None;
};
int main()
{
std::vector<Item> v;
v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
v.push_back(Item(1, Rarity::Common, ItemType::Armor));
v.push_back(Item(1, Rarity::Rare, ItemType::Jewerly));
v.push_back(Item(1, Rarity::Unique, ItemType::Weapon));
{
// 비교를 위해
// 함수 객체를 이용한 예전 방식을 작성해두었다
struct FindItemByItemId
{
FindItemByItemId(int itemId)
: mItemId(itemId)
{
}
bool operator()(Item& item)
{
return item.mItemId == mItemId;
}
int mItemId;
};
int itemId = 4;
// [ ] 캡쳐(capture): 함수 객체 내부에 변수를 저장하는 개념과 유사
// 기본적으로
// 값(복사) 방식으로 하려면 [=]
// 참조 방식으로 하려면 [&]
auto FindItemIdLambda = [=](Item& item) { return item.mItemId == itemId; };
auto findIt = std::find_if(v.begin(), v.end(), FindItemIdLambda);
if (findIt != v.end())
std::cout << "아이템 ID: " << findIt->mItemId << std::endl;
}
}
함수객체에 데이터가 여러 개 있는 경우에
람다 없이는 아래와 같이 사용했었다
enum class Rarity
{
Common,
Rare,
Unique,
};
enum class ItemType
{
None,
Armor,
Weapon,
Jewerly,
Consumable,
};
class Item
{
public:
Item()
{
}
Item(int itemId, Rarity rarity, ItemType type)
: mItemId(itemId)
, mRarity(rarity)
, mType(type)
{
}
public:
int mItemId = 0;
Rarity mRarity = Rarity::Common;
ItemType mType = ItemType::None;
};
int main()
{
std::vector<Item> v;
v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
v.push_back(Item(1, Rarity::Common, ItemType::Armor));
v.push_back(Item(1, Rarity::Rare, ItemType::Jewerly));
v.push_back(Item(1, Rarity::Unique, ItemType::Weapon));
{
// 함수 객체를 이용한 예전 방식
struct FindItem
{
FindItem(int itemId, Rarity rarity, ItemType type)
: mItemId(itemId)
, mRarity(rarity)
, mType(type)
{
}
bool operator()(Item& item)
{
return item.mItemId == mItemId && item.mRarity == mRarity && item.mType == mType;
}
int mItemId;
Rarity mRarity;
ItemType mType;
};
auto findIt = std::find_if(v.begin(), v.end(), FindItem(4, Rarity::Unique, ItemType::Weapon));
if (findIt != v.end())
std::cout << "아이템 ID: " << findIt->mItemId << std::endl;
}
}
위와 같이 여러 데이터를 가지는 경우 람다를 사용하면
아래처럼 매우 편하게 사용 가능하다
enum class Rarity
{
Common,
Rare,
Unique,
};
enum class ItemType
{
None,
Armor,
Weapon,
Jewerly,
Consumable,
};
class Item
{
public:
Item()
{
}
Item(int itemId, Rarity rarity, ItemType type)
: mItemId(itemId)
, mRarity(rarity)
, mType(type)
{
}
public:
int mItemId = 0;
Rarity mRarity = Rarity::Common;
ItemType mType = ItemType::None;
};
int main()
{
std::vector<Item> v;
v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
v.push_back(Item(1, Rarity::Common, ItemType::Armor));
v.push_back(Item(1, Rarity::Rare, ItemType::Jewerly));
v.push_back(Item(1, Rarity::Unique, ItemType::Weapon));
{
int itemId = 4;
Rarity rarity = Rarity::Unique;
ItemType type = ItemType::Weapon;
auto findItemLambda = [=](Item& item)
{
return item.mItemId == itemId && item.mRarity == rarity && item.mType == type;
};
auto findIt = std::find_if(v.begin(), v.end(), findItemLambda);
if (findIt != v.end())
std::cout << "아이템 ID: " << findIt->mItemId << std::endl;
}
}
람다의 변수마다 캡처 모드를 지정해서 사용 가능하며
아래처럼 다양하게 사용가능하다
auto findItemLambda = [=](Item& item)// 모든 변수 값(복사) 방식
{
return item.mItemId == itemId && item.mRarity == rarity && item.mType == type;
};
auto findItemLambda2 = [&](Item& item)// 모든 변수 참조 방식
{
return item.mItemId == itemId && item.mRarity == rarity && item.mType == type;
};
auto findItemLambda3 = [=, &type](Item& item)// 기본적으로 값(복사) 방식 사용하고 type만 참조 방식
{
return item.mItemId == itemId && item.mRarity == rarity && item.mType == type;
};
auto findItemLambda4 = [&itemId, rarity, &type](Item& item)// 하나 하나씩 지정해주는 방식
{
return item.mItemId == itemId && item.mRarity == rarity && item.mType == type;
};
// 위와 같은 하나씩 지정해주는 방식을 지향하는 것이 좋다
// 가독성, 어떤 방식을 쓰는지 알고 있어야 문제 발생 확률을 줄임
// 특히, 참조 방식은 매우 위험하기 때문이다
물론, 값 복사 방식이더라도 위험이 발생할 가능성은 있다
아래를 통해 알아보자
class Knight
{
public:
auto ResetHpJob()
{
auto f = [=]()
{
mHp = 200;
};
return f;
}
public:
int mHp = 100;
};
Knight* k = new Knight();
auto job = k->ResetHpJob();
delete k;// delete를 해놓고
job();// 람다로 만들어진 함수 객체를 실행해버리는 대참사 발생
혹시나 위의 예시에서 Knight의 함수 객체가 이해가 안 된다면
C++의 콜백함수와 함수 객체에 대해 공부하고 오는 것이 좋다
간단하게 방금 위의 예시에서 만든 함수 객체는 아래와 같은 의미이다
class Functor
{
public:
Functor(Knight* k)// 주소를 받아놓고
: mKnight(k)
{
}
void operator()()// ()연산자를 실행하는 것
{
mKnight->mHp = 200;
}
public:
Knight* mKnight;
};
다시 돌아와서,
[&], [=]처럼 작성을 해두면 우리가 파악하기 힘들고
어떤 결과를 초래할지 예측하기 힘들어지기 때문에
의도적으로 [ ] 안에 다양한 변수들을 하나씩 다 캡처 모드를 작성해 주거나
아래의 예시처럼 작성해 두는 것이 좋다
class Knight
{
public:
auto ResetHpJob()
{
auto f = [this]()// this를 의도적으로 작성
{
//mHp = 200;
this->mHp = 200;// 멤버변수에 접근할 때, this 포인터는 생략되어 있지만 의도적으로 작성
};
return f;
}
public:
int mHp = 100;
};
Knight* k = new Knight();
auto job = k->ResetHpJob();
delete k;
job();
// 위에서 의도적으로 this를 작성해둠으로써
// delete 되었는데 함수 객체를 사용하는 것이 위험하다는 사실을 인지하도록 만드는 것이 좋다
// 물론 안전장치의 역할은 아니다 벽에 붙여진 안전 수칙 같은 느낌이다
'C++ > [루키스] Modern C++' 카테고리의 다른 글
[MC++] 스마트 포인터 (Smart pointer) (0) | 2023.07.03 |
---|---|
[MC++] 전달 참조 (Forwarding reference) (0) | 2023.07.02 |
[MC++] 오른 값 참조 (Rvalue reference)와 std::move (0) | 2023.07.02 |
[MC++] override, final (0) | 2023.07.01 |
[MC++] delete (0) | 2023.07.01 |