#!if 문서명2 != null
, [[함수형 프로그래밍]]
#!if 문서명3 != null
, [[람다식]]
#!if 문서명4 != null
, [[]]
#!if 문서명5 != null
, [[]]
#!if 문서명6 != null
, [[]]
프로그래밍 언어 문법 | |
{{{#!folding [ 펼치기 · 접기 ] {{{#!wiki style="margin: 0 -10px -5px; word-break: keep-all" | 프로그래밍 언어 문법 C(포인터 · 구조체 · size_t) · C++(이름공간 · 클래스 · 특성 · 상수 표현식 · 람다 표현식 · 템플릿/제약조건/메타 프로그래밍) · C# · Java · Python(함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript(표준 내장 객체, this) · Haskell(모나드) · 숨 |
마크업 언어 문법 HTML · CSS | |
개념과 용어 함수(인라인 함수 · 고차 함수 · 콜백 함수 · 람다식) · 리터럴 · 문자열 · 식별자(예약어) · 상속 · 예외 · 조건문 · 반복문 · 비트 연산 · 참조에 의한 호출 · eval · 네임스페이스 · 호이스팅 | |
기타 #! · == · === · deprecated · GOTO · NaN · null · undefined · 배커스-나우르 표기법 | }}}}}} |
프로그래밍 언어 목록 · 분류 · 문법 · 예제 |
1. 개요
평가-지시자 auto 표현식-식별자 = [ 람다-포섭... ] < 템플릿-매개변수1 , 템플릿-매개변수2 ...> [[특성]] ( 매개변수-자료형1 매개변수1, 매개변수-자료형2 매개변수2, ...) [[클로저-특성]] {{{#DodgerBlue,#CornFlowerBlue ' noexcept '}}}(예외-명세...) -> 반환-자료형 { {{{#DodgerBlue,#CornFlowerBlue ' return '}}} 반환 값; }; |
람다 표현식, 혹은 익명 함수는 C++11에서 추가된 새로운 문법으로써 언제, 어디에서나 실행시킬 수 있는 함수를 만들고, 매개변수로 전달하고, 제어하는 기능이다. 람다 표현식은 함수와 똑같은 방식으로 사용할 수 있다. 또한 복사와 이동이 자유롭다.
auto
와 함께 일반 사용자들이 가장 접하게 될 모던 C++의 요소다. 이 문서에서는 익명 함수에 대한 개략, 작성 방법, 간단한 예제와 활용하는 방법을 알아보도록 한다.2. 매개변수와 반환 자료형
#!syntax cpp
int main()
{
// (1) 정수를 반환하는 람다 표현식
auto lambda0 = [] { return 1; };
// `lambda0_result`는 1
auto lambda0_result = lambda0();
}
먼저 가장 간단한 람다식의 형태를 소개한다. 람다 표현식 `lambda0`
은 가장 간단한 형태로서 아무런 매개변수 없이 반환문만 존재한다. 람다식을 사용할 때는 함수와 똑같이 ()
를 붙여 실행할 수 있다.#!syntax cpp
int main()
{
// (2) 정수를 반환하는 람다 표현식
auto lambda1 = [] -> unsigned long { return 10UL; };
// `lambda1_result`는 10ul
auto lambda1_result = lambda1();
// (3) 실수를 반환하는 람다 표현식
// 이 람다 표현식은 정수를 실수로 변환한다.
auto lambda2 = [] -> double { return 10; };
// `lambda2_result`는 10.0
auto lambda2_result = lambda2();
}
대괄호 []
뒤에 ->
(화살표 연산자)와 함께 반환 자료형을 명시할 수 있다. `lambda1`
은 반환 자료형을 명시한 예시다. 이 경우 내부의 반환문과 명시한 반환 자료형의 차이가 없다. `lambda2`
는 반환 자료형을 내부의 반환문과 다르게 명시한 예시다. 이 경우엔 static_cast<double>
로 변환되며 10.0
을 반환한다.#!syntax cpp
int main()
{
std::string left{ "Left" };
std::string right{ "Right" };
// (4) auto로 인자를 받아서 더한 뒤 반환하는 람다 표현식
auto lambda3 = [](auto lhs, auto rhs) -> auto { return lhs + rhs; };
// const char[7]
auto str = "Tail";
// `lambda4_result_a`는 std::string
// `lambda4_result_a`의 값은 "LeftTail"
auto lambda3_result_a = lambda3(left, str);
// `lambda4_result_b`는 std::string
// `lambda4_result_b`의 값은 "HeadRight"
auto lambda3_result_b = lambda3("Head", right);
// (5) decltype으로 반환 자료형을 명시한 람다 표현식
auto lambda4 = [](auto lhs, auto rhs) -> decltype(lhs + rhs) { return lhs + rhs; };
// `lambda4_result`는 std::string{ "LeftRight" }
auto lambda4_result = lambda4(left, right);
}
반환 자료형에는 일반 함수와 마찬가지로 auto
와 decltype(expression)
을 사용할 수 있다.#!syntax cpp
int main()
{
// (6) 반환형을 auto로 명시한 람다 표현식
auto lambda5 = [](float lhs, auto rhs) -> auto { return lhs * rhs; };
// `lambda5_result_a`는 float
// `lambda5_result_a`의 값은 30.0f
auto lambda5_result_a = lambda5(10.0f, 3.0f);
// `lambda5_result_b`는 float
// `lambda5_result_b`의 값은 50.0f
auto lambda5_result_b = lambda5(0.5, 100);
}
상기 코드에서는 인자의 자료형과 매개변수의 자료형이 같은 경우와 다른 경우를 보여주고 있다.여담으로 매개변수에
auto
, decltype(auto)
, 템플릿 매개변수가 포함된 람다 표현식은 일반화된 람다식(Generic Lambda)이라고 부른다. 거창한 의미는 아니고 템플릿 함수에서 봤던 경우를 람다 표현식에선 좀 특별하게 부르는 것이다.#!syntax cpp
import <numeric>;
struct [[nodiscard]] Rational
{
unsigned long long num = 0;
unsigned long long den = 0;
[[nodiscard]]
friend constexpr Rational operator+(const Rational& lhs, const Rational& rhs)
{
const auto lcm = std::lcm(lhs.den, rhs.den);
const auto ltm = lcm / lhs.den;
const auto rtm = lcm / rhs.den;
return Rational{ lhs.num * ltm + rhs.num * rtm, lcm };
}
friend constexpr Rational& operator+=(Rational& lhs, const Rational& rhs)
{
return (lhs = lhs + rhs);
}
[[nodiscard]]
friend constexpr Rational operator*(const Rational& lhs, const Rational& rhs)
{
const auto num = lhs.num * rhs.num;
const auto den = lhs.den * rhs.den;
const auto gcd = std::gcd(num, den);
return Rational{ num / gcd, den / gcd };
}
friend constexpr Rational& operator*=(Rational& lhs, const Rational& rhs)
{
return (lhs = lhs * rhs);
}
[[nodiscard]]
constexpr operator double() const
{
return static_cast<double>(num) / static_cast<double>(den);
}
};
int main()
{
constexpr Rational left{ 1, 10 }; // 10분의 1
constexpr Rational right{ 3, 7 }; // 7분의 3
// (7) auto로 인자를 받아서 곱한 뒤 반환하는 람다 표현식
constexpr auto lambda6 = [](auto lhs, auto rhs) -> auto { return lhs * rhs; };
// `lambda6_result`는 Rational
// `lambda6_result`의 값은 Rational{ 3, 70 };
constexpr auto lambda6_result = lambda6(left, right);
// (8) 완벽한 전달로 인자를 받아서 더한 뒤 반환하는 람다 표현식
constexpr auto lambda7 = [](auto&& lhs, auto&& rhs) -> decltype(lhs += rhs) { return lhs += rhs; };
// 상동
constexpr auto lambda7_alt = [](auto&& lhs, auto&& rhs) -> decltype(auto) { return lhs += rhs; };
Rational next{ 5, 9 };
// `lambda7_result`는 Rational&
// `lambda7_result`의 값은 Rational{ 62, 63 };
auto&& lambda7_result = lambda7(next, right);
}
람다 표현식에서도 완벽한 매개변수 전달을 할 수 있다. 상기 코드에서는 좌측값 참조자 &
를 유지하면서 반환하는 람다 표현식을 보여주고 있다. 만약 완벽한 전달을 하지 않았더라면 Rational&
대신 Rational
이 복사되어 반환됐을 것이다.#!syntax cpp
int main()
{
// (9) 정수를 매개변수로 받아서 출력하는 람다 표현식
auto lambda8 = [](int value) -> void { std::println(value); };
std::vector list{ 0, 1, 2, 3, 4 }; // std::vector<int>
for (auto v : list)
{
lambda8(v);
}
}
당연히 람다 표현식은 모든 종류의 함수를 실행할 수 있다.#!syntax cpp
int main()
{
// (10) 실수를 반환하는 람다 표현식
auto lambda0 = [] { return 1.0; };
// 상태를 가지고 있지 않은 람다 표현식은 함수 포인터에 저장이 가능하다.
// 후술하겠지만 decltype(lambda0)은 클로저의 자료형을 반환하므로 주의.
using lambda0_t = double(*)();
lambda0_t lambda0_v = lambda0;
}
상기한 예제에서 소개한 람다 표현식은 모두 일반적인 함수와 동일한 형태를 가지고 있다. 이 정도로는 람다 표현식의 유용함을 알기 어렵다. 다음에 소개할 람다 표현식은 상태를 가지고 있다. 상태가 무엇인지 알아보자.3. 람다 캡처
람다 포섭 (Lambda Capture)람다 표현식을 선언할 때에는, 맨 처음 포섭란(
[]
대괄호) 안에 어떤 필드의 이름, &
, =
, 혹은 this
를 넣어서 람다 표현식 내부에서 사용할 필드를 선정할 수 있다. 이때 선정된 필드는 람다 표현식으로 들어가면서 복사, 이동, 참조될 수 있다. 포섭된 필드는 기본적으로 상수(const
)이므로 수정할 수 없다. 매개변수와는 다른 점이다.이 기능이 존재하는 이유는 람다 표현식은 어떤 문맥[1]에 종속된 존재가 아니라, 하나의 객체로서 모든 곳에서 사용될 수 있어야 하기 때문이다. 그래서 람다 표현식 스스로가 상태를 가질 수 있는데, C++에서는 필드를 가져옴으로써 외부의 상태를 저장한다.
#!syntax cpp
short GetPort() noexcept
{
return 10008;
}
int main()
{
int i{ 5 };
const int j{ 10 };
// (1) 외부의 변수 `i`를 포섭. 이제 람다 표현식 안에서 `i`의 이름을 사용할 수 있다.
[i] { std::println("`i` is {}", i); };
// (2) 외부의 변수 `j`를 포섭. 포섭한 객체와 포섭 필드의 이름은 같아도 된다.
[j = j] {};
// (3) 외부의 변수 `i`를 포섭, `j`를 참조 포섭
[i = std::move(i), &j] {};
// (4) 변수 `port`를 포섭란에서 즉시 정의
[port = GetPort()](std::string_view ip_address) {};
}
상기 예제에서는 포섭란에 필드를 가져오는 방법을 소개하고 있다. 포섭란에 필드의 이름을 기입하면 람다 표현식 내부로 그 필드를 가져온다. 가져온 필드는 상수인 것만 빼면 람다 표현식에서 자유롭게 이용할 수 있다. 참조 포섭에 대해서는 다음 예제에서 설명한다.#!syntax cpp
int main()
{
int i{ 1 };
// (1) `i`를 포섭
auto lambda0 = [i] -> void {
std::println("{}",i);
// 포섭된 `i`는 상수이므로 수정할 수 없음.
};
lambda0();
// (2) `i`를 이름 `integer_inside`로 포섭
// `i = i` 처럼 람다 외부와 내부의 이름은 중복되어도 문제없음.
auto lambda1 = [integer_inside = i, i = i] -> void {
std::println("{}",i);
// 포섭된 `integer_inside`와 `i`는 상수이므로 수정할 수 없음.
};
// (3) `i`를 참조 포섭
auto lambda2 = [&i] {
std::println(i);
// 람다 내부에서 `i`의 값은 4가 됨.
i = 4;
};
lambda2();
// 마찬가지로 람다 외부의 `i`의 값도 4가 됨.
// (4) `i`를 이름 `integer_inside`로 참조 포섭
auto lambda3 = [&integer_inside = i] {
std::println("{}", integer_inside);
// 람다 내부에서 `integer_inside`의 값은 5가 됨.
integer_inside = 5;
};
lambda3();
// 람다에서 외부의 `i`를 `integer_inside`로 참조했으므로 람다 외부의 `i`의 값도 5가 됨.
}
상기한 코드는 참조 포섭의 기작을 보여준다. 참조자로 포섭한 변수는 람다 표현식 내부에서 원본을 그대로 이용할 수 있다. 이때 참조자가 아닌 포섭된 필드는 새로운 상수로 정의되므로 이전의 필드와는 단절된 상태가 된다. 너무 어려운 말 같지만 함수에 &
참조자를 쓰지 않으면 복사나 이동되는 것과 같은 경우다. 람다 표현식이라고 복잡한 무언가가 있는게 아니라 함수와 똑같으며, 다만 편의성을 위한 추가 문법을 지원하는 것이다.#!syntax cpp
// 전역 변수
int globalInteger;
int main()
{
// 정적 변수
static staticInteger = 0;
// (1) 전역 변수는 그대로 사용할 수 있다.
auto lambda0 = [] { return globalInteger * 5 };
// (2) 정적 변수는 그대로 사용할 수 있다.
[staticInteger] {};
// (3) 컴파일 오류! 정적인 지속 시간을 가진 객체는 캡처할 수 없습니다.
[globalInteger] {};
}
주의할 점은 정적인 지속요건을 가진(Static Duration) 필드는 포섭할 수 없다. 정적인 지속요건을 가진 필드에는 전역 변수, static
변수 등이 있다. 이런 필드들은 그냥 람다 표현식안에서 별다른 조치없이 그대로 사용할 수 있다.#!syntax cpp
struct Squirrel
{
int myAge;
constexpr Squirrel(int age = 0) noexcept
: myAge(age)
{}
};
// `Squirrel` 벡터를 받아서 시작 나이부터 `Squirrel`들의 나이를 증분시키는 함수
void enumerate_squirrels(std::vector<Squirrel>& list, int age_begin) noexcept
{
for (auto& squirrel : list)
{
squirrel.myAge = age_begin++;
}
}
int main()
{
// 10개의 `Squirrel` 인스턴스가 담긴 std::vector 지역 변수
std::vector<Squirrel> everySquirrels { 10 };
// (1) `everySquirrels`를 포섭
auto lambda0 = [squirrels = everySquirrels](int age_begin) {
// 컴파일 오류! `enumerate_squirrels`의 인자 `list`에서 형식 한정자가 삭제되었습니다.
// 즉 `std::vector<Squirrel>&`를 받는 함수에 `const std::vector<Squirrel>&`가 전달되어서 실행할 수 없음.
enumerate_squirrels(squirrels, age_begin);
// 포섭된 `squirrels`는 상수이므로 수정할 수 없음.
};
lambda0(3);
// 람다 외부에 정의된 `everySquirrels`는 변화 없음.
// (2) `everySquirrels`를 참조 포섭
// `everySquirrels`를 그대로 기입해도 문제없음.
auto lambda1 = [&squirrels = everySquirrels, &everySquirrels](int age_begin) {
enumerate_squirrels(squirrels, age_begin);
// 람다 내부에서 `squirrels`에 들어있는 `Squirrel` 인스턴스들의 `myAge`가 수정됨.
};
lambda1(3);
// 마찬가지로 람다 외부에 정의된 `everySquirrels`도 수정됨.
}
원시 자료형 말고도 모든 종류의 자료형을 포섭할 수 있다. 상기한 예제에서는 복잡한 구조체에 대하여 포섭을 쓰는 방법을 보여주고 있다. 복사 포섭된 필드는 람다 표현식 외부로 영향을 끼치지 못한다. 참조 포섭은 람다 표현식 내부에서 외부에 영향을 끼칠 수 있게 만든다. 이런 식으로 람다 표현식은 함수와 구별되는 특징으로 자신만의 데이터 멤버를 가지고 있다. 그러나 아직 매개변수를 쓰는 것과 차이가 없어보인다. 사실 당연한 것이, 람다 표현식은 함수와 호환성을 유지하면서 활용 범위를 늘리는 데에 목적이 있기 때문이다. 활용성 면에서 어떤 차이가 있는지 알아보자.4. 전방위 환경 포섭
기본사항 포섭 (Capture Default)포섭란에
&
나 =
중 하나를 기입하면 람다 표현식이 선언된 문맥의 필드를 전부 포섭할 수 있다 [2]. 곧 람다 표현식이 만들어진 당시의 환경을 그대로 가져간다(Capture)는 뜻이다. &
포섭은 현재 문맥을 참조형으로 포섭한다. =
포섭은 현재 문맥을 불변한 상태[const]로 포섭한다. 이때 앞서 소개 했던대로 필드 이름을 직접 기입하면 필드마다 별도의 포섭 방식을 적용할 수 있다.#!syntax cpp
int main()
{
// 지역 변수
int i{ 10 };
std::vector list{ 0, 1, 2, 3, 4 };
// (1) 벡터 `list`를 참조 포섭, 변수 `i`를 참조 포섭
// 벡터 `list`의 세번째 원소의 참조형을 반환하는 람다 표현식
auto lambda0 = [&] -> decltype(auto) {
return list.at(2);
};
// int& lambda0_result == 2
auto&& lambda0_result = lambda0();
// (2) 원래 '2'가 들어있었던 벡터 `list`의 두번째 원소에 '10'이 대입된다.
// 벡터 `list`가 수정된다.
lambda0_result = 10;
// (3) 벡터 `list`를 포섭, 변수 `i`를 포섭
// 벡터 `list`의 네번째 원소를 반환하는 람다 표현식
auto lambda1 = [=] -> decltype(auto) {
return list.at(3);
};
// int lambda1_result == 3
auto&& lambda1_result = lambda1();
// (4) 벡터 `list`를 포섭, 변수 `i`를 참조 포섭
auto lambda2 = [&, list] {
// 포섭된 `list`는 상수이므로 수정할 수 없음.
//list[1] = 60;
// 람다 표현식 외부의 `i`에 '20'이 대입된다.
i = 20;
};
lambda2();
// 람다 표현식 외부의 벡터 `list`는 변화없음.
// 람다 표현식 외부의 `i`가 수정됨.
// (5) 벡터 `list`를 참조 포섭, 변수 `i`를 포섭
auto lambda3 = [=, &list] {
// 람다 표현식 외부의 `list`의 두번째 원소에 '100'이 대입된다.
list[1] = 60;
// 포섭된 `i`는 상수이므로 수정할 수 없음.
//i = 10;
};
lambda3();
// 람다 표현식 외부의 벡터 `list`가 수정됨.
// 람다 표현식 외부의 `i`는 변화없음.
}
상기 예제는 기본사항 포섭이 작동되는 방식을 보여주고 있다. 일반 포섭과 참조 포섭이 혼용될 때 사례도 소개한다.5. 클래스 인스턴스 포섭
처음에 람다 표현식은 어떤 문맥에 종속되지 않고 어디에서나 자유롭게 사용할 수 있는 표현식이라고 얘기했었다. 이는 클래스의 인스턴스에도 종속되지 않는다는 뜻이다. 정확하게는 클래스가 정의된 위치, 실행 시점에 구애받지 않는다는 뜻이다. 람다 표현식에서 참조되는 인스턴스는 람다 표현식과 함께 이리저리 참조될 수 있다. 이 기능의 유용함은 다수의 인스턴스를 중앙에서 관리하는 시스템을 구축할 때 드러난다. 가령 다중 스레드 환경, 하나의 파일을 클래스를 통해 관리하면서, 파일 인스턴스를 여러 곳에서 참조하고 싶지만, 동적 할당이나 포인터를 쓰고 싶지 않을 때, 혹은 다수의 파일을 한번에 종합해서 관리하는 IO 작업, 게임에서 인스턴스를 관리하는 게임 관리자 클래스를 만들 때 등이 있다. 그외의 장점은 인스턴스를 포섭한 람다 표현식은 일반 전역 함수처럼람다-표현식(...);
의 형태로 사용할 수 있기 때문에 코드 호환성이나 심미적으로도 좋은 결과를 볼 수 있다.#!syntax cpp
class Captured
{
public:
float myRatio;
float myCross;
void Method1()
{
// (1) 데이터 멤버 `myRatio`를 참조형으로 포섭
auto setter = [&myRatio = myRatio](float value) { myRatio = value; };
setter(200.0f);
}
void Method2()
{
// (2) 현재 문맥은 클래스의 인스턴스이므로 현재 인스턴스와 모든 멤버를 참조 포섭
auto setter0 = [this](float value) { myRatio = value; i = 0; };
// (3) 인스턴스와 모든 멤버를 참조 포섭
auto setter1 = [&](float value) { myRatio = value; };
// (4) 현재 문맥은 클래스의 인스턴스이므로 인스턴스와 모든 멤버를 참조 포섭
auto setter3 = [=](float value) { myRatio = value; };
// (5) 현재 문맥은 클래스의 인스턴스이므로 인스턴스와 모든 멤버를 참조 포섭
// `myCross`는 `&`에 의해 포섭되므로 오류는 없지만 의미없는 구문임.
auto setter2 = [&, &myCross = myCross](float value) { myCross = value; };
// (6) 현재 인스턴스를 불변 참조 포섭
// setter4는 외부의 인스턴스를 수정할 수 없음.
auto setter4 = [*this](float value) { /* myRatio = value; */ };
// (7) 참조자를 써서 현재 인스턴스를 참조 포섭
auto setter5 = [&self = *this](float value) { self.myRatio = value; };
// (8) 현재 인스턴스를 불변 참조 포섭
// `=`에 의해 참조 포섭되었다가, 나중에 쓰인 `*this`에 의하여 불변 참조 포섭됨.
// 결과적으로 setter6는 외부의 인스턴스에 영향을 끼치지 못함.
auto setter6 = [=, *this](float value) { /* myRatio = value; */ };
setter1(200.0f);
}
void ConstMethod() const
{}
[[nodiscard]]
constexpr auto GetCrossAdder() noexcept
{
// (9) 현재 인스턴스를 참조형으로 포섭하고, 람다 표현식을 바로 반환함.
// 이 람다 표현식은 클래스 `Captured`의 인스턴스를 참조함.
return ([this](float rhs) { myCross += rhs; });
}
[[nodiscard]]
constexpr auto GetMutableRatioAdder() noexcept
{
// (10) 현재 인스턴스를 불변 참조형으로 포섭하고, 람다 표현식을 바로 반환함.
// 이 람다 표현식은 클래스 `Captured`의 인스턴스를 참조함.
// 그런데 'mutable'로 명시되어서 불변 인스턴스를 수정할 수 있음.
return ([*this](float rhs) mutable { myRatio += rhs; });
}
[[nodiscard]]
constexpr auto GetRatioAdder() noexcept
{
// (11) 데이터 멤버 `myRatio`를 참조형으로 포섭하고, 람다 표현식을 바로 반환함.
// 이 람다 표현식은 클래스 `Captured`의 인스턴스의 `myRatio`를 참조함.
return ([&myRatio = myRatio](float rhs) { myRatio += rhs; });
}
// (12)
// C++23 부터는 static 람다 표현식을 사용할 수 있음.
static inline constexpr auto static_lambda = [=]() { std::println("Hello, world!"); };
};
constexpr Captured globalInstance{};
Captured globalMutableInstance{};
int main()
{
// (13) 클래스 `Captured`의 상수 인스턴스를 포섭
[inst = globalInstance] { inst.ConstMethod(); };
// (14) 클래스 `Captured`의 상수 인스턴스를 참조 포섭
[&inst = globalInstance] { inst.ConstMethod(); };
// (15) 클래스 `Captured`의 인스턴스를 참조 포섭
[&globalMutableInstance] { globalMutableInstance.Method1(); };
// (16) 클래스 `Captured`의 인스턴스를 참조 포섭
[&inst = globalMutableInstance] { inst.Method2(); };
// (17) 클래스 `Captured`의 인스턴스를 포섭
// `inst`는 상수 인스턴스임.
[inst = std::move(globalMutableInstance)] { inst.ConstMethod(); };
// (18) `globalInstance`를 포섭한 람다 표현식
// 하지만 `globalInstance`는 상수이므로 전부 실행하지 못함.
/* auto globalInstAdder = globalInstance.GetRatioAdder(); */
/* auto globalInstMutableAdder = globalInstance.GetMutableRatioAdder(); */
// (19) `globalMutableInstAdder`는 `globalMutableInstance`를 포섭한 비상수 람다 표현식
// `globalMutableInstance`는 상수가 아니므로 `globalMutableInstAdder`를 실행할 수 있음.
// 람다 표현식 `globalMutableInstAdder`가 상수인지 아닌지는 상관없음.
const auto globalMutableInstAdder = globalMutableInstance.GetCrossAdder();
// (20) `globalMutableInstance`의 `myCross`는 `0.5f`가 됨.
globalMutableInstAdder(0.5f);
}
상기 예제는 클래스 인스턴스를 포섭하는 방식을 소개하고 있다. 람다 표현식은 현재 문맥이 클래스 인스턴스일 때 this
또는 &
포섭을 사용하면 해당 인스턴스를 그대로 포섭할 수 있다. 데이터 멤버에 대하여 참조 포섭을 사용할 경우 인스턴스의 데이터 멤버들을 개별적으로 포섭할 수 있다.&
, =
둘 다와 this
는 현재 문맥이 클래스의 인스턴스일 경우, 인스턴스의 모든 멤버를 참조형으로 포섭한다. 곧 람다 표현식이 일종의 멤버 함수가 되는 꼴이다. 물론 인스턴스.람다 표현식
의 방식은 쓸 수 없지만 람다 표현식의 코드를 멤버 함수처럼 작성할 수 있다.*this
를 사용하면 현재 문맥의 인스턴스를 불변 참조 포섭한다. this
를 쓴 람다 표현식 안에 중복으로 사용할 수 있다.const
, mutable
등 멤버 함수의 자료형 한정자를 그대로 사용할 수 있다. *this
로 불변 참조 포섭을 하더라도 mutable
한정자를 가진 람다 표현식은 데이터 멤버들을 수정할 수 있다.5.1. 예제1: 스레드를 넘나드는 람다 표현식
- <C++ 예제 보기>
#!syntax cpp #include <print> #include <vector> #include <chrono> #include <array> #include <thread> #include <latch> constinit std::latch terminated_count{ 4 }; enum class [[nodiscard]] TaskCategory { None = 0, Idle = 0, PrintID, EnumerateNumber, Terminate }; void WorkerImplementation(class Worker* worker); class Worker { public: explicit Worker() = default; explicit Worker(std::uintptr_t uid) noexcept : uid(uid), myHandle(std::jthread(::WorkerImplementation, this)) , myTask(TaskCategory::None) {} // 클래스 `Worker`는 복사 생성 및 복사 대입을 할 수 없음. Worker(const Worker&) = delete; Worker& operator=(const Worker&) = delete; ~Worker() { myTask = TaskCategory::Terminate; } int Execute() { using namespace std::chrono_literals; switch (myTask) { case TaskCategory::Idle: { std::println("{} idles", uid); myTask = TaskCategory::None; return 1; } case TaskCategory::PrintID: { std::println("{}'s id is {}", uid, std::this_thread::get_id()); myTask = TaskCategory::None; return 2; } case TaskCategory::EnumerateNumber: { std::println("{} show number {}", uid, ::rand()); myTask = TaskCategory::None; return 3; } case TaskCategory::Terminate: { return 0; } break; default: { return 4; } break; } } auto AcquireTaskSetter() noexcept { return [this](TaskCategory task) noexcept { if (TaskCategory::None == myTask) { myTask = task; } }; } [[nodiscard]] constexpr auto GetID() const noexcept { return uid; } [[nodiscard]] constexpr const std::jthread& GetHandle() const& noexcept { return myHandle; } [[nodiscard]] constexpr std::jthread&& GetHandle() && noexcept { return std::move(myHandle); } private: std::uintptr_t uid; std::jthread myHandle; TaskCategory myTask; }; void WorkerImplementation(Worker* worker) { using namespace std::chrono_literals; int result = 1; while (0 != result) { result = worker->Execute(); std::this_thread::sleep_for(1s); } std::println("{} terminated", std::this_thread::get_id()); terminated_count.arrive_and_wait(); } int main() { std::uintptr_t last_uid = 10000; // `Worker` 인스턴스들을 동적할당 하지 않는다. std::array<Worker, 4> workers { Worker{ last_uid++ }, Worker{ last_uid++ }, Worker{ last_uid++ }, Worker{ last_uid++ } }; std::array task_setters = { workers[0].AcquireTaskSetter(), workers[1].AcquireTaskSetter(), workers[2].AcquireTaskSetter(), workers[3].AcquireTaskSetter() }; std::srand((unsigned) std::time(nullptr)); using namespace std::chrono_literals; std::jthread worker_prompt { [task_setters] { while (true) { if (terminated_count.try_wait()) break; // 복사해서 써도 문제 없음. auto task_setter = task_setters[::rand() % 4]; task_setter(static_cast<TaskCategory>(1 + ::rand() % 3)); std::this_thread::sleep_for(1s); } } }; terminated_count.wait(); }
`Worker`
클래스는 스레드를 만들고 스레드에서 함수를 지속적으로 실행하는 작업자 클래스다. 여기서 `AcquireTaskSetter`
멤버 함수가 작업자 인스턴스를 관리하는 역할을 하는 람다 표현식을 반환한다. `Worker`
클래스는 복사가 불가능하지만 `AcquireTaskSetter`
로 만든 람다식은 어디에서나 사용할 수 있으며 심지어 다른 스레드에서도 마찬가지다. 람다 표현식 객체는 다른 함수에서, 다른 스레드에서 매개변수로 복사해서 써도 문제가 없다.6. 포섭의 제한 사항
#!syntax cpp
int main()
{
int i;
// (1) 컴파일 오류! 캡처 목록에서 하나의 이름이 여러번 표시될 수 없습니다.
[i, i] {};
// (2) 컴파일 오류! 캡처 목록에서 하나의 이름이 여러번 표시될 수 없습니다.
[i, &i] {};
// (3) 컴파일 오류! 캡처 목록에서 하나의 이름이 여러번 표시될 수 없습니다.
[&i, &i] {};
class Captured
{
public:
void Method()
{
// (4) 컴파일 오류! 하나의 이름이 여러번 표시될 수 없습니다.
[this, this] {};
// (5) 컴파일 오류! 하나의 이름이 여러번 표시될 수 없습니다.
[this, *this] {};
}
};
}
유의할 점은 똑같은 객체[4]를 중복해서 캡처할 수 없다. 설령 아무 참조자도 넣지 않고 필드 이름을 넣어도 마찬가지다. 이 부분은 IDE 혹은 컴파일 시에 문제를 바로 확인할 수 있으므로 참고.#!syntax cpp
int* value = new int{0};
// 참조 포섭
auto lambda = [&ref = *value] { ref = 30; };
// 참조 대상 소실
delete value;
// 런타임 오류! 메모리 참조 위반!
lambda();
또 하나 조심해야할 점은 참조 포섭의 경우 좌측값 참조자와 스마트 포인터와 마찬가지로 하드 링크가 아니라는 것이다. 또한 this
, &
로 외부 환경을 통째로 참조 포섭한 경우도 마찬가지다. 언제나 참조 대상 소실 (Dangling Reference)이 발생할 수 있다는 점을 명심하고, 참조할 대상이 소멸하지 않을 게 확실할 때만 참조 포섭을 사용해야 한다. 혹은 std::weak_ptr
등의 도움이 있다면 소멸할 지점을 미리 알고 예방할 수 있다.7. 특성 적용
람다 표현식도 일반 함수와 마찬가지로 특성을 사용할 수 있다. 특성은 함수에 적용할 수 있는 거라면 모두 사용할 수 있다.포섭 대괄호
[]
와 매개변수 소괄호 ()
사이에 기입하면 된다. 그런데 실제 활용할 때 함수에 한번 전달하고 마는 람다 표현식에 쓰는 게 쓸만한지가 의문일 수 있다. 하지만 메모리를 할당하는 람다 표현식이라면 [[nodiscard]]
를 적용하는 건 아주 좋다.- <C++ 예제 보기>
#!syntax cpp int main() { auto allocator = [][[nodiscard]] (std::size_t length) { return new int[length]; }; // (1) int[10] auto array0 = allocator(10); // (2) 경고! 함수의 반환 값이 사용되지 않았습니다. allocator(100); }
[[nodiscard]]
를 적용해서 동적 할당한 메모리를 실수로 놓치지 않도록 작성했다.8. 상수 람다 표현식
람다 표현식의 선언 시에constexpr
C++17 , consteval
C++20 을 사용할 수 있다. 그런데 만약 람다 표현식이 상수 표현식의 조건을 만족하면 constexpr
는 자동으로 붙는다. 일종의 문법적 설탕인 셈인데, 상수 표현식 함수에서 람다 표현식을 쉽게 쓸 수 있도록 한 안배로 보인다.- <C++ 예제 보기>
#!syntax cpp constexpr auto Absolute = [] (auto value) { return 0 <= value ? value : -value; };
9. 템플릿 람다 표현식
람다 표현식에도 템플릿을 사용할 수 있다. 그러나 읽기도 힘들고auto
가 편리함에 힘입어 실용례를 전부 커버하므로 의의는 자료형 매개변수를 직접 이용하는 데에 있다. 자료형을 가지고 메타 프로그래밍을 하려면 using
과 decltype
을 써서 auto
매개변수의 자료형을 가져와야 하는데, 이게 귀찮으면 고려할 수 있다. 그러나 람다 표현식은 함수와는 달리 템플릿 매개변수를 명시할 수가 없다. 무조건 함수 매개변수가 필요하므로 매개변수를 위한 메모리 공간이 필요한 단점이 있다.- <C++ 예제 보기>
#!syntax cpp // (1) 자료형의 크기에서 가까운 2의 배수를 구하는 람다 표현식 // 자료형만 쓰므로 consteval을 쓸 수 있다. consteval auto lambda0 = []<typename T> [[nodiscard]](const T& v) noexcept -> std::size_t { constexpr std::size_t size = sizeof(T); constexpr std::size_t lower = size - (size % 2); constexpr std::size_t upper = lower + 2; return (size - lower < upper - size) ? lower : upper; }; constexpr auto size_integer = lambda0(0); constexpr auto size_squirrel = lambda0(Squirrel{}); // (2) 앞서 소개한 절대값을 구하는 람다 표현식을 엄밀하게 정의한 람다 표현식 constexpr auto Absolute = []<typename T> [[nodiscard]] (const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) - >T { return 0 <= value ? value : -value; };
consteval
람다 표현식을 보여주고 있다. 두번째 사례는 조건적 noexcept
를 명시하는 법을 보여주고 있다. 자료형 T
가 예외없이 복사 생성이 가능하다면 반환할 때(매개변수 value
가 복사되므로) 예외를 일으키지 않는다.10. 함수의 매개변수로 전달
람다 표현식은 상태를 가지면서 복사 가능하고, 이동 가능하고, 참조될 수 있다. 당연히 함수를 통해서도 가능하다. 람다 표현식을 본격적으로 활용하려면 함수로 람다 표현식을 주고받는 방법을 알아야 한다. 실제 용법에서 람다 표현식을 만든 즉시 실행하는 경우는 템플릿과 활용하는 경우와 몇몇을 빼면 거의 없으므로[5] 이쪽이 람다 표현식의 주 용법이라고 할 수 있다.10.1. auto 매개변수
지금까지 살펴본 예제에서는 람다 표현식을 모두auto
에 담아서 사용했다. 함수의 인자로 넣을 때도 마찬가지로 auto
를 사용하면 된다. 별다른 참조자를 넣지 않으면 람다 표현식은 매개변수로 복사 또는 이동되어 전달된다.여기서
auto
의 특징에 대해 다시 말하자면, auto
는 사실 템플릿이며 스스로 존재할 수 없는 자료형의 별칭이며, 변수와 함수에 사용할 때는 정의가 존재해야 한다는 것이다. 그래서 함수에서 람다 표현식을 auto
매개변수로 받으려면 함수의 구현을 반드시 같이 해줘야 한다.- <C++ 예제 보기>
#!syntax cpp // 참고사항: auto에는 또다른 auto 함수 또는 템플릿 함수를 전달할 수 없다. auto GetMapper(auto& container, auto predicate) { return [&] { for (auto& value : container) { value = predicate(value); } }; } auto GetMapper(auto& container) { return [&](auto predicate) { for (auto& value : container) { value = predicate(value); } }; } [[nodiscard]] constexpr int MultiplyFunction(int v) noexcept { return v * 10; } int main() { std::vector list{ 1, 2, 3, 4, 5 }; // (1) 벡터 `list`를 참조형으로 받아서 수정하는 람다 표현식 auto mapper0 = GetMapper(list, [](auto v) noexcept { return v * 10 }); // `list`가 수정된다. // `list` == { 10, 20, 30, 40, 50 }; mapper0(); // (2) 상태가 없는 람다 표현식이므로 함수를 사용해도 같은 결과를 볼 수 있다. // 컴파일러가 릴리스 모드면 성능 상에 차이는 없다. auto mapper1 = GetMapper(list, MultiplyFunction); // (3) 수식 `predicate`를 복사로, 벡터 `list`를 참조형으로 받아서 수정하는 람다 표현식 auto mapper2 = GetMapper(list); // `list`가 수정된다. // `list` == { 1, 2, 3, 4, 5 }; mapper2([](auto v) noexcept { return v / 10; }); }
auto&&
를 쓰지 않고 그냥 auto
로 복사형으로 준다는 점이다. 람다 표현식을 함수에 전달할 때는 완벽한 매개변수 전달을 수행할 필요가 없다.10.2. 템플릿 매개변수
람다 표현식은 당연히 템플릿으로 받을 수 있다. 앞서auto
를 쓰는 방식과 똑같이 사용하면 된다.- <C++ 예제 보기>
#!syntax cpp template<typename It, typename Predicate> constexpr auto find_if(It first, It last, Predicate predicate) { for(; first != last; ++first) { if (predicate(*first)) { return first; } } return first; }
find_if
를 구현한 예제다. 이 함수는 템플릿으로 실행 가능한(invocable) 함수 객체(Function Object)를 받아서 순회자의 값을 인자로 전달하고, 순회자의 값을 통해 실행시켜서 조건을 만족하는 순회자를 찾는 함수다. 만약 써본 경험이 있다면 알겠지만 이 함수는 일반적인 함수, ()
연산자를 오버로딩한 클래스, 람다 표현식 모두를 전달할 수 있다.여기까지가 C++을 어플리케이션에서 활용하고자 하는 사람의 최종 단계다. 이제 심화 내용으로 넘어가며 필요에 의해 배우면 된다. 여기까지 이해했다면 C++에서 문법적 기능은 거의 다 숙지했다고 볼 수 있으며 어플리케이션을 제작하는 단계에서는 굳이 필요한 내용은 아니다. 하지만 라이브러리 혹은 프로젝트 내부에서 유틸리티를 제작할 때 유용한 내용이다.
11. 개념 (Concept) 매개변수
- <C++ 예제 보기>
#!syntax cpp import <type_traits>; import <concepts>; // std::sentinel_for, std::invocable import <iterator>; // std::forward_iterator, std::iter_value_t import <functional>; // std::invoke import <vector>; // std::vector import <ranges>; // std::from_range_t, std::from_range, std::views::iota template<std::forward_iterator It, std::sentinel_for<It> Sentinel> constexpr It find_if(It first, const Sentinel last, std::invocable<std::iter_value_t<It>> auto predicate) { for (; first != last; ++first) { if (std::invoke(predicate, *first)) { return first; } } return first; } int main() { const std::vector list{ std::from_range, std::ranges::views::iota(1, 10) }; // 3으로 나누어 나머지가 0인 값이면 참을 반환 const auto it = ::find_if(list.begin(), list.end(), [](const auto& value) noexcept -> bool { return value % 3 == 0; }); // `what`의 값은 3 auto what = *it; // `write_bytes`는 int, int, double& 매개변수를 받아 실행할 수 있는 실행가능한 객체 (invocable object) std::invocable<int, int, double&> auto write_bytes = [](int left, int right, double& dest) noexcept { *(reinterpret_cast<int*>(&dest) + 1) = left; *(reinterpret_cast<int*>(&dest)) = right; }; double storage{}; write_bytes(4000, 3000, storage); }
find_if
를 한번 더 엄밀하게 정의한 버전을 보여주고 있다. 그리고 개념과 auto
를 조합하여 람다 표현식을 선언한 예시도 보여주고 있다.12. 함수 포인터
맨 처음에 소개했던 예제에서 람다 표현식을 함수 포인터에 담는 구문을 소개했었다. 이걸 쓰면auto
때문에 무조건 함수를 구현해야 할 필요도 없고, 외부 라이브러리로 배포할 때 함수 구현을 숨길 수 있지 않을까? 안타깝게도 항상 가능한 게 아니다. 상태를 가지고 있지 않은 람다 표현식만 함수 포인터에 담을 수 있다. 참고로 템플릿이나 auto
를 쓴 일반화된 람다 표현식(Generic Lambda Expression)인지 아닌지는 상관없다. 포섭한 필드가 없어야만 가능하다.- <C++ 예제 보기>
#!syntax cpp import <type_traits>; // 함수 int Function0() { return 0; } int Function1() noexcept { return 0; } template<typename T> T Function2(T value) { return T * T; } // 함수 포인터 자료형 선언 using Function0_t = int(*)(); using Function1_t = int(*)(); using Function1_nothrow_t = int(*)() noexcept; template<typename T> using Function2_t = T(*)(T value); // 람다 표현식 auto lambda0 = [] { return 0; }; auto lambda1 = [] { return 0; }; auto lambda1_nothrow = [] noexcept { return 0; }; auto lambda2 = []<typename T>(T value) -> T { return value * value; }; using lambda0_t = decltype(lambda0); using lambda1_t = decltype(lambda1); using lambda1_nothrow_t = decltype(lambda1_nothrow); using lambda2_t = decltype(lambda2); // (1) 컴파일 오류! 정적 어설션이 실패했습니다. [3] static_assert(std::is_same_v<Function0_t, lambda0_t>); static_assert(std::is_same_v<Function1_t, lambda1_t>); static_assert(std::is_same_v<Function1_t, lambda1_nothrow_t>); static_assert(std::is_same_v<Function2_t<int>, lambda2_t>); // (2) 컴파일 오류 없음. // 상태가 없는 람다 표현식은 동일 매개변수, 동일한 반환 자료형, 동일한 예외 상세(C++17 부터)를 갖고 있으면 함수 포인터로 변환이 가능함. static_assert(std::is_convertible_v<lambda0_t, Function0_t>); static_assert(std::is_convertible_v<lambda1_t, Function1_t>); static_assert(std::is_convertible_v<lambda1_nothrow_t, Function1_nothrow_t>); // `lambda2_t`는 템플릿 람다 표현식인데, 템플릿 매개변수를 명시할 수가 없어서 우회해서 원래의 멤버 함수를 가져와야 함. // (3) C++17 부터는 동일한 예외 상세를 가져야 함수 포인터로 변환이 가능함. // C++14 까지는 어설션이 성공함. // C++17 부터는 컴파일 오류 발생함. static_assert(std::is_convertible_v<lambda1_t, Function1_nothrow_t>); // (4) 여담으로 예외가 없는(noexcept) 함수 포인터는 예외를 발생시킬 수 있는 함수 포인터로 변환할 수 있음. static_assert(std::is_convertible_v<Function1_nothrow_t, Function1_t>); int main() { // (5) 상태가 없는 람다 표현식은 상수 시간에 초기화될 수 있음. constexpr lambda2_t lambda2_inst{}; // (6) 람다 표현식의 멤버 연산자 함수 // int에 대하여 특수화함. const auto lambda2_op_ptr = &(lambda2_t::operator()<int>); // (7) `lambda2_result`의 값은 400 const auto lambda2_result = (lambda2_inst.operator())(20); }
마지막으로 람다 표현식의 동작이 어떻게 이루어지는지 살펴보자. 람다 표현식도 C++의 구성에서 완전히 동떨어진 존재가 아니다.
13. 정체
클로저 (Closure)람다 표현식은 템플릿과
__func__
매크로도 쓸 수 있는 등 겉보기에는 함수와 다르지 않다. 하지만 람다 표현식은 함수 포인터가 아니며 상태를 가지고 있으면 함수 포인터 변수에 담을 수 없다. 람다 표현식의 정체는 바로 () 연산자를 오버로딩한 클래스 인스턴스다. 즉 람다 표현식도 함자(Functor) 또는 함수 객체(Function Object)이며, 포섭한 필드는 이 인스턴스의 데이터 멤버로 정의되는 것이다. 이에 파생되는 특징은 인자 의존성 탐색이 일어나지 않는다는 점인데 이에 대해서는 여기선 다루지 않겠다.13.1. 상태가 없는 클로저
#!syntax cpp
class StatelessClosure
{
using Fn = ReturnType(*)(parameters...);
public:
constexpr StatelessClosure() = default;
constexpr ~StatelessClosure() = default;
/* 기본 복사/이동 생성자를 가짐 */
constexpr StatelessClosure(const StatelessClosure&) = default;
constexpr StatelessClosure(StatelessClosure&&) = default;
/* 기본 복사/이동 대입 연산자를 가짐 */
constexpr StatelessClosure& operator=(const StatelessClosure&) = default;
constexpr StatelessClosure& operator=(StatelessClosure&&) = default;
/* () 연산자 오버로딩 */
constexpr ReturnType operator()(parameters...);
/* 함수 포인터로 변환할 수 있음 */
constexpr operator Fn() const noexcept;
private:
// 포섭된 필드가 없음.
};
상기 예제는 포섭된 필드가 없는 가장 단순한 람다 표현식 클래스를 보여주고 있다. 함수 포인터로 변환할 수 있으며 복사/이동 생성/대입이 자유롭다.13.2. 상태가 없는 일반화 클로저
#!syntax cpp
class StatelessGenericClosure
{
template<typename... Ts>
using Fn = ReturnType(*)(Ts... parameters);
public:
constexpr StatelessGenericClosure() = default;
constexpr ~StatelessGenericClosure() = default;
/* 기본 복사/이동 생성자를 가짐 */
constexpr StatelessGenericClosure(const StatelessGenericClosure&) = default;
constexpr StatelessGenericClosure(StatelessGenericClosure&&) = default;
/* 기본 복사/이동 대입 연산자를 가짐 */
constexpr StatelessGenericClosure& operator=(const StatelessGenericClosure&) = default;
constexpr StatelessGenericClosure& operator=(StatelessGenericClosure&&) = default;
/* () 연산자 오버로딩 */
template<typename... Ts>
constexpr ReturnType operator()(Ts... parameters);
/* 함수 포인터로 변환할 수 있음 */
template<typename... Ts>
constexpr operator Fn<Ts...>() const noexcept;
private:
// 포섭된 필드가 없음.
};
상기 예제는 템플릿 혹은 auto
를 사용한 일반화된 람다 표현식(Generic Lambda Expression)의 클래스를 보여주고 있다. 함수 포인터로 변환할 수 있으며 마찬가지로 복사/이동 생성/대입이 자유롭다.13.3. 상태를 가진 클로저
#!syntax cpp
class Closure
{
public:
/* 기본 생성자를 가지고 있지 않음. 람다 표현식 인스턴스가 생성될 때 결합(Aggregate) 생성 방식으로 전달됨. */
// constexpr Closure() = default;
constexpr ~Closure() = default;
/* 기본 복사/이동 생성자를 가짐 */
constexpr Closure(const Closure&) = default;
constexpr Closure(Closure&&) = default;
/* 복사 대입 연산자는 삭제됨 */
Closure& operator=(const Closure&) = delete;
/* 이동 대입 연산자는 컴파일러에 의해 알아서 정의됨 */
/* 참조 포섭을 했으면 좌측값 참조 데이터 멤버가 있으므로 이동 대입 연산자가 정의되지 않음 */
//Closure& operator=(Closure&&) = default;
/* () 연산자 오버로딩 */
constexpr ReturnType operator()(parameters...);
/* 함수 포인터로 변환은 불가능함 */
//using Fn = ReturnType(*)(parameters...);
//constexpr operator Fn() const noexcept;
private:
// 포섭된 필드는 데이터 멤버임.
T0 capture0;
T1 capture2;
// ...
};
상기 예제는 상태를 가진 람다 표현식의 클래스를 보여주고 있다. 포섭한 필드는 클래스의 데이터 멤버로 들어온다. 인스턴스의 생성은 집결 초기화(Aggregate Initialization)로 이루어진다. 복사 대입은 불가능하며 이동 대입의 경우 좌측값 참조자 데이터 멤버가 없으면 가능하다.13.4. 상태를 가진 일반화 클로저
#!syntax cpp
class GenericClosure
{
public:
/* 기본 생성자를 가지고 있지 않음. 람다 표현식 인스턴스가 생성될 때 집결 초기화(Aggregate Initialization) 방식으로 전달됨. */
// constexpr GenericClosure() = default;
constexpr ~GenericClosure() = default;
/* 기본 복사/이동 생성자를 가짐 */
constexpr GenericClosure(const GenericClosure&) = default;
constexpr GenericClosure(GenericClosure&&) = default;
/* 복사 대입 연산자는 삭제됨 */
GenericClosure& operator=(const GenericClosure&) = delete;
/* 이동 대입 연산자는 컴파일러에 의해 알아서 정의됨 */
/* 참조 포섭을 했으면 좌측값 참조 데이터 멤버가 있으므로 이동 대입 연산자가 정의되지 않음 */
//GenericClosure& operator=(GenericClosure&&) = default;
/* () 연산자 오버로딩 */
template<typename... Ts>
constexpr ReturnType operator()(Ts... parameters);
/* 함수 포인터로 변환은 불가능함 */
//template<typename... Ts> using Fn = ReturnType(*)(Ts... parameters);
//template<typename... Ts> constexpr operator Fn<Ts...>() const noexcept;
private:
// 포섭된 필드는 데이터 멤버임.
T0 capture0;
T1 capture2;
// ...
};
상기 예제는 상태를 가진 일반화된 람다 표현식의 클래스를 보여주고 있다. 포섭한 필드는 클래스의 데이터 멤버로 들어온다. 인스턴스의 생성은 집결 초기화(Aggregate Initialization)로 이루어진다. 역시 복사 대입은 불가능하며 이동 대입의 경우 좌측값 참조자 데이터 멤버가 없으면 가능하다.[1] 예를 들어서 단순한 전역 함수 또는 어떤 클래스[2] 둘 중 하나만 가능하다[const] [4] 필드, 필드의 참조형, this[5] 일반화 프로그래밍 분야에서 코드 재사용성을 극단으로 올리고자할 때 유용하다