반응형

 c++ 11이상 부터 람다 표현식이라는 익명의 함수를 호출할 수 있는 기능을 지원합니다. 

 기존의 c++ 문법과는 다르지만 배워 둔다면 매우 편리한 기능이기 때문에 꼭 시간 투자 하셔서 배우시길 바랍니다.

람다 표현식 문법 

msdn의 람다 표현식 그림

[캡쳐 블록](파라미터 목록) 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;   
}
 

lambda 테스트 결과

예제를 보면 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

 

C++의 람다 식

C++의 람다 식Lambda Expressions in C++ 이 문서의 내용 --> C + + 11 이상에서 람다 식 (종종 람다)은 호출 되거나 함수에 인수로 전달 되는 위치에서 무명 함수 개체 ( 클로저)를 정의 하는 편리한 방법입니다.In C++11 and later, a lambda expression—often called a lambda—is a convenient way of defining an anonymous function objec

docs.microsoft.com

https://docs.microsoft.com/ko-kr/cpp/cpp/examples-of-lambda-expressions?view=vs-2019

 

람다 식의 예

람다 식의 예Examples of Lambda Expressions 이 문서의 내용 --> 이 문서에서는 프로그램에 람다 식을 사용하는 방법을 보여 줍니다.This article shows how to use lambda expressions in your programs. 람다 식의 개요를 보려면 람다 식합니다.For an overview of lambda expressions, see Lambda Expressions. 람다 식의 구조에 대 한 자세한

docs.microsoft.com

 

 

반응형

'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

+ Recent posts