Lambda Expressions
개요
C++ 람다 표현식은 이름 없는 함수 객체를 간결하게 생성할 수 있는 기능입니다. 람다는 코드 내에서 인라인으로 함수 객체를 정의할 수 있게 해주며, 특히 알고리즘과 함께 사용할 때 유용합니다.
C++ 버전별 주요 키워드 도입 시기
- C++11: 기본 람다 표현식 도입
- C++14: 일반화된 람다 캡처, 반환 타입 추론 개선
- C++17: constexpr 람다,
*this
캡처 - C++20: 템플릿 람다, [=, this] 명시적 캡처 경고 제거
내용 설명
기본 구문
[캡처](매개변수) -> 반환타입 { 함수 본문 }
캡처 모드
[=]
: 값으로 모든 외부 변수 캡처[&]
: 참조로 모든 외부 변수 캡처[a, &b]
: 특정 변수만 명시적으로 캡처 (a는 값으로, b는 참조로)[this]
: 현재 객체의 멤버에 접근[=, &x]
: x는 참조로, 나머지는 값으로 캡처[&, x]
: x는 값으로, 나머지는 참조로 캡처
람다의 내부 동작
컴파일러는 람다 표현식을 함수 객체(펑터)로 변환합니다. 캡처된 변수들은 해당 함수 객체의 멤버 변수가 됩니다. 다음은 람다가 컴파일러에 의해 어떻게 변환되는지 보여주는 예제입니다.
1. 기본 람다의 내부 구현
다음 람다 표현식은:
auto l = [](const auto n){ return n > 0; };
컴파일러에 의해 다음과 유사한 함수 객체로 변환됩니다:
template <typename T>
class _lambda1
{
public:
bool operator()(const T n) const
{
return n > 0;
}
};
2. 캡처가 있는 람다의 내부 구현
캡처가 있는 람다의 경우, 캡처된 변수들이 클래스의 멤버 변수로 추가됩니다. 예를 들어:
int min = 0, max = 100;
auto in_range = [min, &max](int n) {
return min <= n && n <= max;
};
위 코드는 컴파일러에 의해 다음과 유사한 클래스로 변환됩니다:
template <typename T>
class _lambda2
{
int minimum_; // 값으로 캡처된 변수
int& maximum_; // 참조로 캡처된 변수
public:
// 생성자에서 캡처된 변수들을 초기화
explicit _lambda2(int min, int& max)
: minimum_(min), maximum_(max) {}
// 복사 생성자, 이동 생성자 등
_lambda2(const _lambda2 &) = default;
_lambda2(_lambda2 &&) = default;
_lambda2 &operator=(const _lambda2 &) = delete;
~_lambda2() = default;
// 함수 호출 연산자 오버로딩
bool operator()(const T n) const
{
return minimum_ <= n && n <= maximum_;
}
};
이 변환 과정을 통해 람다는 일반 함수 객체처럼 동작하게 되며, 캡처된 변수들은 해당 함수 객체의 멤버 변수로 관리됩니다.
예제 코드
#include <algorithm>
#include <iostream>
#include <vector>
#include <numeric>
// 람다가 컴파일 타임에 어떻게 변환되는지 보여주는 예제 클래스들
// [](int n) { return n > 0; };
template <typename T>
class _lambda1
{
public:
bool operator()(const T n) const
{
return n > 0;
}
};
int main()
{
// 1. 기본 람다 표현식
auto is_positive = [](int n) { return n > 0; };
std::cout << "5 is positive: " << std::boolalpha << is_positive(5) << '\n';
// 2. 캡처를 사용한 예제
int min = 10, max = 20;
auto is_in_range = [min, &max](int n) {
return n >= min && n <= max;
};
std::cout << "15 is in range: " << is_in_range(15) << '\n';
// 3. STL 알고리즘과 함께 사용
std::vector<int> numbers(10);
std::iota(numbers.begin(), numbers.end(), 1);
int count = std::count_if(numbers.begin(), numbers.end(),
[](int n) { return n % 2 == 0; });
// std::iota는 <numeric> 헤더에 정의된 함수로, 지정된 범위([first, last))를
// 주어진 시작값(value)부터 시작하여 1씩 증가하는 값으로 채웁니다.
// 위 예제에서는 numbers 벡터를 1, 2, 3, ..., 10으로 초기화합니다.
std::cout << "Even numbers count: " << count << '\n';
// 4. 일반화된 람다 (C++14+)
auto generic_lambda = [](auto a, auto b) {
return a + b;
};
std::cout << "Sum (int): " << generic_lambda(1, 2) << '\n';
std::cout << "Sum (double): " << generic_lambda(1.5, 2.3) << '\n';
return 0;
}
실행 결과
5 is positive: true
15 is in range: true
Even numbers count: 5
Sum (int): 3
Sum (double): 3.8
실제 사용 예제
1. 벡터 출력 유틸리티
template <typename T>
void print_vector(const std::vector<T> &vec)
{
for (const auto &val : vec)
{
std::cout << val << '\n';
}
std::cout << '\n';
}
2. 일반 함수와 람다 비교
// 일반 함수 버전
template <typename T>
bool check_even(const T value)
{
return (value % 2) == 0;
}
// 람다 버전
const auto N = 2;
// Cap. List Param List Function Body
auto is_even = [N](const auto& value) { return (value % N) == 0; };
// 사용 예
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
// 일반 함수 사용
int count1 = std::count_if(numbers.begin(), numbers.end(), check_even<int>);
// 람다 사용
int count2 = std::count_if(numbers.begin(), numbers.end(),
[](int n) { return (n % 2) == 0; });
3. STL 알고리즘과의 조합
int main()
{
std::vector<int> numbers(10);
std::iota(numbers.begin(), numbers.end(), 1);
// 3의 배수 찾기
int multiple = 3;
auto is_multiple = [multiple](int n) { return n % multiple == 0; };
auto it = std::find_if(numbers.begin(), numbers.end(), is_multiple);
if (it != numbers.end()) {
std::cout << "첫 번째 " << multiple << "의 배수: " << *it << '\n';
}
// 짝수만 제거
numbers.erase(
std::remove_if(numbers.begin(), numbers.end(),
[](int n) { return n % 2 == 0; }),
numbers.end()
);
print_vector(numbers);
return 0;
}
활용팁
- 성능 고려사항: 작은 람다는 인라인 최적화가 잘 되어 있어 함수 포인터보다 빠를 수 있습니다.
- 가독성: 간단한 연산에 사용할 때 가독성이 좋지만, 복잡한 로직은 일반 함수나 함수 객체로 분리하는 것이 좋습니다.
- 캡처 주의사항: 참조 캡처 시 람다의 수명이 참조 대상보다 길어지지 않도록 주의해야 합니다.
- 템플릿 람다 (C++20):
auto
매개변수를 사용하여 일반화된 람다를 만들 수 있습니다. - 재귀 호출: 람다를 재귀적으로 호출하려면
std::function
을 사용하거나 명시적 함수 포인터 캐스팅이 필요합니다. - 디버깅: 람다의 타입은 컴파일러에 의해 생성되므로 디버깅 시 타입 이름이 복잡할 수 있습니다.
- noexcept 지정: 필요한 경우
noexcept
를 지정하여 예외 안전성을 보장할 수 있습니다.
추가 예제: 정렬 기준으로 사용하기
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 25},
{"Bob", 20},
{"Charlie", 30}
};
// 나이순으로 정렬 (오름차순)
std::sort(people.begin(), people.end(),
[](const auto& a, const auto& b) {
return a.age < b.age;
});
for (const auto& person : people) {
std::cout << person.name << " (" << person.age << ")\n";
}
return 0;
}
실행 결과
Bob (20)
Alice (25)
Charlie (30)
'개발 > C++ (98,03,11,14,17,20,23)' 카테고리의 다른 글
Modern C++ : std::numeric_limits (98, 11, 17) (0) | 2025.09.07 |
---|---|
Modern C++ : std::chrono 날짜 및 시간대 (11, 14, 17, 20) (1) | 2025.09.06 |
Modern C++ : std::chrono 시간 측정 (11, 14, 17, 20) (0) | 2025.09.05 |
Modern C++ : std::mt19937 (11, 14, 17, 20) (0) | 2025.09.04 |
Modern C++ : std::filesystem (0) | 2025.09.03 |
Modern C++ : std::ostream & std::istream (98, 11, 17, 20) (0) | 2025.09.02 |
Modern C++ : std::string_view (17, 20) (1) | 2025.09.01 |
Modern C++ : Small String Optimization (SSO) (3) | 2025.08.31 |