#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();
}
};
댓글남기기