ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Lambda(람다)
    Modern C++/Lambda 2020. 7. 15. 02:52

    1. Lamda Syntax Overview

    람다는 클로져(closure)를 만들어내는 표현식(expression)으로 보통 람다 표현식 또는 람다 함수라고 부른다. 

    클로져는 범위 내 변수를 캡쳐 할 수 있는 이름없는 함수 객체이다.

    람다 표현식 정의

    
    [] (int x, int y) -> int {return x + y; }
    //후행 반환 타입은 해당 람다의 본문의 반환 값을 통해서 추론 가능해 생략 가능하다. 
    [] (int x, int y) { return x + y; }
    
    []() { std::cout << "hello world"; }
    //함수 인자 목록이 없고 지정자나 예외 명세등이 없이 바로 본문이 오면 함수 인자 목록은 생략가능하다. 
    [] { std::cout << "hello world";}
    
    [&x]() mutable { x = 5; };
    //함수 인자와 본문 사이에 mutable 지정자가 있으므로 아래와 같이 생략 불가
    [&x] mutable { x= 5;} 
    
    [x, y]() -> int { return x * y ;}
    //함수 인자와 본문 사이 후행 반환 타입이 있으므로 아래와 같이 생략 불가 
    [x, y] -> int { return x * y; }
    
    

    captures : 외부 변수를 람다의 본문(body)에서 사용할 있게 캡쳐할  변수 목록

    tparams : 템플릿 가변 인자 리스트 

    params : 함수 인자 목록

    specifier :  mutable, constexpr(c++17), consteval(c+++20) 

    exception : 예외 명세 설정 함수 호출 연산자의 예외명세를 설정 

    ret : 반환 값 

    requires : 제약사항 추가 

     

    일반적인 람다 함수의 경우 captures, params, specifier, ret 정도만 사용한다. 

     

    2. 람다와 클로져  

     C++11 이전에 사람을 나이 순으로 정렬하기 위해 std::sort() 함수를 이용할 때  객체를 하나 생성하고 함수 연산자를 재정의해서 이를 넘겼다. 

    #include <iostream>
    #include <vector>
    
    struct Person {
        std::string name;
        int age;
    };
    
    struct by_age {
        // 함수 호출 연산자 재정의
        bool operator()(const Person &a, const Person &b) const {
            return a.age <= b.age;
        }
    };
    
    int main() {
        std::vector<Person> people{
                {"a", 20},
                {"b", 30},
                {"c", 5}
        };
        //by_age객체를 따로 정의하고 해당 생성자를 predicate에 전달
        std::sort(people.begin(), people.end(), by_age());
    
        std::vector<Person>::iterator iter;
        for(iter = people.begin() ; iter != people.end(); ++iter) {
            std::cout << iter->name << " ";
        }
        std::cout << "\n";
        return 0;
    }

    이를 동일하게 람다를 통해서 아래와 같이 구현 가능하다. 

    int main() {
        std::vector<Person> people{
                {"a", 20},
                {"b", 30},
                {"c", 5}
        };
        //predicate 를 람다로 직접 전달 
        std::sort(people.begin(), people.end(), [](const Person& p0, const Person& p1) {
            return p0.age <= p1.age;
        });
        
        for(const auto& p: people) {
            std::cout << p.name << " ";
        }
        std::cout << "\n";
        
        return 0;
    }

    람다는 불필요한 클래스를 정의하지 않고 함수 내에서 정의하고  직접 호출 할 수 있고  객체와 동일하게 함수의 파라미터로 코드를 전달 할 수 있다.  따라서 코드가 간략해 지고 가독성이 좋아진다. 또한 컴파일러는 람다의 최적화를 수행해 람다 사용에 대한 비용이 들지 않는다.  (const Person& p0, const Person& p1) {...} 은 아래와 같은 객체를 생성하는 데 이를 클로져라 한다. 

    (여기서는 임으로 클래스 이름을 anonymous_type 이라고 가정한다.) 

    struct anonymous_type {
    	//함수 호출 연산자는 기본적으로 const 함수
        auto operator() (const Person& p0, const Person& p1) const {
            return p0.age <= p1.age;
        }
    };

     

    3. 람다 캡쳐 

    람다는 자신의 범위 내에서 접근 가능한 변수를 람다 내부에서 사용할 수 있도록 [] 내에 변수 이름을 전달할 수 있는 데 이를 캡쳐(capture)라 한다.

    #include <iostream>
    #include <vector>
    
    int main() {
        std::string message = "hello world";
        //lambda 는 message를 캡쳐해 본문(body)에서 사용할 수 있다.
        //message는 복사를 통해 람다 본문에서 사용하므로 두 message는 다른 객체 임   
        auto lambda = [message]{ std::cout << message; };
        lambda();
        return 0;
    }

     캡쳐는 [ ] 사이에 캡쳐할 사이에 변수를 콤마(,)로 구분하여 캡처할 목록을 지정할 수 있다. 여기서는 message를 캡쳐하는 데 기본적으로  캡쳐는 값 복사를 통해서 이루어진다.

     

    위 람다는 아래와 같은 이름 없는 함수 객체를 생성하는 데 아래에서 보는 것과 같이 캡쳐한 값을 저장할 멤버 변수가 만들어지고 멤버 변수는 생성자의 인자로 전달된다. 기본적으로 캡쳐는 값을 복사해서 이루어진다. 

    struct anonymous_type {
        //캡쳐할 값을 저장할 변수 
        std::string message;
        //캡쳐한 값은 함수 객체의 생성자로 복사를 통해서 전달 
        anonymous_type(std::string message) : message(message) {}
        //함수 호출 연산자는 const 함수로 멤버 변수를 변경할 수 없다. 
        auto operator () () const {
            std::cout << message; 
        }
    };
    
    anonymous_type lambda(message);
    lambda();

     

     람다 본문 내 에서  캡쳐된  외부 변수 값을 변경하고 자 할 때는 참조를  통해 캡쳐를 해야하는 데 이때 변수 이름 앞에 &를 사용한다. 

    int main() {
        std::string message = "Hello world";
        //message는 참조를 통한 캡쳐로 이제 람다 본문에서 수정 가능 
        auto lambda = [&message](){ message = "Bye world";};
        lambda();
        std::cout << message << std::endl; //"Bye world" 출력
        return 0;
    }

    참조를 통한 캡쳐는 참조 멤버 변수를 만들고 생성자를 통해서 전달된다.  

    struct anonymous_type {
        std::string& message;
        //생성자는 message를 참조 
        anonymous_type(std::string& message) : message(message) {}
        auto operator () () const {
            message = "Bye world"; 
        }
    };

    람다는 외부 변수를 캡쳐하기 위해 여러 구문을 지원한다. 

    [ = ]  람다 가시 범위 내 모든 외부 변수를 값으로 갭쳐한다
    [ & ]  람다 가시 범위 내 모든 외부 변수를 참조로 캡쳐한다. 
    [ =, &a ]  람다 가시 범위 내 a는 참조로 나머지 변수들은 값으로 캡쳐한다. 
    [ &, a ] 람다 가시 범위 내 a는 값으로 나머지는 참조로 캡쳐한다. 
    #include <iostream>
    int main() {
        int a = 10, b = 20, c = 30;
        //1. 람다 가시 범위 내 모든 값을 복사
        auto capture_all_by_copy = [=] (int x, int y){
            return a*x + b*y + c;
        };
        std::cout << capture_all_by_copy(10, 20) << std::endl;
    
        //2. 람다 가시 범위 내 모든 값을 참조
        auto capture_all_by_referece = [&]( int x, int y) {
            a = x;
            b = y;
        };
        capture_all_by_referece(2, 3);
        std::cout << "a= " << a << ", b= " << b << std::endl;
    
        //3. a 참조로 나머지는 값으로 복사
        auto caputre_by_capy_without_a = [=, &a]() mutable {
            a = 0;
            b = 10;
            c = 30;
        };
        caputre_by_capy_without_a();
        //b 와 c 는 복사된 값으로 변경되지 않음 
        std::cout << "a= " << a << ", b= " << b << ", c= " << c << std::endl;
        return 0;
    }

    3번째 예에서 복사된 값을 변경하기 위해 mutable 지정자가 설정되어 있음을 알 수 있다. 이는 람다 함수는 기본적으로 함수 호출 연산을 

    const 함수로 만들어 내기 때문인데 이는 복사로 캡쳐된 변수 값은 멤버 변수가 되기 때문에 이를 const 함수에서 변경할 수 없기 때문이다. 

        // 람다함수는 암묵적으로 const라 멤버 변수를 변경할 수 없으므로
        // 컴파일 에러가 발생한다. 
        auto caputre_by_capy_without_a = [=, &a]() {
            a = 0;
            b = 10;
            c = 30;
        };

     

    댓글

Designed by Tistory.