람다 표현식이란?
C++에서는 C++11에 도입된 람다 표현식(Lambda Expressions)은
코드를 더 간결하고 가독성 있게 작성할 수 있는 코딩 표현식중 하나이다.
특히 알고리즘, STL과 함께 사용 시 진가를 발휘 할 수 있는 기능.
간단히 말하자면 이름 없는 함수(익명함수)를 의미한다. 일반적으로 짧고 간단한 작업을
임시로 함수처럼 사용할 때 유용하다. 기존에 함수를 정의하고 이름을 지정하는 번거로움 없이
코드 내에서 직접 함수 로직을 작성할 수 있다.
주요 장점들은 아래와 같다.
1. 코드를 더 간결하게 작성 가능하다.
2. 함수를 사용하는 곳과 가까운 위치에 정의하여 가독성을 향상시킨다.
3. 지역 변수에 쉽게 접근 가능하다.(캡처)
4. 함수 객체를 즉석에서 생성 가능하다.
5. STL 알고리즘과 결합하여 기능 구현이 가능하다.
람다의 기본 문법
[capture-list](parameters) -> return-type {
// 함수 본문
}
- capture-list : 외부 변수를 람다 내부에서 사용하기 위한 캡처 리스트
- parameters : 함수의 매개변수 목록(생략 가능)
- return - type : 반환타입(생략 가능, 컴파일러가 추론함)
- 함수 본문 : 실행코드
가장 간단한 람다 예제
#include <iostream>
using namespace std;
int main() {
// 기본 람다 정의 및 호출
auto sayHello = []() {
cout << "Hello, Lambda!" << endl;
};
sayHello(); // 출력: Hello, Lambda!
return 0;
}
인자와 반환값이 있는 람다
#include <iostream>
using namespace std;
int main() {
// 인자를 받고 값을 반환하는 람다
auto add = [](int a, int b) -> int {
return a + b;
};
cout << "3 + 5 = " << add(3, 5) << endl; // 출력: 3 + 5 = 8
// 반환 타입 생략 가능 (자동 추론)
auto multiply = [](int a, int b) {
return a * b;
};
cout << "4 * 6 = " << multiply(4, 6) << endl; // 출력: 4 * 6 = 24
return 0;
}
변수 캡처(Capture)
람다의 중요한 기능 중 하나인 변수캡처는 하나의 외부변수를 캡처해서
람다 내부에서 사용할 수 있는 기능이 있다. 캡처에는 두가지 방식이 있다.
1. 값에 의한 캡처(by value) : 변수의 복사본을 사용한다.
2. 참조에 의한 캡처(by reference) : 변수의 참조를 사용한다.
람다 캡처 실행 과정 상세 분석
#include <iostream>
using namespace std;
int main() {
int x = 10;
// 값으로 캡처
auto print = [x]() {
cout << "x = " << x << endl;
};
// 참조로 캡처
auto change = [&x]() {
x += 5;
};
print(); // 출력: x = 10
change(); // x 값 변경
print(); // 출력: x = 10 (값으로 캡처했기 때문에 내부 값은 변경되지 않음)
cout << "x in main: " << x << endl; // 출력: x in main: 15 (참조로 변경된 값)
return 0;
}
1. 초기 상태
main()의 x = 10
2. 람다 생성 시점
auto print = [x]() { ... }; // print 람다 내부에 x=10 복사 저장
auto change = [&x]() { ... }; // change 람다는 원본 x의 참조 저장
메모리 상태
- main()의 x: 10
- print 람다 내부의 x 복사본: 10
- change 람다: 원본 x를 참조
3. 첫 번째 print() 호출
print(); // 출력: x = 10
- 람다 내부의 복사본 값(10) 출력
4. change() 호출
change(); // x += 5 실행
- 참조를 통해 원본 x를 15로 변경
메모리 상태:
- main()의 x: 15 ← 변경됨
- print 람다 내부의 x 복사본: 10 ← 여전히 10
- change 람다: 원본 x를 참조
5. 두 번째 print() 호출
print(); // 출력: x = 10
- 여전히 람다 내부의 복사본 값(10) 출력
- 원본이 15로 변경되었지만, 복사본은 영향받지 않음
6. main에서 x 출력
cout << "x in main: " << x << endl; // 출력: x in main: 15
- 원본 x의 변경된 값(15) 출력
핵심 포인트
- 값 캡처는 복사본을 생성: print 람다는 생성 시점의 x 값을 복사하여 독립적으로 보관
- 참조 캡처는 원본을 참조: change 람다는 원본 x를 직접 수정
- 복사본은 원본 변경에 영향받지 않음: 원본이 바뀌어도 이미 복사된 값은 그대로 유지
만약 print에서도 변경된 값을 보려면?
// 참조로 캡처하면 변경된 값을 볼 수 있음
auto print_ref = [&x]() {
cout << "x = " << x << endl;
};
또는
// 람다를 다시 생성하면 현재 값으로 새로 복사됨
print = [x]() {
cout << "x = " << x << endl;
};
즉, x를 값으로 캡처한 람다 함수는 x가 10이라는 정보를 참조가 아니기에 원본 외부 변수(x)를 확인하지 않고서
그대로 10으로 함수 내부에서 가지고 있다는 것.
캡처 리스트의 다양한 형태
[] | 아무것도 캡처하지 않음 |
[x, y] | x와 y를 값으로 캡처 |
[&x, &y] | x와 y를 참조로 캡처 |
[=] | 모든 외부 변수를 값으로 캡처 |
[&] | 모든 외부 변수를 참조로 캡처 |
[=, &x] | 모든 변수는 값으로, x만 참조로 캡처 |
[&, x] | 모든 변수는 참조로, x만 값으로 캡처 |
[this] | 현재 객체(this)를 캡처 (C++11) |
[*this] | 현재 객체의 복사본을 캡처 (C++17) |
복합 캡처 예제:
#include <iostream>
using namespace std;
int main() {
int a = 1, b = 2, c = 3;
// a, b는 값으로, c는 참조로 캡처
auto lambda = [a, b, &c]() {
// a, b는 변경 불가 (값 캡처)
// c는 변경 가능 (참조 캡처)
c = a + b + c;
cout << "c = " << c << endl;
};
lambda(); // 출력: c = 6
cout << "c in main: " << c << endl; // 출력: c in main: 6
return 0;
}
STL과 함께 사용하기
람다는 STL 알고리즘과 함께 사용시 특히 유용하다.
복잡한 함수 객체를 따로 정의하지 않고도 즉석에서 조건이나 동작을 정의할 수 있다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> nums = {5, 2, 8, 1, 9};
// 사용자 정의 정렬 (내림차순)
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
// 각 요소 출력
for_each(nums.begin(), nums.end(), [](int n) {
cout << n << " "; // 출력: 9 8 5 2 1
});
cout << endl;
// 조건에 맞는 요소 찾기 (5보다 큰 첫 번째 요소)
auto it = find_if(nums.begin(), nums.end(), [](int n) {
return n > 5;
});
if (it != nums.end()) {
cout << "First element > 5: " << *it << endl; // 출력: First element > 5: 9
}
return 0;
}
더 많은 STL 알고리즘과 람다 활용 예:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// count_if: 조건에 맞는 요소 개수 세기
int evens = count_if(nums.begin(), nums.end(), [](int n) {
return n % 2 == 0;
});
cout << "Even numbers: " << evens << endl; // 출력: Even numbers: 5
// transform: 각 요소 변환
vector<int> squared(nums.size());
transform(nums.begin(), nums.end(), squared.begin(), [](int n) {
return n * n;
});
cout << "Squared values: ";
for (int n : squared) {
cout << n << " ";
}
cout << endl; // 출력: Squared values: 1 4 9 16 25 36 49 64 81 100
// remove_if와 erase: 조건에 맞는 요소 제거
auto new_end = remove_if(nums.begin(), nums.end(), [](int n) {
return n % 3 == 0;
});
nums.erase(new_end, nums.end());
cout << "After removing multiples of 3: ";
for (int n : nums) {
cout << n << " ";
}
cout << endl; // 출력: After removing multiples of 3: 1 2 4 5 7 8 10
return 0;
}
Even numbers: 5
Squared values : 1 4 9 16 25 36 49 64 81 100
After removing multiples of 3: 1 2 4 5 7 8 10
Program ended with exit code: 0
mutable 람다
기본적으로 값으로 캡처된 변수는 람다 내부에서 수정 가능하다.
하지만 mutable 키워드를 사용하면 람다 내부에서 값으로 캡처된 변수를 수정할 수 있다.
#include <iostream>
using namespace std;
int main() {
int count = 0;
// mutable 람다 (값으로 캡처한 변수를 수정 가능)
auto counter = [count]() mutable {
count++;
cout << "count = " << count << endl;
};
counter(); // 출력: count = 1
counter(); // 출력: count = 2
// 원본 변수는 변경되지 않음
cout << "Original count: " << count << endl; // 출력: Original count: 0
return 0;
}
mutable을 사용하더라도 원본 변수는 수정되지 않지만,
람다 내부에서는 캡처된 변수의 복사본을 수정하는 것이다.
std::function과 람다
람다를 변수에 저장할 때 auto 대신 std::function을 사용할 수도 있다.
std::function을 사용하면 함수 타입을 명시적으로 지정할 수 있고,
다양한 함수형 객체를 저장할 수 있는 장점이 있다.
#include <iostream>
#include <functional>
using namespace std;
// 함수 인자로 람다를 받는 함수
void operate(int a, int b, function<int(int, int)> operation) {
cout << "Result: " << operation(a, b) << endl;
}
int main() {
// std::function에 람다 저장
function<int(int, int)> add = [](int a, int b) {
return a + b;
};
// 함수 호출
cout << "3 + 4 = " << add(3, 4) << endl; // 출력: 3 + 4 = 7
// 함수에 람다 전달
operate(5, 3, add); // 출력: Result: 8
// 람다를 직접 전달
operate(5, 3, [](int a, int b) {
return a * b;
}); // 출력: Result: 15
return 0;
}
재귀 람다식
람다에서 재귀를 사용하려면 약간의 트릭이 필요하다.
람다가 자기자신을 참조 할 수 있어야 하므로, 이를 위해 std::function과 같은 참조 캡처를 함께 사용 가능하다.
#include <iostream>
#include <functional>
using namespace std;
int main() {
// 재귀 람다: 팩토리얼 계산
function<int(int)> factorial = [&factorial](int n) -> int {
return (n <= 1) ? 1 : n * factorial(n - 1);
};
cout << "5! = " << factorial(5) << endl; // 출력: 5! = 120
// 재귀 람다: 피보나치 수열
function<int(int)> fibonacci = [&fibonacci](int n) -> int {
return (n <= 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
cout << "Fibonacci(7) = " << fibonacci(7) << endl; // 출력: Fibonacci(7) = 13
return 0;
}
람다에서 재귀를 사용시에 주의할 점은 람다가 참조하는 std::function 객체가
람다 정의전에 선언되어야만 한다는 것.
또한 람다가 참조로 자기 자신을 캡처([&factorial])하여 재귀호출을 가능하게 한다.
람다와 타입
람다는 고유한 익명 클래스 타입을 가지고 있다.
컴파일러는 각 람다식에 대해 고유한 클래스 타입을 생성하기 때문에, 해당 타입을 직접 쓰기는 어렵다.
그래서 보통 람다 사용시 auto 나 std::function을 사용해서 람다를 저장한다.
람다의 내부 동작 이해
컴파일러는 람다를 만나면 내부적으로 다음과 같은 작업을 수행한다.
1. 람다에 해당하는 익명클래스(함수 객체)를 생성
2. operator() 함수를 오버로딩하고서 람다의 본문을 구현
3. 필요한 경우 캡처된 변수의 클래스의 멤버변수로 저장.
예를 들어서 람다는 컴파일러에 의해서 대략 아래와 같은 클래스로 변하는데,
//람다
auto add = [](int a, int b) { return a + b; };
//컴파일러가 람다식을 클래스로 변환함.
class __Lambda_Add {
public:
int operator()(int a, int b) const {
return a + b;
}
};
auto add = __Lambda_Add{};
auto vs std::function
#include <iostream>
#include <functional>
using namespace std;
int main() {
// auto를 사용한 람다 저장
auto lambda1 = [](int x) { return x * x; };
// std::function을 사용한 람다 저장
function<int(int)> lambda2 = [](int x) { return x * x; };
cout << "Using auto: " << lambda1(5) << endl; // 출력: Using auto: 25
cout << "Using std::function: " << lambda2(5) << endl; // 출력: Using std::function: 25
return 0;
}
두 방식의 차이점이 있는데,
1. auto
- 컴파일러가 정확한 람다 타입을 그대로 사용한다.
- 더 효율적이고 성능이 좋다.
- 타입 정보가 컴파일 시간에 결정된다.
2. std::function
- 함수 타입을 명시적으로 지정가능하다.
- 다양한 함수형 객체(람다, 일반 함수, 함수 객체)를 동일한 타입으로 저장가능.
- 약간의 런타임 오버헤드 발생 가능.
- 함수 인자나 반환 타이으로 사용하기 편리함.
C++14/17/20에서의 람다 개선
람다는 이후 C++ 표준에서 계속 개선되어 왔다.
C++14 개선사항
- 제네릭 람다: 매개변수 타입으로 auto 사용 가능
auto generic = [](auto x, auto y) {
return x + y;
};
// 다양한 타입에 사용 가능
cout << generic(5, 3) << endl; // int + int
cout << generic(3.14, 2.71) << endl; // double + double
cout << generic(string("Hello "), string("World")) << endl; // string + string
- 초기화 캡처(Init Capture): 람다 내에서 변수 초기화 가능
auto resource = make_unique<Resource>();
auto lambda = [ptr = move(resource)]() {
ptr->use();
};
C++17 개선사항
- constexpr 람다: 컴파일 시간에 평가 가능
constexpr auto square = [](int x) constexpr {
return x * x;
};
// 컴파일 시간에 계산
constexpr int result = square(5); // 25
- this 캡처: 객체의 복사본을 캡처
struct Counter {
int value = 0;
auto byRef() {
return [this]() { return ++value; }; // 참조로 캡처
}
auto byCopy() {
return [*this]() mutable { return ++value; }; // 복사본 캡처
}
};
C++20 개선사항
- 템플릿 매개변수 람다:
auto lambda = []<typename T>(T x) {
return x.size();
};
- 캡처 시 구조체 분해:
auto [x, y] = getCoordinates();
auto lambda = [x, y]() {
return x * x + y * y;
};
10. 실제 사용 사례 및 모범 사례
실제 사용 사례
- 이벤트 핸들러/콜백 함수:
button.setOnClickListener([](auto event) {
cout << "Button clicked at: " << event.x << ", " << event.y << endl;
});
- 데이터 변환 및 필터링:
vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
vector<string> strings;
transform(numbers.begin(), numbers.end(), back_inserter(strings),
[](int n) { return n % 2 == 0 ? "even" : "odd"; });
- RAII(Resource Acquisition Is Initialization) 패턴:
void processFile(const string& filename) {
FILE* file = fopen(filename.c_str(), "r");
// 람다를 사용한 리소스 자동 해제
auto cleanup = [&]() {
if (file) {
fclose(file);
file = nullptr;
}
};
// 함수 종료 시 자동으로 cleanup 호출
auto guard = make_shared<void>(nullptr, [&](void*) { cleanup(); });
// 파일 처리 로직...
// 예외가 발생하더라도 cleanup은 보장됨
}
람다를 사용한 개발 팁
1. 간결성 유지하기.
- 람다는 짧고 집중된 목적을 가지고 있어야 한다.
- 너무 긴 람다는 별도의 명명도니 함수로 분리하는게 좋다.
2. 캡처 최소화 하기
- 필요한 변수만 캡처할 것. [=]나 [&]는 편리하긴 하지만 의도치 않은 캡처가 발생.
- 참조 캡처시 변수의 생명주기를 잘 봐둘 것,. 람다가 참조하는 변수가 스코프를 벗어나면 위험하다.
3. 가독성을 위한 명명된 람다 사용.
- 복잡한 조건이나 로직의 경우, 람다에 이름을 부여하면 코드 가독성이 향상된다.
auto isEven = [](int n) { return n % 2 == 0; };
auto isOdd = [](int n) { return n % 2 == 1; };
auto evenCount = count_if(nums.begin(), nums.end(), isEven);
auto oddCount = count_if(nums.begin(), nums.end(), isOdd);
람다 vs 함수 : 어떤 걸 어느때에 써야 할까?
- 간단한 일회용 함수는 람다가 적합하다.
- 재사용 가능하거나 상태유지가 필요한 복잡한 로직은 함수 객체가 훨씬 좋다.
'Game DevTip > C++' 카테고리의 다른 글
18. C++ 템플릿 활용 (4) | 2025.07.28 |
---|---|
16. C++ 순수 가상함수(추상 클래스 & 인터페이스)에 대해서 (5) | 2025.07.26 |
15. C++ 예외 처리 Try Catch문 (0) | 2025.07.25 |
14. C++ 이동의미론과 rvalue & lvalue (0) | 2025.07.22 |
13. C++ 스마트 포인터에 대해서 (1) | 2025.07.19 |
댓글