[C++] 람다 표현식

dumdumer·2024년 9월 11일
0

C++

목록 보기
3/3

람다 표현식(Lambda expression)


C++의 기능 중에는 람다 표현식이라는 것이 있다.

C++ 레퍼런스의 설명에는 다음과 같이 적혀있다.

"클로져(closure)를 생성하는 범위 내의 변수를 캡쳐할 수 있는 이름 없는 함수객체"

클로져는 뭐고, 캡쳐한다는 것은 또 무엇일까?
이에 대해 공부해 보았다.


람다의 형태

람다는 다음과 같은 표현식 형태를 가진다.

1. auto x = [capture clause] (parameter) {body}
2. auto x = [capture clause] (parameter)->return type {body}
  • capture clause : 람다 표현식임을 정의하고, 람다가 속한 지역 범위 내의 변수를 어떻게 사용할 지 정의한다.
  • parameter : 함수의 parameter와 같은 기능이다.
  • return type : 함수의 반환 값과 같은 기능이다.
  • body : 함수의 구현부와 같은 기능이다.

아래와 같이 람다 표현식을 사용할 수 있다.

auto lambda = []() {std::cout << "hello" << "\n"; };
lambda();
// print "hello"

캡쳐(Capture)

람다 표현식에서 캡쳐란, 람다 표현식이 속한 지역 범위의 변수들을 값으로 가져오거나 참조하여 람다 표현식 내에서 접근 가능하게 하는 방법을 의미한다.

캡쳐하지 않은 외부 변수들은 일부를 제외하고는 람다 표현식에서 사용할 수 없다.

람다의 캡쳐에는 이러한 방법들이 있다.
[]안에 적은 캡쳐할 내용들을 캡쳐 리스트(Capture List)라고 한다.

[] : 캡쳐하지 않음. 즉, 외부 변수를 사용하지 않음.
[=] : 모든 변수들을 값으로 캡쳐함. (const로 가져오게 된다.)
[&] : 모든 변수들을 참조로 캡쳐함.
[val] : 범위 내의 변수 val을 값으로 캡쳐함.(val만 캡쳐)
[&val] : 범위 내의 변수 val을 참조로 캡쳐함.(val만 캡쳐)
[=, &val] : 모든 변수를 값으로 캡쳐함, val변수만 참조로 캡쳐
[val1, &val2] : 각 변수를 각 방식대로 캡쳐함.
클래스에서의 캡쳐 리스트는 아래에서 설명한다.

추가로, 캡쳐를 하지 않고도 람다 표현식에서 쓰거나 읽을 수 있는 변수들이 있다.

쓰기 가능

  • 전역 변수, static변수, thread local 저장기간인 변수
  • 상수 표현식(constant expression)으로 선언된 참조 변수

읽기 가능

  • const인 정수형 혹은 열거형 변수
  • 상수 표현식으로 선언된 변수
  • mutable 멤버가 없는 constexpr 변수

캡쳐한 예시를 살펴보자.

size_t st = 1;
std::vector<int> v{ 1,2,3 };

int main() {
	int val1 = 10;
	const int cVal = 2;
	constexpr char val2 = 'm';
	std::string s = "hello";

	// auto lambda1 = [=](int a) { val1 = a; }; // 컴파일 에러 : 값 수정 불가능
	auto lambda1 = [&s]() {s[0] = val2; std::cout << s << "\n"; };  // 캡쳐 없이 val2 사용
	auto lambda2 = []() {st = v.size(); std::cout << st << "\n"; }; // 캡쳐 없이 st, v 사용
	lambda1();	// print "mello"
	lambda2();	// print "3"
}

클로져(Closure)

클로져는 람다 표현식의 런타임 결과로써 나오는 임시객체이다.

우리가 작성한 람다 표현식은 표현식일 뿐, 런타임에는 존재하지 않는다.
대신 컴파일 시간에 람다 표현식은 고유한 클래스를 만들어내고,
런타임에 그 클래스 타입의 객체(클로져)가 생성되는 것이다.

auto f = [](int a, int b) { return a < b; };

위와 같은 람다 표현식이 있을 때, 컴파일 타임에 해당 표현식이 평가되어 클로져가 생성된다.
여기서 f는 클로져가 아닌 클로져의 복사된 값을 갖는 변수가 된다.


람다 표현식는 어디에 쓸까?

  • 람다 식은 함수에 인자로 전달될 때 쓰일 수 있다.
  • 특정 지역 범위에서만 쓰일 함수를 사용할 때 전통적인 함수 대신 사용하기 좋은 기능이다.

아래 코드는 분기에 따라 오름차순 혹은 내림차순으로 배열을 정렬하는 코드이다.

int main() 
{
	bool flag = false;
	int arr[5]{ 10, -2, 1, 7, -12 };

	if (flag)
		std::sort(arr, arr + 5, [](int a, int b) {return a < b; });
	else 
		std::sort(arr, arr + 5, [](int a, int b) {return a > b; });
	
	for (int i = 0; i < 5; ++i) {
		std::cout << arr[i] << ", ";
	}
}

람다 표현식과 클래스

클래스에서 람다를 어떻게 사용하는지 살펴보자.

먼저 클래스 내에서 람다를 선언해 멤버변수에 접근하려면 위에서 설명한 [멤버변수] 이런 식으로는 불가능하다.
왜냐하면 멤버변수는 객체의 this포인터를 통해 접근해야 하기 때문이다.

클래스에서 사용하는 캡쳐 리스트들은 이런 것들이 있다.

[this] : 현재 객체의 포인터(*this)를 참조로 가져와 멤버에 접근할 수 있게 해준다.
[x=x] : 현재 객체의 멤버변수 x를 값으로 가져온다.
[&x=x] : 현재 객체의 멤버변수 x를 참조로 가져온다.
[=]를 통해 this포인터에 접근하는 행위는 C++20부터 권장되지 않는다.(컴파일 경고 발생)

예시)

struct A {
	int x;

	void foo() {
		auto lambda = [=]() { this->x = 1; }; // 컴파일러 경고(C++ 20부터)
		auto lambda1 = [x = x]() { return x; };
		auto lambda2 = [&x = x]() {x = 2; };
		// auto lambda3 = [x]() {return x; }; // this가 캡쳐 목록에 없기에 x를 쓸 수 없음.
		auto lambda4 = [this]() {return x; }; 
		auto lambda5 = [*this]() {return x; };
	}
};

.
.
.
C++ 공부를 위해 작성된 글입니다. 오류가 있다면 지적해 주시면 감사하겠습니다.

참고자료
https://github.com/federico-busato/Modern-CPP-Programming (07.Basic_Concepts_V)
https://en.cppreference.com/w/cpp/language/lambda
https://lunchballer.com/archives/284

profile
tik tok

0개의 댓글