#16. [모던 C++] 람다 표현식(C++11, C++14, C++17, C++20)
- [MEC++#31] 기본 람다 캡쳐 모드를 피하라.(람다 캡쳐 참고)
 - [MEC++#32] 객체를 클로저 안으로 이동시키려면 람다 캡쳐 초기화를 사용하라.(클로저에 이동 연산 전달, 람다 캡쳐 초기화 참고)
 - [MEC++#33] 일반화된 람다 표현식에서 forward()를 통해서 전달 참조 할 경우 decltype()을 사용하라.(일반화된 람다 표현식 참고)
 
- (C++11~) 람다 표현식이 추가되어 1회용 익명 함수를 만들 수 있습니다.
 - (C++14~) 람다 캡쳐 초기화가 추가되어 람다 표현식내에서 사용하는 임의 변수를 정의하여 사용할 수 있습니다.
 - (C++14~) 일반화된 람다 표현식이 추가되어 auto를 인자로 받아 마치 함수 템플릿처럼 사용할 수 있습니다.
 - (C++17~) 람다 캡쳐시 *this가 추가되어 개체 자체를 복제하여 사용할 수 있습니다.
 - (C++17~) constexpr 람다 표현식이 추가되어 람다 표현식도 컴파일 타임 함수로 만들 수 있습니다.
 - (C++20~) 람다 표현식에서 템플릿 인자를 지원합니다.
 - (C++20~) 람다 캡쳐에서 파라메터 팩을 지원합니다.
 - (C++20~) 람다 캡쳐에서 구조화된 바인딩을 지원합니다.
 - (C++20~) 상태없는 람다 표현식의 기본 생성과 복사 대입을 지원합니다.
 - (C++20~) 미평가 표현식에서도 람다 표현식을 허용하기 때문에 decltype()안에서 사용할 수 있습니다.
 - (C++20~) 람다 캡쳐에서 [=] 사용시 this의 암시적 캡쳐가 deprecate되었으므로 명시적으로 작성해야 합니다.
 
개요
기존에는 함수자를 이용하여 함수를 개체화 했는데요(함수자 참고),
C++11 부터는 람다 표현식을 추가하여 함수 지향 프로그래밍이 좀 더 간편해 졌습니다.
람다 표현식은 prvalue 타입(값 카테고리 참고)의 1회용 익명 함수자를 만들며, 이를 클로저 라고 합니다.
람다 표현식
람다 표현식은 STL의 알고리즘과 잘 호환될 뿐만 아니라, 람다 캡쳐 기능등의 활용도도 높아서 복잡하게 함수자를 만들기 보다는 람다 표현식을 사용하는게 훨씬 좋습니다.
람다 표현식의 기본 문법은 다음과 같습니다.
[람다 캡쳐](인자들) -> 리턴 타입 {표현식}
예를 들면,
1
[](int a, int b) -> int {return a + b;}
이는 다음의 함수 정의와 같습니다.
1
2
3
int f(int a, int b) {
    return a + b;
}
인자나 리턴값이 없다면 다음과 같이 생략하여 [] {} 로 작성할 수 있습니다.
1
[] {std::cout << "lambda" << std::endl;}
클로저(Closure)
람다 표현식으로 런타임에 생성된 개체입니다. 람다 표현식 정의시 임시 개체로 만들어 집니다.
람다 표현식 리턴 생략
후행 리턴이 생략되면, 함수의 리턴값에 의해 추론됩니다.(리턴 타입 추론 참고)
리턴값이 없으면 void로 추론됩니다.
1
[](int a, int b) {return a + b;} // a + b의 결과 타입으로 리턴 타입이 추론됨
클로저 호출(람다 표현식 실행)
일반 함수를 호출하려면 f(10, 20) 와 같이 호출하는데요, 람다 표현식으로 작성된 클로저를 호출하려면 다음과 같이 합니다.
- 
    
람다 표현식으로부터 생성된 클로저를 변수에 저장한뒤 호출할 수 있고,
1 2 3 4 5
auto f_11{ [](int a, int b) -> int {return a + b;} }; int val{f_11(10, 20)}; // 함수 호출하듯이 f_11(10, 20) 로 호출합니다. EXPECT_TRUE(val == 30);
 - 
    
람다 표현식에
()을 붙여 바로 클로저를 호출할 수 있습니다.1 2 3 4
int val_11{ [](int a, int b) -> int {return a + b;}(10, 20) // 람다 표현식에 (10, 20)을 붙여 호출합니다. }; EXPECT_TRUE(val_11 == 30);
 
람다 캡쳐
람다 표현식 바깥 부분의 개체 정보들을 람다 표현식 내부에 전달합니다.
| 항목 | 내용 | 
|---|---|
[] (C++11~) | 
      아무것도 람다 캡쳐하지 않습니다. | 
[=] (C++11~) | 
      외부의 모든 변수를 const형 값으로 가져옵니다. 암시적으로 this도 캡쳐합니다. (C++20~) 람다 캡쳐에서 [=] 사용시 this의 암시적 캡쳐가 deprecate되었으므로 명시적으로 작성해야 합니다.  | 
    
[&] (C++11~) | 
      외부의 모든 변수의 참조자를 가져옵니다. 암시적으로 this도 캡쳐합니다.  | 
    
[=, &x, &y] (C++11~) | 
      외부의 모든 변수를 값으로 가져오되 x, y 만 참조로 가져옵니다. | 
    
[&, x, y] (C++11~) | 
      외부의 모든 변수의 참조자를 가져오되 x, y 만 값으로 가져옵니다. | 
    
| this (C++11~) | 람다 표현식을 사용한 개체의 this 포인터를 값으로 가져옵니다. | 
*this (C++17~) | 
      this는 포인터를 가져오지만, *this는 개체 자체를 복제하여 가져옵니다. | 
    
다음 코드에서는 람다 표현식 외부에 정의된 sum을 람다 캡쳐하고, 람다 표현식 내에서 v의 각 요소의 값을 전달받아 sum에 누적합니다.
1
2
3
4
5
6
7
8
9
10
int sum{0};
std::vector<int> v_11{1, 2, 3};
std::for_each( // 시퀀스 안의 요소들에 대해 f(요소)를 실행합니다. f는 람다 표현식입니다.
    v_11.begin(), 
    v_11.end(),
    [&sum](int val) {sum += val;} // 람다 캡쳐된 sum에 시퀀스 요소의 값(val)을 누적합니다.
);
EXPECT_TRUE(sum == 1 + 2 + 3);
[=]나 [&] 는 람다 캡쳐되는 대상이 무엇인지 람다 표현식 본문을 확인해야 알 수 있으므로 [&var1, &var2]와 같이 람다 캡쳐하는 항목을 나열하는게 좀 더 직관적이여서 좋습니다. 또한, [=]는 this를 암시적으로 람다 캡쳐하므로 주의하는게 좋습니다.
(C++20~) 람다 캡쳐에서 [=] 사용시 this의 암시적 캡쳐가 deprecate되었으므로 명시적으로 작성해야 합니다.
람다 캡쳐 시점
람다 표현식은 클로저가 생성될 때 람다 캡쳐를 합니다. 따라서, 클로저를 생성한 시점과 호출한 시점이 다르면, 값이 다를 수 있습니다.
1
2
3
4
5
6
7
int val{1};
auto f_11{
    [=]() -> int {return val;} // 클로저가 생성되는 시점에 val을 람다 캡쳐합니다.
}; 
val = 2;
EXPECT_TRUE(f_11() == 1); // 람다 캡쳐할 때의 값을 사용하므로 1입니다.  
값 캡쳐
값으로 개체를 람다 캡쳐하면 복제본을 만들어 const로 사용합니다. 따라서, 그 값을 수정할 수 없습니다. 다만, 포인터 변수는 int* const여서 ptr은 수정할 수 없지만, *ptr은 수정할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
int a{1};
int b{2};
int c{3};
int* ptr{&b};
int& ref{c};
[=]() { // 값으로 복제하며 `const` 입니다.
    // a = 10; // (X) 컴파일 오류. const 이므로 수정할 수 없습니다.
    // ptr = &a; // (X) 컴파일 오류. const 이므로 수정할 수 없습니다.
    *ptr = 20; // int* const 여서 ptr 은 수정할 수 없지만, ptr이 가리키는 개체는 수정할 수 있습니다.
    // ref = 30; // (X) 컴파일 오류. const 이므로 수정할 수 없습니다.
}();
EXPECT_TRUE(b == 20);         
이런 경우 mutable을 사용하면 람다 캡쳐한 개체를 수정할 수 있으나, 원본이 아닌 복제본만 수정됩니다.(원본을 수정하려면 참조 캡쳐를 이용하셔야 합니다.)
1
2
3
4
5
6
7
8
9
10
11
12
int a{1};
int b{2};
int c{3};
int* ptr{&b};
int& ref{c};
[=]() mutable { // 값 캡쳐했지만 수정할 수 있습니다.
    a = 10; // (△) 비권장. 복제본이 수정될 뿐, 원본이 수정되는 건 아닙니다.
    *ptr = 20;
    ref = 30; // (△) 비권장. 복제본이 수정될 뿐, 원본이 수정되는 건 아닙니다.
}();
EXPECT_TRUE( a == 1 && b == 20 && c == 3);         
개체의 멤버 함수 내에서 사용할 때에는 값 캡쳐시 this 포인터가 복제되어 개체의 내부 멤버 변수를 수정할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class T_11 {
    int m_Member{1};
public:
    int GetMember() const {return m_Member;}
    int Func() {
        int local{2};
        auto f_11{
            [=]() -> int {
                m_Member = 10; // this->m_Member = 10; 과 동일. 멤버 변수의 값을 수정합니다.
                return m_Member + local;
            }
        };  
        return f_11();
    }
};
T_11 t;
EXPECT_TRUE(t.Func() == 12);
EXPECT_TRUE(t.GetMember() == 10); // 멤버 변수가 수정되어 있습니다.
(C++20~) 람다 캡쳐에서 [=] 사용시 this의 암시적 캡쳐가 deprecate되었으므로 명시적으로 작성해야 합니다.
참조 캡쳐
참조 캡쳐를 이용하면, 람다 캡쳐한 개체를 수정할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
int a{1};
int b{2};
int c{3};
int* ptr{&b};
int& ref{c};
[&]() { // 람다 캡쳐한 개체를 수정할 있습니다.
    a = 10; 
    *ptr = 20;
    ref = 30;
}();
EXPECT_TRUE( a == 10 && b == 20 && c == 30);     
클로저에 이동 연산 전달
람다 캡쳐는 값 캡쳐를 이용한 복사 대입과, 참조 캡쳐를 이용한 참조로 람다 표현식 내부에 람다 표현식 바깥의 개체 정보들을 전달합니다. 그런데, 이동 연산으로 전달하려면 어떻게 할까요?
다음과 같이 이동 생성자만 지원하는 개체 A_11이 있다고 합시다.
1
2
3
4
5
6
7
8
9
10
class A_11 {
public:
    A_11() {}
    A_11(const A_11& other) = delete;
    A_11(A_11&& other) noexcept {
        std::cout << "A : Lambda Move" << std::endl;
    }
    A_11& operator =(const A_11& other) = delete;
    A_11& operator =(A_11&& other) noexcept = delete; 
};
다음과 같이 값 캡쳐는 복사 생성자가 없으므로 사용할 수 없습니다.
1
2
3
A_11 a; // 기본 생성과 이동 생성만 가능한 개체입니다.
[a]() {a;}(); // (X) 컴파일 오류. 복사 생성자가 delete 되었습니다.
물론 참조 캡쳐는 이용할 수 있으나, 이동된 것은 아니죠.
1
2
3
A_11 a; // 기본 생성과 이동 생성만 가능한 개체입니다.
[&a]() {a;}(); // (O) 참조로 사용해서 컴파일 오류는 없습니다. 그러나 이동된 것은 아니죠.
이동을 시키려면 다음과 같이 bind()를 이용하여 함수자를 만들면 됩니다.
func_11은 함수자를 저장합니다. 추후func_11()와 같이 호출해 주어야 합니다.- 
    
    
이때 첫번째 인자는 함수처럼 호출될 수 있는 함수자 입니다. 람다 표현식으로 만들어진 클로저 처럼요.
 - 람다 표현식으로 클로저를 전달합니다. 여기서 
const A_11& param를 인자로 사용하는데요, 이동 생성자로부터 생성된 개체(#4에서 생성된 개체)를 참조로 전달받습니다. std::move(a)로 우측값 참조를 전달합니다. bind()는 이값을 개체로 저장(변수 = 우측값 참조;이다 보니, 이동 생성자가 호출됩니다.)하고 있다가 함수자를 호출하는 #5 시점에 사용합니다.- 함수자를 실행합니다. 즉, 함수를 호출합니다.
 
1
2
3
4
5
6
7
8
9
A_11 a; // 기본 생성과 이동 생성만 가능한 개체입니다.
auto func_11 { // #1. bind 개체를 저장합니다. func_11() 형태로 호출할 수 있습니다.
    std::bind( // #2. bind 개체를 생성합니다. lambda(obj)를 호출하는 함수자를 만듭니다. 이시점에 이동 생성자가 호출됩니다. 
        [](const A_11& param) {param;}, // #3. 이동 생성자로부터 생성된 개체(#4에서 생성된 개체)를 참조합니다. 
        std::move(a) // #4. 우측값 참조. bind 내에서 이동 생성자로부터 생성된 개체가 만들어 집니다.
    )
};
func_11(); // #5. 함수를 호출합니다.
(C++14~) 람다 캡쳐 초기화가 추가되어 람다 표현식내에서 사용하는 임의 변수를 정의하여 사용할 수 있습니다. 이를 이용하면 이동 연산 전달이 다음처럼 훨씬 간단해 집니다.
1
2
3
4
5
6
A_11 a; // 기본 생성과 이동 생성만 가능한 개체입니다.
auto f_14{
    [b = std::move(a)](){b;} // a를 이동 생성한 b를 사용합니다.
}; 
f_14();
클로저 대입
클로저는 auto와 함수 포인터와 function을 이용하여 변수에 대입할 수 있습니다.
- 
    
auto(단, auto이기 때문에 함수의 인자로 사용할 수 없습니다.)
1 2 3 4
auto f_11{ [](int a, int b) -> int {return a + b;} }; EXPECT_TRUE(f_11(10, 20) == 30);
 - 
    
    
1 2 3 4 5
typedef int (*Func)(int, int); // 함수 포인터 typedef Func f_11{ [](int a, int b) -> int {return a + b;} }; EXPECT_TRUE(f_11(10, 20) == 30);
 - 
    
function(람다 캡쳐도 지원하고, 함수 인자로 사용할 수도 있습니다.)
1 2 3 4 5 6 7 8 9
int g(const std::function<int(int, int)>& lambda_11, int a, int b) { // 함수의 인자로 람다를 지정합니다. return lambda_11(a, b); } int c{30}; std::function<int(int, int)> f_11; f_11 = [=](int a, int b) -> int {return a + b + c;}; // 람다 캡쳐를 사용하는 람다 표현식도 사용할 수 있습니다. EXPECT_TRUE(g(f_11, 10, 20) == 60); // g() 함수에 클로저를 저장한 f_11을 전달합니다.
 
클로저 복사 부하
참조 캡쳐를 이용하는게 좋습니다.
다음 T_11클래스는 생성자와 소멸자 호출을 탐지하기 위한 테스트 클래스입니다.
1
2
3
4
5
6
7
8
class T_11 {
public: 
    T_11() {std::cout << "T_11::Default Constructor" << std::endl;}
    T_11(const T_11&) {std::cout << "T_11::Copy Constructor" << std::endl;}
    T_11(T_11&&) noexcept {std::cout << "T_11::Move Constructor" << std::endl;}
    ~T_11() {std::cout << "T_11::Destructor" << std::endl;}
    void operator =(const T_11&) noexcept {std::cout << "T_11::operator =()" << std::endl;}
};
- 
    
값 캡쳐던, 참조 캡쳐던, 해당 개체를 람다 표현식에서 사용하지 않으면 복사하지 않습니다.
1 2
T_11 t; [=]() {std::cout << "Run Lambda" << std::endl;}(); // t를 사용하지 않았습니다.
1 2 3
T_11::Default Constructor Run Lambda T_11::Destructor
 - 
    
값 캡쳐를 사용하면, 대상 개체가 람다 표현식에 사용될때, 복사 생성자를 호출하여, const 복제본을 만듭니다.
1 2 3 4 5
T_11 t; [=]() { t; // t;로 사용합니다. 람다 캡쳐시 복사 부하가 있습니다. std::cout << "Run Lambda" << std::endl; }();
1 2 3 4 5
T_11::Default Constructor T_11::Copy Constructor // 람다 캡쳐시 복사 생성자를 호출하여 const 복제본을 만듭니다. Run Lambda T_11::Destructor // 복제본을 삭제합니다. T_11::Destructor
 - 
    
값 캡쳐를 사용하고, 클로저를 변수에 저장하고 호출하는 것은 추가의 복사 부하가 없습니다. 즉, 2와 동일합니다.
1 2 3 4 5 6 7 8
T_11 t; auto f_11{ [=]() { // 변수에 저장하고, 호출합니다. t; std::cout << "Run Lambda" << std::endl; } }; f_11();
1 2 3 4 5
T_11::Default Constructor T_11::Copy Constructor // 추가 복사 부하가 없습니다. Run Lambda T_11::Destructor T_11::Destructor
 - 
    
값 캡쳐를 사용할때 클로저 끼리 복제하면 추가의 복사 부하가 발생합니다.
1 2 3 4 5 6 7 8 9 10
T_11 t; auto f1_11{ [=]() { t; std::cout << "Run Lambda" << std::endl; } }; auto f2_11{f1_11}; // 클로저를 복제합니다. 복사 부하가 발생합니다. f1_11(); f2_11();
1 2 3 4 5 6 7 8
T_11::Default Constructor T_11::Copy Constructor // 람다 캡쳐시 복사 생성자를 호출하여 const 복제본을 만듭니다. T_11::Copy Constructor // f2{f1}시 클로저 복제본을 만듭니다. 추가 복사 부하가 있습니다. Run Lambda Run Lambda T_11::Destructor T_11::Destructor T_11::Destructor
 - 
    
참조 캡쳐를 사용하면 복사 부하가 없습니다.
1 2 3 4 5 6 7 8 9 10
T_11 t; auto f1_11{ [&]() { // 람다 캡쳐시 복사 부하가 없습니다. t; std::cout << "Run Lambda" << std::endl; } }; auto f2_11{f1_11}; // 대입시 복사 부하가 없습니다. f1_11(); f2_11();
1 2 3 4
T_11::Default Constructor Run Lambda Run Lambda T_11::Destructor
 
(C++14~) 람다 캡쳐 초기화
C++14 부터는 람다 캡쳐시에 람다 표현식내에서 사용하는 임의 변수를 정의하여 사용할 수 있습니다. 정의한 변수는 초기값이 지정되어야 하며, 람다 표현식 내에서만 사용할 수 있습니다.
클로저 생성시에 람다 캡쳐하므로, 람다 표현식을 여러번 호출하더라도 기존 값이 유지됩니다.
1
2
3
4
5
6
7
8
9
10
// 클로저 생성시 람다 내에서 사용할 수 있는 val_14 변수를 만들어 람다 캡쳐 합니다.
// 람다 캡쳐된 val_14 을 수정하기 위해 mutable로 만듭니다.
auto f_14{
    [val_14 = 0]() mutable -> int {return ++val_14;}
}; 
// 호출시마다 람다 캡쳐된 val이 증가합니다.
EXPECT_TRUE(f_14() == 1);
EXPECT_TRUE(f_14() == 2);
EXPECT_TRUE(f_14() == 3);
(C++14~) 일반화된 람다 표현식
C++14 부터는 일반화된 람다 표현식이 추가되어 auto를 인자로 받아 마치 함수 템플릿처럼 사용할 수 있습니다. 다만 리턴 타입 추론에서와 같이 auto의 중괄호 초기화 특수 추론 규칙은 적용되지 않습니다.
1
2
3
4
5
6
auto add_14{
    [](auto a, auto b) {return a + b;}
}; 
EXPECT_TRUE(add_14(1, 2) == 3);
EXPECT_TRUE(add_14(std::string{"hello"}, std::string{"world"}) == "helloworld");
마치 다음의 함수 템플릿과 유사합니다.(add_14(T a, T b)가 아닌 add_14(T a, U b)인 것을 주목하세요.)
1
2
3
4
5
6
7
8
template<typename T, typename U> 
auto add_14(T a, U b) { // add_14(T a, T b)가 아닙니다. 
                        // auto이다 보니 a, b가 같은 타입이라는 보장이 없습니다.
    return a + b;
} 
EXPECT_TRUE(add_14(1, 2) == 3);
EXPECT_TRUE(add_14(std::string{"hello"}, std::string{"world"}) == "helloworld");
auto가 전달된 인자를 따로 따로 추론하다보니 a, b가 같은 타입인지 보장할 수 없는데요, decltype()을 이용하면, 인자로 부터 타입을 결정할 수 있으므로, a, b를 같은 타입으로 보장할 수 있습니다.
1
2
3
4
5
6
7
auto add_14{
    // b는 a와 같은 타입입니다.
    [](auto a, decltype(a) b) {return a + b;}
}; 
EXPECT_TRUE(add_14(1, 2) == 3);
EXPECT_TRUE(add_14(std::string{"hello"}, std::string{"world"}) == "helloworld");
decltype()과 일반화된 람다 표현식은 또다른 곳에서도 좋은 궁합을 보입니다.
일반화된 람다 표현식은 템플릿 표현이 아니다 보니 완벽한 전달을 위한 전달 참조 구문을 사용하기 어렵습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int f_11(int&) {return 1;}
int f_11(int&&) {return 2;}
auto Forwarding_14{
    [](auto&& param) {
        return f_11(std::forward<??>(param)); // (X) 컴파일 오류. 템플릿이라면 템플릿 인자 T를 전달하면 되는데, ??에 무얼 전달해야 할까요? 
    }
}; 
int val;
int& ref = val;
EXPECT_TRUE(Forwarding_14(val) == 1);
EXPECT_TRUE(Forwarding_14(ref) == 1);
EXPECT_TRUE(Forwarding_14(std::move(val)) == 2);
다행히 전달 참조 인 변수를 decltype()을 해보면, 좌측값 참조는 좌측값 참조로 나오고, 우측값 참조는 우측값 참조로 나옵니다. 즉, decltype()을 사용하면, forward() 함수의 리턴값은 forward() 원리에서와 살펴본 것과 동일해 집니다.
| 항목 | 전달 참조 | forward() 함수의 템플릿 인스턴스화 | forward() 함수의 리턴값 | 
|---|---|---|---|
| 값 타입 | param == int&decltype<param> == int& | 
      T == int&, param == int& | 
      int& | 
    
| 좌측값 참조 | param == int&<br/>decltype(param) == int&|T == int&, param == int&|int&` | 
      ||
| 함수 인자처럼 우측값을 참조하는 좌측값 참조 | param == int&decltype(param) == int&& | 
      T == int&&, param == int& | 
      int&& | 
    
따라서 일반화된 람다 표현식에서는 decltype()을 이용하여 완벽한 전달을 할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int f_11(int&) {return 1;}
int f_11(int&&) {return 2;}
auto Forwarding_14{
    [](auto&& param) {
        // 전달 참조일때 decltype(param)은 
        // param이 좌측값 참조이면 T&, param이 우측값 참조이면 T&& 입니다.
        return f_11(std::forward<decltype(param)>(param)); 
    }
}; 
int val;
int& ref = val;
EXPECT_TRUE(Forwarding_14(val) == 1); // 전달 참조에서 값 타입은 좌측값 참조입니다.
EXPECT_TRUE(Forwarding_14(ref) == 1); // 전달 참조에서 좌측값 참조는 그대로 좌측값 참조입니다.
EXPECT_TRUE(Forwarding_14(std::move(val)) == 2); // 전달 참조에서 우측값 참조는 그대로 우측값 참조입니다.       
(C++20~) 람다 표현식에서 템플릿 인자를 지원합니다.
(C++17~) constexpr 람다 표현식
C++17 부터 constexpr 람다 표현식이 추가되어 요구사항이 충족되면 자동으로 암시적 컴파일 타임 함수로 만들어집니다. 또한, constexpr을 지정하여 명시적으로 컴파일 타임 함수로 만들 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 명시적 constexpr 람다 표현식 입니다.
auto id_17{
    [](int n) constexpr {return n;}
};
static_assert(id_17(10) == 10);
// 암시적 constexpr 람다 표현식 입니다.
auto add_17{
    [](int a, int b) {return a + b;}
};
static_assert(add_17(1, 2) == 3);
// 인자로 전달한 a_11, b_11는 컴파일 타임 상수이기 때문에 요구사항이 맞아 암시적 constexpr 람다 표현식 입니다.
constexpr int a_11{1};
constexpr int b_11{2};
static_assert(add_17(a_11, b_11) == 3);
// 인자로 전달한 c, d는 런타임 변수이기 때문에 요구사항이 맞지 않아 런타임 람다 표현식 입니다.
int c{1};
int d{2};
EXPECT_TRUE(add_17(c, d) == 3);
(C++20~) 람다 표현식에서 템플릿 인자 지원
C++14에 도입된 일반화된 람다 표현식은 템플릿 인자를 지원하지 않았는데요(일반화된 람다 표현식 참고),
C++20 부터는 람다 표현식에서 템플릿 인자를 지원합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
auto add_11 {
    [](int a, int b) {return a +b;} // 람다 표현식
};
auto add_14{
    [](auto a, decltype(a) b) {return a + b;} // 일반화된 람다 표현식
}; 
auto add_20{
    []<typename T>(T a, T b) {return a + b;} // 람다 표현식에서 템플릿 인자 지원
};
EXPECT_TRUE(add_11(1, 2) == 3);
EXPECT_TRUE(add_14(1, 2) == 3);
EXPECT_TRUE(add_20(1, 2) == 3);
(C++20~) 람다 캡쳐에서 파라메터 팩 지원
C++20 부터는 람다 캡쳐에서 파라메터 팩을 지원합니다.
다음은 전달 참조로 받은 파라메터 팩을 람다 표현식의 인자에서 값 캡쳐하여 Sum()함수에 포워딩하는 예입니다.
특이하게 forward<const Params>와 같이 const를 덧붙였는데요, 이는 값 캡쳐가 복제본을 만들어 const로 사용하기 때문입니다.(그냥 forward<Params>로 사용하면, int를 const int에 대입하기 때문에 상수성 계약 위반으로 컴파일 오류가 발생합니다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int Sum(int a, int b, int c) {return a + b + c;}
template <typename... Params>
auto Sum_20(Params&&... params) { // 전달 참조입니다. 정수형 상수는 int&& 로 전달받습니다. 
    // 값 캡쳐
    // 값 캡쳐는 복제본을 만들어 const로 사용하므로
    // lambdaParams는 const int로 대입받습니다. 
    return [...lambdaParams = std::forward<Params>(params)] { 
                                                                
        // lambdaParams은 const int여서
        // forward<Params>로 사용하면, forward<int&&>이므로 상수성 계약 위반입니다.
        // 따라서 forward<const Params>으로 전달해야 합니다.
        return Sum(std::forward<const Params>(lambdaParams)...); 
    }(); // 클로저를 실행합니다.
}
EXPECT_TRUE(Sum_20(1, 2, 3) == 1 + 2 + 3);
다음은 전달 참조로 받은 파라메터 팩을 람다 표현식의 인자에서 참조 캡쳐하여 Add()함수에 포워딩하는 예입니다. Add()함수에서 result의 값을 수정하며, 호출한 곳에서 수정된 값을 확인할 수 있습니다.
참조 캡쳐는 자기가 참조성을 덧붙입니다. 따라서, [&...lambdaParams = std::forward<Params>(params)]와 같이 람다 캡쳐 하면 타입이 맞지 않아 컴파일 오류가 발생합니다. 따라서, 억지로 [std::forward<Params&>(params)] 와 같이 forward()함수시 참조성을 더하여 lambdaParams에 저장한 뒤, 다시 [std::forward<Params>(params)]를 이용하여 원복합니다. 자세한 원리는 forward() 원리를 참고하세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void Add(int& result, int a, int b, int c) {result += a + b + c;}
template <typename... Params>
void Add_20(Params&&... params) { // 전달 참조입니다. 이름이 있는 좌측값은 int&, 정수형 상수는 int&& 로 전달받습니다.
    // 참조 캡쳐이기 때문에 참조성이 더해집니다.
    // 즉 params[2] == int&& 일때, lambdaParams[2] == int&& + & == int& 입니다.
    // 따라서, int&& 를 int&에 바인딩할 수 없다는 컴파일 오류가 발생합니다.
    // [&...lambdaParams = std::forward<Params>(params)] { // (X) 컴파일 오류. 
    // 참조 캡쳐에서 참조성을 더해서 대입받는 문제를 우회하기 위해
    // forward의 T에 Params& 로 참조성을 억지로 추가하여 형변환 합니다.
    // ** 사례 분석 : params[1]이 int& 인 경우 **
    //    1. lambdaParams[1] == int& + 참조 캡쳐에 의한 & == int& 을 전달 받아야 합니다.
    //    2. params[1]은 좌측값 참조이므로, 좌측값 참조를 인자로 받는 forward() 함수가 호출됩니다. 
    //    3. forward 함수에 전달되는 템플릿 인자는 T == params[1] + & == int& + & == int& 가 전달되어 전개됩니다.
    //    4. forward 함수는 static_cast<T&&>를 리턴하므로 static_cast<int& &&> 를 리턴하므로 참조 축약에 의해 int&을 리턴합니다.
    // ** 사례 분석 : params[2]가 int&& 인 경우 **
    //    1. lambdaParams[2] == int&& + 참조 캡쳐에 의한 & == int& 을 전달 받아야 합니다.
    //    2. params[1]은 int&&를 참조하지만, 이름을 부여받았으므로 좌측값 참조를 인자로 받는 forward() 함수가 호출됩니다. 
    //    3. forward 함수에 전달되는 템플릿 인자는 T == params[2] + & == int&& + & == int& 가 전달되어 전개됩니다.
    //    4. forward 함수는 static_cast<T&&>를 리턴하므로 static_cast<int& &&> 를 리턴하므로 참조 축약에 의해 int&을 리턴합니다.
    //    5. lambdaParams[2] 는 int&인 좌측값 참조입니다.
    // 즉, lambaParams는 모두 좌측값 참조로 저장합니다.
    [&...lambdaParams = std::forward<Params&>(params)] { 
        // 애초에 캡쳐할때의 Params로 forward()를 호출합니다.
        // ** 사례 분석 : params[1]이 int& 이고 lambdaParams[1]이 int&인 경우 **
        //    1. lambdaParams[1]이 int&이므로, 좌측값 참조를 인자로 받는 forward() 함수가 호출됩니다. 
        //    2. forward 함수에 전달되는 템플릿 인자는 T == params[1] == int& 가 전달되어 전개됩니다.
        //    3. forward 함수는 static_cast<T&&>를 리턴하므로 static_cast<int& &&> 를 리턴하므로 참조 축약에 의해 int&을 리턴합니다.
        //    4. Add() 함수에 int&로 전달합니다.
        // ** 사례 분석 : params[2]가 int&& 이고 lambdaParams[2]가 int& 인 경우 **
        //    1. lambdaParams[1]은 좌측값 참조이므로, 좌측값 참조를 인자로 받는 forward() 함수가 호출됩니다.
        //    2. forward 함수에 전달되는 템플릿 인자는 T == params[2] == int&& 가 전달되어 전개됩니다.
        //    3. forward 함수는 static_cast<T&&>를 리턴하므로 static_cast<int&& &&> 를 리턴하므로 참조 축약에 의해 int&&을 리턴합니다.
        //    4. Add() 함수에 int&&로 전달합니다.
        // 즉, lambdaParams가 모두 좌측값 참조로 저장했지만, forward<Params>을 이용해서 원복해서 Add() 함수에 전달합니다.
        Add(std::forward<Params>(lambdaParams)...); 
    }(); // 클로저를 실행합니다
}
int result = 10;
Add_20(result, 1, 2, 3);
EXPECT_TRUE(result == 10 + 1 + 2 + 3); // result 값이 잘 수정되어 있습니다.
(C++20~) 람다 캡쳐에서 구조화된 바인딩 지원
C++20 부터는 람다 캡쳐에서 구조화된 바인딩을 지원합니다.
1
2
3
4
5
6
7
8
9
10
11
12
class A_11 {
public:
    int m_X{1};
    int m_Y{2};
};
auto [x_17, y_17]{A_11{}}; 
auto lambda_20{
    [x_17, y_17] {return x_17 + y_17;} // 구조화된 바인딩을 람다 캡쳐합니다.
};
EXPECT_TRUE(lambda_20() == 1 + 2);
(C++20~) 상태없는 람다 표현식의 기본 생성과 복사 대입 지원
상태없는 람다 표현식이란, 캡쳐하는 것이 없는 람다 표현식 입니다.
기존에는 상태없는 람다 표현식의 기본 생성과 복사 대입을 지원하지 않았지만,
1
2
3
4
5
6
7
auto lambda_11{
    [](int left, int right) {return left > right;} // 큰수에서 작은 수로 정렬합니다.
};
decltype(lambda_11) a_11{lambda_11}; // 복사 생성은 가능합니다.
decltype(lambda_11) b_11; // (X) 컴파일 오류. 기본 생성은 불가능합니다.
a_11 = lambda_11; // (X) 컴파일 오류. 복사 대입은 불가능합니다.
C++20 부터는 상태없는 람다 표현식의 기본 생성과 복사 대입을 지원합니다.
1
2
3
4
5
6
7
auto lambda_11{
    [](int left, int right) {return left > right;} // 캡쳐를 사용하지 않습니다.
};
decltype(lambda_11) a_20{lambda_11}; // 복사 생성은 가능합니다.
decltype(lambda_11) b_20; // (O) 기본 생성이 가능합니다.
a_20 = lambda_11; // (O) 복사 대입이 가능합니다.
만약 람다 캡쳐를 사용한다면, 여전히 기본 생성과 복사 대입은 지원하지 않습니다.
1
2
3
4
5
6
7
auto lambda_11{
    [=](int left, int right) {return left > right;} // 캡쳐를 사용합니다.
};
decltype(lambda_11) a_20{lambda_11}; // 복사 생성은 가능합니다.
decltype(lambda_11) b_20; // (X) 컴파일 오류. 기본 생성은 불가능합니다.
a_20 = lambda_11; // (X) 컴파일 오류. 복사 대입은 불가능합니다.
상태없는 람다 표현식의 기본 생성과 복사 대입을 지원함으로서 컨테이너등에서 람다 표현식의 사용이 편해졌습니다.
예를 들어 set에서는 기본적으로 std::less<_Key>를 이용하여 요소들을 비교하는데요, 이를 사용자 정의하려면, Compare의 타입과 개체를 같이 전달해야 했습니다. 이렇게 set의 생성자에 Compare개체를 전달하기 때문에, 생성자에서 initializer_list
를 사용할 수 없었습니다.(initializer_list를 사용하면 우선 순위에 의해 다른 생성자를 사용할 수 없죠. 기존 생성자와 initializer_list 생성자와의 충돌 참고)
1
2
3
4
5
6
7
8
9
10
11
12
13
auto lambda_11{
    [](int left, int right) {return left > right;} // 큰수에서 작은 수로 정렬합니다.
};
std::set<int, decltype(lambda_11)> data_11(lambda_11); // Compare 개체를 전달해야 합니다.
// std::set<int, decltype(lambda_11)> data_11{1, 2, 3}; // (X) 컴파일 오류. initializer_list로 초기화하면 Compare개체를 전달할 수 없습니다.
data_11.insert(1); 
data_11.insert(2);
data_11.insert(3);
for (auto i : data_11) {
    std::cout << "set : " << i << std::endl; // 3 2 1 의 순서로 출력합니다.
}
C++20 부터는 컨테이너 내부에서 상태없는 람다 표현식을 기본 생성합니다. 즉, 상기 예에서 템플릿 인자에 decltype(lambda_11)로 비교하는 함수자의 타입을 전달했다면, 굳이 Compare 개체를 전달할 필요가 없습니다.
1
2
3
4
5
6
7
8
9
10
11
auto lambda_11{
    [](int left, int right) {return left > right;} 
};
std::set<int, decltype(lambda_11)> data_20; // 상태없는 람다 표현식은 기본 생성하므로 Compare 개체를 전달할 필요가 없습니다.
data_20.insert(1);
data_20.insert(2);
data_20.insert(3);
for (auto i : data_20) {
    std::cout << "set : " << i << std::endl;
}
따라서 다음과 같이 initializer_list를 이용하여 간편하게 초기화 할 수 있습니다.
1
2
3
4
5
6
7
8
auto lambda_11{
    [](int left, int right) {return left > right;} 
};
std::set<int, decltype(lambda_11)> data_20{1, 2, 3}; // initializer_list로 초기화 할 수 있습니다.
for (auto i : data_20) {
    std::cout << "set : " << i << std::endl;
}
(C++20~) 미평가 표현식에서의 람다 표현식 허용
미평가 표현식은 정의없이 선언만으로도 사용할 수 있는 표현식을 말합니다. decltype(), sizeof(), typeid() 처럼요.
다음 예에서 decltype()은 AddDeclare()의 타입만 필요로 하기 때문에 AddDeclare()의 선언만 있어도 됩니다.
1
2
3
4
5
6
extern int AddDeclare(int, int); // 함수 선언
int AddDefine(int a, int b) {return a + b;} // 함수 정의
decltype(*AddDeclare) AddFunc_11{AddDefine}; // AddDeclare함수 선언으로부터 변수 AddFunc_11을 정의하고 
                                             // AddDefine으로 초기화합니다. 
EXPECT_TRUE(AddFunc_11(10, 20) == 30); // 함수 실행    
C++20 부터는 미평가 표현식에서도 람다 표현식을 허용하기 때문에 decltype()안에서 사용할 수 있습니다.
즉, 다음과 같이 set의 Compare를 정의할 수 있습니다.
1
2
3
4
5
6
7
8
9
std::set<
    int, 
    decltype(
        [](int left, int right) {return left > right;} // 미평가 표현식에서 람다 표현식을 사용할 수 있습니다.
    )
> data_20{1, 2, 3}; 
for (auto i : data_20) {
    std::cout << "set : " << i << std::endl; // 3 2 1 의 순서로 출력합니다.
}  
(C++20~) 람다 표현식에서 [=] 사용시 this의 암시적 캡쳐 deprecate
람다 캡쳐시 [=]는 this도 암시적으로 캡쳐했습니다. 하지만 암시적 형변환이나 클래스의 암시적 정의처럼 암시적인건 언제나 좋지 않습니다. 항상 예상치 못한 곳에서 사이드 이펙트가 발생하니까요.
애초에 지원하지 않았으면 좋았을텐데, 조금 늦었지만, C++20 부터는 람다 캡쳐에서 [=] 사용시 this의 암시적 캡쳐가 deprecate되었으므로 명시적으로 작성해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class T_11 {
    int m_Member{1};
public:
    int GetMember() const {return m_Member;}
    int Func() {
        int local{2};
        auto f_11{
#if 202002L <= __cplusplus // C++20~ 
                // C++20 부터는 this를 명시적으로 작성합니다.
            [=, this]() -> int { 
#else
                // 기존에는 암시적으로 this가 캡쳐되었습니다.
            [=]() -> int {
#endif
                m_Member = 10; // this->m_Member = 10; 과 동일. 멤버 변수의 값을 수정합니다.
                return m_Member + local;
            }
        };  
        return f_11();
    }
};
T_11 t;
EXPECT_TRUE(t.Func() == 12);
EXPECT_TRUE(t.GetMember() == 10); // 멤버 변수가 수정되어 있습니다.
[&] 사용시에는 여전히 this를 캡쳐합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class T_11 {
    int m_Member{1};
public:
    int GetMember() const {return m_Member;}
    int Func() {
        int local{2};
        auto f_11{
            [&]() -> int { // C++20에서도 여전히 this를 참조로 캡쳐합니다.
                m_Member = 10; // this->m_Member = 10; 과 동일. 멤버 변수의 값을 수정합니다.
                return m_Member + local;
            }
        };  
        return f_11();
    }
};
댓글남기기