람다 표현식
람다 표현식 (Lambda expression)
람다 표현식(lambda expression) 은 클로저(closure) — 범위 안의 변수를 캡처할 수 있는 이름 없는 함수 객체 — 를 만드는 표현식이에요.
출처: cppreference
본문
문법 (Syntax)
명시적 템플릿 매개변수 목록이 없는 람다 (가능하면 비제네릭)
[captures] front-attr(optional) (params) specs(optional) except(optional)
back-attr(optional) trailing(optional) requires(optional) contract-specs(optional) { body } (1)
[captures] { body } (2, C++23까지)
[captures] front-attr(optional) trailing(optional) contract-specs(optional) { body } (2, C++23부터)
[captures] front-attr(optional) except back-attr(optional) trailing(optional)
contract-specs(optional) { body } (3, C++23부터)
[captures] front-attr(optional) specs except(optional) back-attr(optional)
trailing(optional) contract-specs(optional) { body } (4, C++23부터)
- 매개변수 목록이 있는 람다 표현식.
2~4) 매개변수 목록이 없는 람다 표현식. 2) 가장 단순한 문법,
back-attr은 적용할 수 없어요. 3,4)back-attr은specs와except중 하나라도 있으면 적용할 수 있어요.
명시적 템플릿 매개변수 목록이 있는 람다 (항상 제네릭, C++20부터)
[captures] <tparams> t-requires(optional) front-attr(optional) (params) specs(optional)
except(optional) back-attr(optional) trailing(optional) requires(optional)
contract-specs(optional) { body } (1)
[captures] <tparams> t-requires(optional) { body } (2, C++23까지)
[captures] <tparams> t-requires(optional) front-attr(optional) trailing(optional)
contract-specs(optional) { body } (2, C++23부터)
[captures] <tparams> t-requires(optional) front-attr(optional) except back-attr(optional)
trailing(optional) contract-specs(optional) { body } (3, C++23부터)
[captures] <tparams> t-requires(optional) front-attr(optional) specs except(optional)
back-attr(optional) trailing(optional) contract-specs(optional) { body } (4, C++23부터)
- 매개변수 목록이 있는 람다 표현식. 2~4) 매개변수 목록이 없는 람다 표현식.
설명 (Explanation)
- captures — 캡처할 개체를 지정해요.
- tparams — 비어 있지 않은 템플릿 매개변수의 쉼표로 구분된 목록. 제네릭 람다의 템플릿 매개변수에 이름을 제공하는 데 쓰여요 (아래 ClosureType::operator() 참조).
- t-requires —
tparams에 제약을 더해요. t-requires 가 속성 지정자 시퀀스로 끝나면, 그 속성들은 front-attr 의 속성으로 취급돼요. (C++23부터) - front-attr — (C++23부터) 클로저 타입의
operator()에 적용되는 속성 지정자 시퀀스 (따라서[[noreturn]]속성을 쓸 수 있어요). - params — 클로저 타입
operator()의 매개변수 목록. 명시적 객체 매개변수를 가질 수 있어요. (C++23부터) - specs — 다음 지정자의 목록으로, 각 지정자는 시퀀스에 최대 한 번만 허용돼요.
mutable— 본문이 복사로 캡처된 객체를 수정하고 그 비-const 멤버 함수를 호출하도록 허용해요. 명시적 객체 매개변수가 있으면 쓸 수 없어요. (C++23부터)constexpr(C++17부터) —operator()가 constexpr 함수임을 명시적으로 지정해요.operator()가 모든 constexpr 함수 요구 사항을 만족하면constexpr이 없어도 constexpr 이 돼요.consteval(C++20부터) —operator()가 즉시 함수(immediate function)임을 지정해요.consteval과constexpr은 동시에 지정할 수 없어요.static(C++23부터) —operator()가 정적 멤버 함수임을 지정해요.static과mutable은 동시에 지정할 수 없어요. captures 가 비어 있지 않거나 명시적 객체 매개변수가 있으면 쓸 수 없어요.
- except — 클로저 타입
operator()의 동적 예외 사양(C++17까지) 또는noexcept지정자를 제공해요. - back-attr — 클로저 타입
operator()의 타입에 적용되는 속성 지정자 시퀀스 ([[noreturn]]속성은 쓸 수 없어요). - trailing —
-> ret로,ret가 반환 타입을 지정해요. - requires — (C++20부터) 클로저 타입
operator()에 제약을 더해요. - contract-specs — (C++26부터) 클로저 타입
operator()의 함수 계약 지정자 목록. - body — 함수 본문.
매개변수 타입으로 auto를 쓰거나, 명시적 템플릿 매개변수 목록이 제공되면(C++20부터) 그 람다는 제네릭 람다(generic lambda) 예요. (C++14부터)
변수 __func__가 body 시작에 암시적으로 정의되는데, 의미는 여기서 설명한 대로예요.
클로저 타입 (Closure type)
람다 표현식은 클로저 타입(closure type) 으로 알려진 고유한 이름 없는 비-union 비-집계 클래스 타입의 prvalue 표현식이에요. 클로저 타입은 (ADL 목적으로) 람다 표현식을 포함하는 가장 작은 블록 범위, 클래스 범위, 또는 네임스페이스 범위에서 선언돼요.
클로저 타입은 captures 가 비어 있을 때에만 구조적 타입(structural type) 이에요. (C++20부터)
클로저 타입은 다음 멤버들을 가지는데, 이들은 명시적으로 인스턴스화되거나, 명시적으로 특수화되거나, friend 선언에서 이름이 붙을 수 없어요 (C++14부터).
ClosureType::operator()(params)
ret operator()(params) { body }
(static과 const가 있을 수 있어요, 아래 참조)
template<template-params>
ret operator()(params) { body }
(C++14부터, 제네릭 람다, static과 const가 있을 수 있어요)
호출되면 람다 표현식의 body 를 실행해요. 변수에 접근할 때는 (복사로 캡처된 개체면) 캡처된 복사본에, (참조로 캡처된 개체면) 원래 객체에 접근해요.
operator()의 매개변수 목록은 params가 제공되면 그것이고, 그렇지 않으면 비어 있어요.
operator()의 반환 타입은 trailing에 지정된 타입이에요. trailing이 제공되지 않으면 반환 타입은 자동으로 추론돼요.[1]
람다 지정자에 mutable 키워드가 없거나, 명시적 객체 매개변수가 없으면(C++23부터), operator()의 cv-한정자는 const이고 복사로 캡처된 객체들은 이 operator() 내부에서 수정할 수 없어요. 명시적 const 한정자는 허용되지 않아요. operator()는 절대 virtual 이 아니고 volatile 한정자를 가질 수 없어요.
operator()는 constexpr 함수의 요구 사항을 만족하면 항상 constexpr 이에요. 람다 지정자에 constexpr 키워드가 쓰였어도 constexpr 이에요. (C++17부터)
operator()는 람다 지정자에 consteval 키워드가 쓰였으면 즉시 함수예요. (C++20부터)
operator()는 람다 지정자에 static 키워드가 쓰였으면 정적 멤버 함수예요. (C++23부터)
operator()는 params에 명시적 객체 매개변수가 있으면 명시적 객체 멤버 함수예요. (C++23부터)
params에서 타입이 auto로 지정된 각 매개변수에 대해, 발명된 템플릿 매개변수가 나타나는 순서대로 template-params에 추가돼요. 발명된 템플릿 매개변수는 params의 해당 함수 멤버가 함수 매개변수 팩이면 매개변수 팩일 수 있어요.
// generic lambda, operator() is a template with two parameters
auto glambda = [](auto a, auto&& b) { return a < b; };
bool b = glambda(3, 3.14); // OK
// generic lambda, operator() is a template with one parameter
auto vglambda = [](auto printer)
{
return [=](auto&&... ts) // generic lambda, ts is a parameter pack
{
printer(std::forward<decltype(ts)>(ts)...);
// nullary lambda (takes no parameters):
return [=] { printer(ts...); };
};
};
auto p = vglambda([](auto v1, auto v2, auto v3)
{
std::cout << v1 << v2 << v3;
});
auto q = p(1, 'a', 3.14); // outputs 1a3.14
q(); // outputs 1a3.14
(C++14부터)
람다 정의가 명시적 템플릿 매개변수 목록을 쓰면 그 목록이 operator()에 사용돼요. params에서 타입이 auto로 지정된 각 매개변수에 대해, 추가 발명 템플릿 매개변수가 그 목록 끝에 덧붙여져요.
// generic lambda, operator() is a template with two parameters
auto glambda = []<class T>(T a, auto&& b) { return a < b; };
// generic lambda, operator() is a template with one parameter pack
auto f = []<typename... Ts>(Ts&&... ts)
{
return foo(std::forward<Ts>(ts)...);
};
(C++20부터)
람다 표현식의 except 예외 사양은 operator()에 적용돼요.
이름 룩업, this 포인터의 타입과 값 결정, 비정적 클래스 멤버 접근을 위한 목적으로, 클로저 타입 operator()의 본문은 람다 표현식의 맥락에서 고려돼요.
struct X
{
int x, y;
int operator()(int);
void f()
{
// the context of the following lambda is the member function X::f
[=]() -> int
{
return operator()(this->x + y); // X::operator()(this->x + (*this).y)
// this has type X*
};
}
};
매달린 참조 (Dangling references)
비참조 개체를 참조로, 암시적이든 명시적이든, 캡처했는데 클로저 객체의 operator()가 그 개체의 수명이 끝난 뒤에 호출되면 미정의 동작이 일어나요. C++ 클로저는 참조로 캡처된 객체의 수명을 연장하지 않아요. this로 캡처된 현재 *this 객체의 수명에도 똑같이 적용돼요.
[1] 함수 반환 타입 추론은 C++14에서 도입됐지만, 그 규칙은 C++11에서 람다 반환 타입 추론에 이미 쓸 수 있었어요.
ClosureType::operator ret(*)(params)()
멤버 함수 포인터로의 변환. captures 없는 비제네릭 람다:
using F = ret(*)(params);
operator F() const noexcept;
(C++17까지)
using F = ret(*)(params);
constexpr operator F() const noexcept;
(C++17부터)
captures 없는 제네릭 람다:
template<template-params> using fptr_t = /* see below */;
template<template-params>
operator fptr_t<template-params>() const noexcept;
(C++14부터, C++17까지)
template<template-params> using fptr_t = /* see below */;
template<template-params>
constexpr operator fptr_t<template-params>() const noexcept;
(C++17부터)
이 사용자 정의 변환 함수는 람다 표현식에 captures 가 없고 명시적 객체 매개변수도 없을 때만(C++23부터) 정의돼요. 클로저 객체의 public, constexpr(C++17부터), 비-virtual, 비-explicit, const noexcept 멤버 함수예요.
이 함수는 함수 호출 연산자(제네릭 람다의 경우 특수화)가 즉시 함수면 즉시 함수예요. (C++20부터)
제네릭 captures 없는 람다는 operator()와 같은 발명 템플릿 매개변수 목록을 가진 사용자 정의 변환 함수 템플릿을 가져요.
void f1(int (*)(int)) {}
void f2(char (*)(int)) {}
void h(int (*)(int)) {} // #1
void h(char (*)(int)) {} // #2
auto glambda = [](auto a) { return a; };
f1(glambda); // OK
f2(glambda); // error: not convertible
h(glambda); // OK: calls #1 since #2 is not convertible
int& (*fpi)(int*) = [](auto* a) -> auto& { return *a; }; // OK
(C++14부터)
변환 함수가 반환하는 값은 C++ 언어 링키지를 가진 함수 포인터로, 호출되면 클로저 타입의 기본 생성 인스턴스에서 클로저 타입의 함수 호출 연산자를 호출하는 것과 같은 효과를 가져요.
- (C++23부터)
operator()가 static 이면, 그operator()를 가리키는 C++ 언어 링키지 포인터. 그렇지 않으면 C++ 언어 링키지 함수 포인터로, 호출되면 (비제네릭 람다의 경우) 클로저 타입의 기본 생성 인스턴스에서 클로저 타입의operator()를 호출하는 것, (제네릭 람다의 경우) 클로저 타입의 기본 생성 인스턴스에서 제네릭 람다의 대응하는operator()특수화를 호출하는 것과 같은 효과를 가져요. - (C++14부터, C++23까지) 비제네릭 람다는 클로저 타입의 기본 생성 인스턴스에서 클로저 타입의
operator()를, 제네릭 람다는 기본 생성 인스턴스에서 대응하는operator()특수화를 호출하는 것과 같은 효과.
이 함수는 함수 호출 연산자(제네릭 람다의 경우 특수화)가 constexpr 이면 constexpr 이에요.
auto Fwd = [](int(*fp)(int), auto a) { return fp(a); };
auto C = [](auto a) { return a; };
static_assert(Fwd(C, 3) == 3); // OK
auto NC = [](auto a) { static int s; return a; };
static_assert(Fwd(NC, 3) == 3); // error: no specialization can be
// constexpr because of static s
클로저 객체의 operator()가 비-예외 예외 사양을 가지면, 이 함수가 반환하는 포인터는 noexcept 함수 포인터 타입이에요. (C++17부터)
ClosureType::ClosureType()
ClosureType() = default;
(C++20부터, captures 가 지정되지 않았을 때만)
ClosureType(const ClosureType&) = default;
ClosureType(ClosureType&&) = default;
클로저 타입은 기본 생성 가능하지 않아요. 기본 생성자가 없어요. (C++20까지)
captures 가 지정되지 않으면 클로저 타입은 defaulted 기본 생성자를 가져요. 그렇지 않으면 기본 생성자가 없어요 (capture-default 가 있고 실제로 아무것도 캡처하지 않는 경우도 포함해요). (C++20부터)
복사 생성자와 이동 생성자는 defaulted 로 선언되고, 복사 생성자·이동 생성자의 보통 규칙에 따라 암시적으로 정의될 수 있어요.
ClosureType::operator=(const ClosureType&)
ClosureType& operator=(const ClosureType&) = delete;
(C++20까지)
ClosureType& operator=(const ClosureType&) = default;
ClosureType& operator=(ClosureType&&) = default;
(C++20부터, captures 가 지정되지 않았을 때만)
ClosureType& operator=(const ClosureType&) = delete;
(C++20부터, 그 외 경우)
복사 대입 연산자는 삭제된 것으로 정의돼요 (이동 대입 연산자는 선언되지 않아요). 클로저 타입은 복사 대입 가능(CopyAssignable)하지 않아요. (C++20까지)
captures 가 지정되지 않으면 클로저 타입은 defaulted 복사 대입 연산자와 defaulted 이동 대입 연산자를 가져요. 그렇지 않으면 삭제된 복사 대입 연산자를 가져요 (capture-default 가 있고 실제로 아무것도 캡처하지 않는 경우도 포함해요). (C++20부터)
ClosureType::~ClosureType()
~ClosureType() = default;
소멸자는 암시적으로 선언돼요.
ClosureType::Captures
T1 a;
T2 b;
...
람다 표현식이 무엇이든 복사로 캡처하면(캡처 절 [=]로 암시적으로든, & 문자를 포함하지 않는 캡처 [a, b, c]로 명시적으로든), 클로저 타입은 지정되지 않은 순서로 선언되는 이름 없는 비정적 데이터 멤버를 포함해요. 이 멤버들은 그렇게 캡처된 모든 개체의 복사본을 담아요.
이니셜라이저가 없는 캡처에 해당하는 데이터 멤버는 람다 표현식이 평가될 때 직접 초기화돼요. 이니셜라이저가 있는 캡처에 해당하는 것들은 이니셜라이저가 요구하는 대로 초기화돼요 (복사 또는 직접 초기화일 수 있어요). 배열이 캡처되면 배열 요소들은 증가하는 인덱스 순서로 직접 초기화돼요. 데이터 멤버가 초기화되는 순서는 선언된 순서예요 (지정되지 않음).
각 데이터 멤버의 타입은 대응하는 캡처된 개체의 타입이에요. 단 개체가 참조 타입이면 예외가 있어요 (그 경우 함수에 대한 참조는 참조된 함수에 대한 lvalue 참조로 캡처되고, 객체에 대한 참조는 참조된 객체의 복사본으로 캡처돼요).
(capture-default [&] 또는 & 문자 사용, 예: [&a, &b, &c]로) 참조로 캡처된 개체에 대해, 추가 데이터 멤버가 클로저 타입에 선언되는지 여부는 지정되지 않아요. 단 그런 추가 멤버들은 LiteralType 을 만족해야 해요 (C++17부터).
람다 표현식은 비평가 표현식, 템플릿 인자, 별칭 선언, typedef 선언, 그리고 함수(또는 함수 템플릿) 선언에서 함수 본문과 함수의 기본 인자를 제외한 어디에도 허용되지 않아요. (C++20까지)
람다 캡처 (Lambda capture)
captures 는 람다 함수 본문 안에서 접근할 수 있는 외부 변수를 정의해요. 그 문법은 이렇게 정의돼요.
capture-default (1)
capture-list (2)
capture-default , capture-list (3)
- capture-default —
&와=중 하나. - capture-list — capture 들의 쉼표로 구분된 목록.
capture 의 문법은 이렇게 정의돼요.
| 형태 | 종류 |
|---|---|
identifier |
(1) 단순 복사 캡처 |
identifier ... |
(2) 팩 확장인 단순 복사 캡처 |
identifier initializer |
(3) 이니셜라이저가 있는 복사 캡처 (C++14부터) |
& identifier |
(4) 단순 참조 캡처 |
& identifier ... |
(5) 팩 확장인 단순 참조 캡처 |
& identifier initializer |
(6) 이니셜라이저가 있는 참조 캡처 (C++14부터) |
this |
(7) 현재 객체의 단순 참조 캡처 |
* this |
(8) 현재 객체의 단순 복사 캡처 (C++17부터) |
... identifier initializer |
(9) 팩 확장인 이니셜라이저가 있는 복사 캡처 (C++20부터) |
& ... identifier initializer |
(10) 팩 확장인 이니셜라이저가 있는 참조 캡처 (C++20부터) |
capture-default 가 &이면, 뒤의 단순 캡처는 &로 시작하면 안 돼요.
struct S2 { void f(int i); };
void S2::f(int i)
{
[&] {}; // OK: by-reference capture default
[&, i] {}; // OK: by-reference capture, except i is captured by copy
[&, &i] {}; // Error: by-reference capture when by-reference is the default
[&, this] {}; // OK, equivalent to [&]
[&, this, i] {}; // OK, equivalent to [&, i]
}
capture-default 가 =이면, 뒤의 단순 캡처는 &로 시작하거나 *this(C++17부터) 또는 this(C++20부터)여야 해요.
struct S2 { void f(int i); };
void S2::f(int i)
{
[=] {}; // OK: by-copy capture default
[=, &i] {}; // OK: by-copy capture, except i is captured by reference
[=, *this] {}; // until C++17: Error: invalid syntax
// since C++17: OK: captures the enclosing S2 by copy
[=, this] {}; // until C++20: Error: this when = is the default
// since C++20: OK, same as [=]
}
어떤 캡처든 한 번만 나타날 수 있고, 그 이름은 어떤 매개변수 이름과도 달라야 해요.
struct S2 { void f(int i); };
void S2::f(int i)
{
[i, i] {}; // Error: i repeated
[this, *this] {}; // Error: "this" repeated (C++17)
[i] (int i) {}; // Error: parameter and capture have the same name
}
람다 표현식은 다음 변수라면 캡처 없이 쓸 수 있어요.
- 비지역 변수이거나 정적 또는 스레드 지역 저장 기간을 가진 변수 (그 경우 변수를 캡처할 수 없어요), 또는
- 상수 표현식으로 초기화된 참조.
람다 표현식은 다음 변수라면 캡처 없이 그 값을 읽을 수 있어요.
- 상수 표현식으로 초기화된 const 비휘발성 정수 또는 열거 타입이거나,
constexpr이고 mutable 멤버가 없는 변수.
현재 객체(*this)는 캡처 기본값이 있으면 암시적으로 캡처될 수 있어요. 암시적으로 캡처되면 capture-default 가 =여도 항상 참조로 캡처돼요. capture-default 가 =일 때 *this의 암시적 캡처는 폐기 예정(deprecated)이에요. (C++20부터)
다음 조건 중 하나를 만족하는 람다 표현식만 capture-default 나 이니셜라이저 없는 캡처를 가질 수 있어요.
- 가장 안쪽의 둘러싸는 범위가 블록 범위이다.
- 기본 멤버 이니셜라이저 안에 나타나고 가장 안쪽 둘러싸는 범위가 그에 대응하는 클래스 범위이다.
- 계약 어서션 안에 나타나고 가장 안쪽 둘러싸는 범위가 대응하는 계약-어서션 범위이다. (C++26부터)
그런 람다 표현식에 대해, 도달 범위(reaching scope) 는 가장 안쪽 둘러싸는 함수(와 그 매개변수)를 포함하기까지의 둘러싸는 범위들의 집합으로 정의돼요. 여기엔 중첩 블록 범위와, 이 람다가 중첩돼 있으면 둘러싸는 람다들의 범위가 포함돼요.
이니셜라이저 없는 캡처(this-캡처 제외)의 식별자는 람다의 도달 범위에서 보통 비한정 이름 룩업으로 찾아져요. 룩업 결과는 도달 범위에 선언된 자동 저장 기간 변수이거나, 그 대응 변수가 그런 요구를 만족하는 구조적 바인딩이어야 해요 (C++20부터). 그 개체는 명시적으로 캡처돼요.
이니셜라이저가 있는 캡처, init-capture 는 타입 지정자 auto와 같은 이니셜라이저로 선언된 변수를 선언하고 명시적으로 캡처하는 것처럼 행동해요. 그 변수의 선언 영역(declarative region)은 람다 표현식의 본문이에요 (즉 이니셜라이저 안에서는 범위에 있지 않아요). 단,
- 복사 캡처이면, 클로저 객체의 도입된 비정적 데이터 멤버는 그 변수를 가리키는 또 다른 방식이에요. 즉 원본 변수는 실제로 존재하지 않고,
auto를 통한 타입 추론과 초기화가 비정적 데이터 멤버에 적용돼요. - 참조 캡처이면, 참조 변수의 수명은 클로저 객체의 수명이 끝날 때 끝나요.
이것은 x = std::move(x) 같은 캡처로 이동 전용 타입을 캡처하는 데 쓰여요. 또 &cr = std::as_const(x) 같은 것으로 const 참조로 캡처하는 것도 가능하게 해요.
int x = 4;
auto y = [&r = x, x = x + 1]() -> int
{
r += 2;
return x * x;
}(); // updates ::x to 6 and initializes y to 25.
(C++14부터)
captures 가 capture-default 를 가지고 둘러싸는 객체(this 또는 *this 로), 또는 람다 본문에서 odr-사용 가능한 자동 변수, 또는 대응 변수가 원자 저장 기간을 가진 구조적 바인딩을(C++20부터) 명시적으로 캡처하지 않으면, 어떤 개체가 표현식 내부의 잠재적으로 평가되는 표현식에서 이름이 붙을 때(비정적 클래스 멤버 사용 전에 암시적 this->가 더해지는 경우 포함) 그 개체를 암시적으로 캡처해요.
암시적 캡처를 결정하기 위해, typeid는 자신의 피연산자를 비평가로 만드는 것으로 절대 간주되지 않아요.
개체는 람다 본문 인스턴스화 후 버려진 문(statement) 안에서만 이름이 붙어도 암시적으로 캡처될 수 있어요. (C++17부터)
void f(int, const int (&)[2] = {}) {} // #1
void f(const int&, const int (&)[1]) {} // #2
struct NoncopyableLiteralType
{
constexpr explicit NoncopyableLiteralType(int n) : n_(n) {}
NoncopyableLiteralType(const NoncopyableLiteralType&) = delete;
int n_;
};
void test()
{
const int x = 17;
auto l0 = []{ f(x); }; // OK: calls #1, does not capture x
auto g0 = [](auto a) { f(x); }; // same as above
auto l1 = [=]{ f(x); }; // OK: captures x (since P0588R1) and calls #1
// the capture can be optimized away
auto g1 = [=](auto a) { f(x); }; // same as above
auto ltid = [=]{ typeid(x); }; // OK: captures x (since P0588R1)
// even though x is unevaluated
// the capture can be optimized away
auto g2 = [=](auto a)
{
int selector[sizeof(a) == 1 ? 1 : 2] = {};
f(x, selector); // OK: is a dependent expression, so captures x
};
auto g3 = [=](auto a)
{
typeid(a + x); // captures x regardless of
// whether a + x is an unevaluated operand
};
constexpr NoncopyableLiteralType w{42};
auto l4 = []{ return w.n_; }; // OK: w is not odr-used, capture is unnecessary
// auto l5 = [=]{ return w.n_; }; // error: w needs to be captured by copy
}
람다 본문이 복사로 캡처된 개체를 odr-사용하면 클로저 타입의 멤버가 접근돼요. 개체를 odr-사용하지 않으면 접근은 원래 객체로 가요.
void f(const int*);
void g()
{
const int N = 10;
[=]
{
int arr[N]; // not an odr-use: refers to g's const int N
f(&N); // odr-use: causes N to be captured (by copy)
// &N is the address of the closure object's member N, not g's N
}();
}
람다가 참조로 캡처된 참조를 odr-사용하면, 캡처된 참조 자체가 아니라 원래 참조가 가리키는 객체를 사용해요.
#include <iostream>
auto make_function(int& x)
{
return [&] { std::cout << x << '\n'; };
}
int main()
{
int i = 3;
auto f = make_function(i); // the use of x in f binds directly to i
i = 5;
f(); // OK: prints 5
}
capture-default =가 있는 람다 본문 안에서, 어떤 캡처 가능한 개체의 타입은 마치 캡처된 것처럼 돼요 (그래서 람다가 mutable 이 아니면 const-한정이 자주 더해져요). 심지어 그 개체가 비평가 피연산자에 있고 캡처되지 않아도(예: decltype 안에서) 그래요.
void f3()
{
float x, &r = x;
[=]
{ // x and r are not captured (appearance in a decltype operand is not an odr-use)
decltype(x) y1; // y1 has type float
decltype((x)) y2 = y1; // y2 has type float const& because this lambda
// is not mutable and x is an lvalue
decltype(r) r1 = y1; // r1 has type float& (transformation not considered)
decltype((r)) r2 = y2; // r2 has type float const&
};
}
람다에 의해 캡처된 (암시적이든 명시적이든) 어떤 개체든 람다 표현식에 의해 odr-사용돼요 (그래서 중첩 람다에 의한 암시적 캡처는 둘러싸는 람다의 암시적 캡처를 일으켜요).
모든 암시적으로 캡처된 변수는 람다의 도달 범위 안에 선언되어야 해요.
람다가 둘러싸는 객체(this 또는 *this 로)를 캡처하면, 가장 가까운 둘러싸는 함수가 비정적 멤버 함수이거나 람다가 기본 멤버 이니셜라이저 안에 있어야 해요.
struct s2
{
double ohseven = .007;
auto f() // nearest enclosing function for the following two lambdas
{
return [this] // capture the enclosing s2 by reference
{
return [*this] // capture the enclosing s2 by copy (C++17)
{
return ohseven; // OK
}
}();
}
auto g()
{
return [] // capture nothing
{
return [*this] {}; // error: *this not captured by outer lambda expression
}();
}
};
람다 표현식(또는 제네릭 람다의 함수 호출 연산자의 특수화)(C++14부터)이 *this나 자동 저장 기간 변수를 odr-사용하면, 반드시 람다 표현식에 의해 캡처되어야 해요.
void f1(int i)
{
int const N = 20;
auto m1 = [=]
{
int const M = 30;
auto m2 = [i]
{
int x[N][M]; // N and M are not odr-used
// (ok that they are not captured)
x[0][0] = i; // i is explicitly captured by m2
// and implicitly captured by m1
};
};
struct s1 // local class within f1()
{
int f;
void work(int n) // non-static member function
{
int m = n * n;
int j = 40;
auto m3 = [this, m]
{
auto m4 = [&, j] // error: j is not captured by m3
{
int x = n; // error: n is implicitly captured by m4
// but not captured by m3
x += m; // OK: m is implicitly captured by m4
// and explicitly captured by m3
x += i; // error: i is outside of the reaching scope
// (which ends at work())
x += f; // OK: this is captured implicitly by m4
// and explicitly captured by m3
};
};
}
};
}
클래스 멤버는 이니셜라이저 없는 캡처로 명시적으로 캡처될 수 없어요 (앞서 말했듯 capture-list 에는 변수만 허용돼요).
class S
{
int x = 0;
void f()
{
int i = 0;
// auto l1 = [i, x] { use(i, x); }; // error: x is not a variable
auto l2 = [i, x = x] { use(i, x); }; // OK, copy capture
i = 1; x = 1; l2(); // calls use(0,0)
auto l3 = [i, &x = x] { use(i, x); }; // OK, reference capture
i = 2; x = 2; l3(); // calls use(1,2)
}
};
람다가 암시적 복사 캡처로 멤버를 캡처하면 그 멤버 변수의 복사본을 만들지 않아요. 멤버 변수 m의 사용은 표현식 (*this).m으로 취급되고, *this는 항상 암시적으로 참조로 캡처돼요.
class S
{
int x = 0;
void f()
{
int i = 0;
auto l1 = [=] { use(i, x); }; // captures a copy of i and
// a copy of the this pointer
i = 1; x = 1; l1(); // calls use(0, 1), as if
// i by copy and x by reference
auto l2 = [i, this] { use(i, x); }; // same as above, made explicit
i = 2; x = 2; l2(); // calls use(1, 2), as if
// i by copy and x by reference
auto l3 = [&] { use(i, x); }; // captures i by reference and
// a copy of the this pointer
i = 3; x = 2; l3(); // calls use(3, 2), as if
// i and x are both by reference
auto l4 = [i, *this] { use(i, x); }; // makes a copy of *this,
// including a copy of x
i = 4; x = 4; l4(); // calls use(3, 2), as if
// i and x are both by copy
}
};
람다 표현식이 기본 인자에 나타나면, 모든 캡처가 기본 인자에 나타날 수 있는 표현식의 제약을 만족하는 이니셜라이저를 가지지 않는 한(C++14부터), 어떤 것이든 명시적이거나 암시적으로 캡처할 수 없어요.
void f2()
{
int i = 1;
void g1( int = [i] { return i; }() ); // error: captures something
void g2( int = [i] { return 0; }() ); // error: captures something
void g3( int = [=] { return i; }() ); // error: captures something
void g4( int = [=] { return 0; }() ); // OK: capture-less
void g5( int = [] { return sizeof i; }() ); // OK: capture-less
// C++14
void g6( int = [x = 1] { return x; }() ); // OK: 1 can appear
// in a default argument
void g7( int = [x = i] { return x; }() ); // error: i cannot appear
// in a default argument
}
익명 공용체(anonymous union)의 멤버는 캡처될 수 없어요. 비트 필드는 복사로만 캡처될 수 있어요.
중첩 람다 m2가 바로 둘러싸는 람다 m1도 캡처하는 것을 캡처하면, m2의 캡처는 이렇게 변환돼요.
- 둘러싸는 람다
m1이 복사로 캡처하면,m2는 원래 변수나*this가 아니라m1클로저 타입의 비정적 멤버를 캡처해요.m1이 mutable 이 아니면 그 비정적 데이터 멤버는 const-한정된 것으로 간주돼요. - 둘러싸는 람다
m1이 참조로 캡처하면,m2는 원래 변수나*this를 캡처해요.
#include <iostream>
int main()
{
int a = 1, b = 1, c = 1;
auto m1 = [a, &b, &c]() mutable
{
auto m2 = [a, b, &c]() mutable
{
std::cout << a << b << c << '\n';
a = 4; b = 4; c = 4;
};
a = 3; b = 3; c = 3;
m2();
};
a = 2; b = 2; c = 2;
m1(); // calls m2() and prints 123
std::cout << a << b << c << '\n'; // prints 234
}
람다가 무엇이든 캡처하면, 함수 호출 연산자의 명시적 객체 매개변수 타입(있으면)은 다음만 될 수 있어요. (C++23부터)
- 클로저 타입,
- 클로저 타입에서 공개적으로 모호하지 않게 파생된 클래스 타입, 또는
- 가능하면 cv-한정된 그런 타입에 대한 참조.
struct C
{
template<typename T>
C(T);
};
void func(int i)
{
int x = [=](this auto&&) { return i; }(); // OK
int y = [=](this C) { return i; }(); // error
int z = [](this C) { return 42; }(); // OK
auto lambda = [n = 42] (this auto self) { return n; };
using Closure = decltype(lambda);
struct D : private Closure {
D(Closure l) : Closure(l) {}
using Closure::operator();
friend Closure;
};
D{lambda}(); // error
}
참고 (Notes)
기능 테스트 매크로는 이래요.
| 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_lambdas |
200907L |
C++11 | 람다 표현식 |
__cpp_generic_lambdas |
201304L |
C++14 | 제네릭 람다 표현식 |
201707L |
C++20 | 제네릭 람다의 명시적 템플릿 매개변수 목록 | |
__cpp_init_captures |
201304L |
C++14 | 람다 init-capture |
201803L |
C++20 | 람다 init-capture 에서 팩 확장 허용 | |
__cpp_capture_star_this |
201603L |
C++17 | [=, *this] 로서의 *this 값 캡처 |
__cpp_constexpr |
201603L |
C++17 | constexpr 람다 |
__cpp_static_call_operator |
202207L |
C++23 | captures 없는 람다의 static operator() |
암시적 람다 캡처 규칙은 결함 보고 P0588R1 로 약간 바뀌었어요. 2023-10 기준으로 일부 주요 구현이 완전히 DR 을 반영하지 않아서, odr-사용을 감지하던 옛 규칙이 일부 경우에 여전히 쓰여요.
P0588R1 이전의 옛 규칙 — captures 가 capture-default 를 가지고 둘러싸는 객체(this 또는 *this 로)나, 람다 본문에서 odr-사용 가능한 자동 변수나, 대응 변수가 원자 저장 기간을 가진 구조적 바인딩을(C++20부터) 명시적으로 캡처하지 않으면, 그 개체가 (C++14부터) 제네릭 람다의 템플릿 매개변수에 의존하는 표현식 내부의 잠재적으로 평가되는 표현식에서 이름이 붙거나, 람다 본문에 의해 odr-사용되면 암시적으로 캡처해요.
예제 (Example)
이 예는 람다를 제네릭 알고리즘에 넘기는 방법과 람다 표현식에서 나온 객체를 std::function 객체에 저장하는 방법을 보여 줘요.
#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
int main()
{
std::vector<int> c{1, 2, 3, 4, 5, 6, 7};
int x = 5;
c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; }), c.end());
std::cout << "c: ";
std::for_each(c.begin(), c.end(), [](int i) { std::cout << i << ' '; });
std::cout << '\n';
// the type of a closure cannot be named, but can be inferred with auto
// since C++14, lambda could own default arguments
auto func1 = [](int i = 6) { return i + 4; };
std::cout << "func1: " << func1() << '\n';
// like all callable objects, closures can be captured in std::function
// (this may incur unnecessary overhead)
std::function<int(int)> func2 = [](int i) { return i + 4; };
std::cout << "func2: " << func2(6) << '\n';
constexpr int fib_max {8};
std::cout << "Emulate `recursive lambda` calls:\nFibonacci numbers: ";
auto nth_fibonacci = [](int n)
{
std::function<int(int, int, int)> fib = [&](int n, int a, int b)
{
return n ? fib(n - 1, a + b, a) : b;
};
return fib(n, 0, 1);
};
for (int i{1}; i <= fib_max; ++i)
std::cout << nth_fibonacci(i) << (i < fib_max ? ", " : "\n");
std::cout << "Alternative approach to lambda recursion:\nFibonacci numbers: ";
auto nth_fibonacci2 = [](auto self, int n, int a = 0, int b = 1) -> int
{
return n ? self(self, n - 1, a + b, a) : b;
};
for (int i{1}; i <= fib_max; ++i)
std::cout << nth_fibonacci2(nth_fibonacci2, i) << (i < fib_max ? ", " : "\n");
#ifdef __cpp_explicit_this_parameter
std::cout << "C++23 approach to lambda recursion:\n";
auto nth_fibonacci3 = [](this auto self, int n, int a = 0, int b = 1) -> int
{
return n ? self(n - 1, a + b, a) : b;
};
for (int i{1}; i <= fib_max; ++i)
std::cout << nth_fibonacci3(i) << (i < fib_max ? ", " : "\n");
#endif
}
가능한 출력:
c: 5 6 7
func1: 10
func2: 10
Emulate `recursive lambda` calls:
Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13
Alternative approach to lambda recursion:
Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13
결함 보고 (Defect reports)
동작 변경 결함 보고로 주요하게 조정된 내용은 이래요. 람다 매개변수 목록의 기본 인자 허용(CWG 974), 반환 타입 추론 개선(CWG 1048), 비-mutable 둘러싸는 람다의 캡처된 멤버가 const 로 간주됨(CWG 1249), 변환 함수 반환 함수 타입이 C++ 언어 링키지를 가짐(CWG 1557), 함수·함수 템플릿 시그니처에서 람다 금지(CWG 1607), 익명 공용체 멤버 캡처 금지(CWG 1612), captures 없는 람다의 변환 함수가 noexcept 가 됨(CWG 1722), __func__가 클로저 클래스의 operator()를 가리킴(CWG 1772), 제네릭 람다 클로저 멤버의 명시적 인스턴스화·특수화 금지(CWG 1780), 클로저에 기본 생성자가 없고 복사/이동 생성자가 defaulted 됨(CWG 1891), 변환 함수 결과 호출 효과의 기본 생성 인스턴스 명시(CWG 1937), operator() 매개변수 목록이 params만 가리킬 수 있게 됨(CWG 1973), 참조 캡처가 원래 참조된 개체를 가리킴(CWG 2011), 함수 참조의 복사 캡처 명확화(CWG 2095), 캡처 이름과 매개변수 이름이 같으면 규칙 위반(CWG 2211), 기본 인자에서 이니셜라이저가 있는 캡처 허용(CWG 2358), 각 지정자의 중복 금지(CWG 2509), 명시적 객체 매개변수 람다의 함수 포인터 변환 제거(CWG 2561), 비공개·모호 상속에서 명시적 매개변수 operator() 인스턴스화 금지(CWG 2881), 암시적 캡처 규칙 단순화(P0588R1) 등이에요.