c++ 11이상 부터 람다 표현식이라는 익명의 함수를 호출할 수 있는 기능을 지원합니다.
기존의 c++ 문법과는 다르지만 배워 둔다면 매우 편리한 기능이기 때문에 꼭 시간 투자 하셔서 배우시길 바랍니다.
람다 표현식 문법
[캡쳐 블록](파라미터 목록) mutable 익셉션_목록 -> 리턴 타입 { 람다 함수 바디 }
이 름 | 설 명 |
캡쳐 블록 |
람다 함수 안에서 참조할 바깥 변수를 지정합니다. 여기서 지정한 변수를 람다 함수 바디에서 호출 할 수 있습니다. 자세한 설명을 아래에서 진행합니다. |
파라미터 목록 | 람다 함수 호출에서 사용할 파라미터 목록을 지정합니다. (일반적인 함수의 파라미터와 비슷) |
mutable 키워드(생략 가능) |
캡쳐 블록에서 복사한 복제본은 기본적으로 const타입을 가지기 때문에 수정 할 수 없습니다. 하지만 람다 함수에 mutable을 붙이면 const속성이 제거되어 수정 할 수 있습니다. |
익셉션 목록(생략 가능) | 람다 표현식에서 throw할 수 있는 익셉션의 종류를 지정합니다. |
리턴타입(생략 가능) | 람다 표현식의 리턴 타입을 지정합니다. 리턴이 생략되면 컴파일러가 추론해서 지정합니다. |
람다 함수 바디 | 람다 표현식이 실제적으로 수행할 구문입니다. |
// 파라미터를 더하는 함수
int FuncAdd(int iLeft, int iRight)
{
return iLeft + iRight;
}
int _tmain(int argc, _TCHAR* argv[])
{
// FuncAdd를 람다로 표현하면
auto lambdaAdd = [](int iLeft, int iRight) /*-> int 생략 */
{
return iLeft + iRight;
};
std::cout << "FuncAdd(10, 10) = " << FuncAdd(10, 10) << std::endl;
std::cout << "lambdaAdd(10, 10) = " << lambdaAdd(10, 10) << std::endl;
}
예제를 보면 FunAdd 함수를 간단하게 람다로 변환해서 표현할 수 있습니다.
closure(클로저)
람다에 의해 만들어진 실행시점의 객체를 closure라고 합니다.
// lambdaAdd는 closure에 의해서 생성된 closure의 복사본입니다.
auto lambdaAdd = [](int iLeft, int iRight) /*-> int 생략 */
{
return iLeft + iRight;
};
// lambdaAdd2는 lambdaAdd의 복사본입니다.
auto lambdaAdd2 = lambdaAdd;
// lambdaAdd3는 lambdaAdd2의 복사본입니다.
auto lambdaAdd3 = lambdaAdd2;
캡쳐 블록
람다의 캡쳐 블록에 대해서 자세히 알아 봅니다.
람다는 호출되는 스코프 범위에서 존재하는 변수들을 캡쳐 할 수 있습니다. 그리고 대상을 값으로 캡쳐 할 것인지
참조로 캡쳐할 것인지 설정 할 수 있습니다.
Scope내의 모든 변수 캡쳐
- [=]
- 스코프 내의 모든 변수를 값 복사 합니다.
- 멤버함수에서 람다가 호출 된다면 this 포인터도 전달됩니다.
- [&]
- 스코프 내의 모든 변수를 참조로 복사합니다.
DWORD dwValue = 100;
DWORD dwCount = 10;
DWORD dwTotal;
// 캡쳐 없이 외부 변수를 사용하려고 함 에러!! C3493
auto lambdaCapture = []()
{
dwTotal = dwValue * dwCount;
return dwTotal;
};
// 값으로 외부 변수를 캡쳐합니다.
// 값으로 캡쳐 된 값은 기본적으로 const로 지정되서 수정할 수 없습니다. C3493
auto lambdaCaptureValue = [=]()
{
dwTotal = dwValue * dwCount;
return dwTotal;
};
// 레퍼런스로 외부 변수를 캡쳐합니다.
// 참조로 캡쳐했기 때문에 dwTotal의 값이 변경됩니다.
auto lambdaCaptureReference = [&]()
{
dwTotal = dwValue * dwCount;
return dwTotal;
};
여기서 확인 할 점은 값으로 캡쳐되는 값은 const로 캡쳐된다는 점입니다. 기본적으로 수정이 되지 않습니다.
auto lambdaCaptureValue = [=]() mutable
{
dwTotal = dwValue + dwCount;
return dwTotal;
};
혹시라도 값으로 캡쳐된 값을 수정하시려면 위에서 말씀 드린 mutable을 사용하시면 됩니다.
하지만 값으로 캡쳐한 변수를 수정하는 건 일반적인 상황은 아닙니다.
다른 방법으로 충분히 구현 가능하기 때문에 한번 더 설계를 생각 해 보시는게 좋습니다.
Scope내의 일반 변수 캡쳐
- [&x]
- 변수 x만 참조로 캡쳐합니다. 다른 변수는 캡쳐하지 않습니다.
- [x]
- 변수 x만 값을 캡쳐합니다. 다른 변수는 캡쳐하지 않습니다.
- [=, &x]
- 모든 변수는 기본으로 값으로 캡쳐하고 x변수만 참조로 캡쳐합니다.
- [&, x]
- 모든 변수는 기본으로 참조로 캡쳐하고 x변수만 값으로 캡쳐합니다.
- [this]
- 클래스 멤버 함수인 경우 자기 자신을 전달합니다. ( [=]로도 전달 됨 )
캡쳐는 람다가 생성되는 scope 안에서 보이는 지역 변수에만 적용됩니다.
(static 이나 전역변수는 따로 정의하지 않아도 접근 가능)
Generalized capture
c++14에서는 스코프 범위에 해당 변수가 없어도 캡쳐 절에서 새 변수를 도입하고 초기화 할 수 있습니다.
DWORD dwValue = 100;
DWORD dwCount = 10;
auto lambdaCaptureValue =
// Total이라는 새로운 값을 지정해서 사용
[Total = (dwValue * dwCount)]()
{
return Total;
};
이 기능을 사용하면 이동 전용 변수(unique_ptr)을 캡쳐해서 사용 할 수 있다는 점입니다.
auto pNums = make_unique<vector<int>>(nums);
// unique_ptr을 캡쳐로 전달할 수 있습니다.
auto a =
[ptr = std::move(pNums)]()
{
// use ptr
};
캡쳐 블록 사용시 주의 할 점
람다에서 참조로 캡쳐된 변수를 수정하면 원본도 같이 수정됩니다.
기본 캡쳐를 사용해서 전체를 값이나 참조로 캡쳐하는 것 보다는 명시적으로 캡쳐하는 것을 권장합니다.
(특히 공동으로 작업하는 프로젝트에서는 다른 작업자가 코드를 수정 할 수 있기 때문에 더욱 중요)
람다에서 참조로 변수를 캡쳐한 람다가 실행되는 시점까지 유효해야 합니다. 람다는 수행시점과 선언 시점이 다르기
때문에 주의해서 사용 해야 합니다.
std::function<void()> lambdaCapture;
{
std::shared_ptr<DWORD> pdw = std::make_shared<DWORD>(10);
// pdw를 참조로 캡쳐합니다.
lambdaCapture = [&pdw]()
{
std::cout << *pdw << std::endl;
};
}
// 수행시점에 pdw은 비정상적인 값을 가집니다.
lambdaCapture();
값으로 캡쳐한다고 하더라고 포인터를 대상으로 하거나 대상 객체 멤버 중에 포인터도 된 변수를
람다식에서 사용한다면 수행 시점까지 유효함을 지킬 수 있도록 설계 해야 합니다.
class SomeClass
{
//... 생략
public :
// 함수객체를 리턴합니다.
std::function<int(const int)> GetAddFunc()
{
// [value=value](const int iadd) c++ 14부터는
// Generalized capture를 지원하기 때문에 해당 문제를 해결할 수 있습니다.
return [=](const int iadd)
{
// value는 실제로는 this->value 입니다.
return value + iadd;
};
}
private :
int value = 1;
};
// SomeClass를 통한 함수 사용
{
// SomeClass 할당
SomeClass * s = new SomeClass;
// f에 Closure의 복사본을 저장합니다.
auto f = nullptr != s ? s->GetAddFunc() : nullptr;
// SomeClass 할당 해제
delete s;
// 비정상적인 값 출력
if(f)
std::cout << f(10) << std::endl;
}
위의 예제에서는 값으로 전체를 캡쳐해서 SomeClass의 this가 캡쳐되었습니다.
람다가 수행될 때 SomeClass 객체가 유지되지 않기 때문에 비 정상적인 동작이 발생하는 것을 확인 할 수 있습니다.
파라미터 목록
람다는 캡쳐 이외에도 매개변수를 사용 할 수 있습니다. 일반적으로는 함수의 매개 변수와 비슷합니다.
c++ 14부터는 auto키워드를 파라미터로 지정할 수 있습니다. 이렇게 하면 함수 호출을 템플릿으로 만들도록
컴파일러에 알립니다.
// auto로 구성된 lambda
auto GenericLambda = [](auto first, auto second)
{
return first + second;
};
// int로 추론됨
auto value = GenericLambda(1, 3);
std::cout << value << std::endl;
std::cout << "value typeid(" << typeid(value).name() << ")" << std::endl;
// float로 추론됨
auto value2 = GenericLambda(11.1f, 201.2f);
std::cout << value2 << std::endl;
std::cout << "value2 typeid(" << typeid(value2).name() << ")" << std::endl;
// std::string으로 추론됨
auto value3 = GenericLambda(std::string("i am "), std::string("jung woong"));
std::cout << value3 << std::endl;
std::cout << "value3 typeid(" << typeid(value3).name() << ")" << std::endl;
Function 클래스와 사용
stl의 함수 객체 래퍼 클래스인 std::function을 지원하며 lambda 표현식을 사용할 때 유용하게 사용 할 수 있습니다.
기본적인 사용법은 간단합니다.
std::function<리턴타입(파라미터)> 형식으로 선언합니다.
// 함수 객체
class FuncObject
{
public :
int operator()()
{
return 2;
}
};
// 함수 포인터
int Func() { return 1; }
// 리턴 타입 int, 파라 미터 : void
// 함수 포인터 대입
std::function<int()> function1 = Func;
// 함수 객체 대입
function1 = FuncObject();
// 람다 표현식 대입
function1 = [](){return 2; };
// 리턴 타입 int, 파라 미터 : int
std::function<int(int)> function2 =
[](int input) {return 2 + input; };
파라미터로 람다 표현식 사용
람다 표현식을 파라미터로 받는 함수도 만들 수 있는데 콜백 구현에 많이 사용됩니다.
// callbackfun 콜백함수 전달 합니다.
int CallBackFun(const std::vector<int> & vec,
const std::function<bool(int)> callbackfun)
{
for (auto i : vec)
{
if (!callbackfun(i))
break;
std::cout << "It's OK = " << i << std::endl;
}
}
std::vector<int> vecCon = { 1,2,3,4,5,6,7,8,9 };
// 콜백함수를 람다를 통해서 호출합니다.
CallBackFun(vecCon, [](int val)
{
return val < 5;
});
또한 stl의 알고리즘과 람다 표현식을 함께 사용하기도 합니다.
std::vector<int> vecCon = { 1,2,3,4,5,6,7,8,9 };
// for_each를 호출시 람다로 간단히 호출
std::for_each(vecCon.begin(), vecCon.end(),
[](int & value)
{
std::cout << "for_each print " << value << std::endl;
});
MSDN의 람다관련 페이지
람다에 관한 더 자세한 내용을 MSDN의 문서를 참조하시길 바랍니다.
https://docs.microsoft.com/ko-kr/cpp/cpp/lambda-expressions-in-cpp?view=vs-2019
https://docs.microsoft.com/ko-kr/cpp/cpp/examples-of-lambda-expressions?view=vs-2019
'c++ > modern c++' 카테고리의 다른 글
[c++] std::move와 std::forward (4) | 2020.03.07 |
---|---|
[c++] Move semantics (0) | 2020.03.07 |
[c++] weak_ptr (1) | 2020.03.02 |
[c++] shared_ptr (0) | 2020.02.29 |
[c++] unique_ptr (0) | 2020.02.27 |