안녕하세요.
오늘날 C++ 개발에서 람다(Lambda) 표현식을 알게 모르게 사용하고 있습니다. 코드를 간결하게 만들고 알고리즘과 함께 사용할 때 가독성을 크게 높여줍니다.
하지만 람다가 표준으로 채택된 것은 C++11부터입니다.
그렇다면 람다가 없던 C++03 시절에는 비슷한 작업을 어떻게 처리했는지 알아보겠습니다.
count_if 알고리즘을 예제로 C++ 프로그래밍 기법으로 확인해 보겠습니다.
1. 가장 전통적인 방식: 함수 포인터 (Function Pointer)
가장 고전적인 방법은 함수 포인터를 알고리즘에 직접 전달하는 것입니다. 특정 조건을 검사하는 전역 함수를 만들고, 이 함수의 주소를 넘겨주는 방식입니다.
<코드 예시>
#include <stdio.h>
#include <vector>
#include <functional>
// 1. 조건 검사를 위한 전역 함수
bool bLess1(int i) {
return i < 5;
}
void main()
{
int data[] = { 1, 2, 3, 4, 5, 6 };
int iCount = count_if(&data[0], &data[6], bLess1);
printf("bLess1 count = %d\n", iCount); // 출력: 4
}
<코드 설명>
count_if 함수는 bLess1 함수의 메모리 주소(포인터)를 받아, data 배열의 각 요소를 순회하며 bLess1을 호출합니다. 조건이 참일 때마다 카운트가 증가합니다.
간단하지만, 만약 5가 아닌 다른 값과 비교하고 싶다면 새로운 함수를 또 만들어야 하는 유연성의 한계가 있습니다.
2. 함수 객체 (Function Object / Functor)
함수 포인터의 한계를 극복하기 위해 등장한 것이 바로 함수 객체(Functor)입니다. 클래스에 operator() 연산자를 오버로딩하여, 객체인데도 마치 함수처럼 호출할 수 있게 만드는 기법입니다.
// 2. 함수처럼 동작하는 클래스
class CLess2 {
public:
bool operator()(int left) {
return left < 5;
}
};
void main()
{
...
CLess2 cLess2; // 객체 생성
iCount = count_if(&data[0], &data[6], cLess2);
printf("cLess2 count = %d\n", iCount); // 출력: 4
}
<코드 설명>
cLess2 객체는 함수가 아니지만 () 연산자 덕분에 count_if 내부에서 함수처럼 호출할 수 있습니다. 함수 객체는 멤버 변수(상태)를 가질 수 있어 함수 포인터보다 훨씬 유연합니다. (예: 생성자로 비교 값을 전달받아 저장) 하지만 위 예제 자체는 여전히 비교 값 5가 코드에 고정되어 있습니다.
3. std::bind와 함수 객체의 조합하여 유연성 주기
count_if는 인자를 하나만 받는 조건 함수(Unary Predicate)를 요구합니다. 하지만 우리가 만들고 싶은 일반적인 비교 로직은 보통 a < b처럼 인자가 두 개 필요합니다.
C++03 시절, 이 문제를 해결하고 유연성을 극대화하기 위해 사용한 최고의 조합이 바로 std::bind와 일반화된 함수 객체였습니다.
<std::bind란?>
std::bind는 이름 그대로 함수의 인자를 묶어주는 도구입니다. 기존 함수의 인자 중 일부를 특정 값으로 미리 채워 넣어, 마치 새로운 함수인 것처럼 만들어 줍니다.
- 인자 개수 조절 : 인자 2개짜리 함수를 인자 1개짜리 함수로 바꿀 수 있습니다.
- 자리 표시자(std::placeholders::_1): "이 자리는 나중에 채워질 첫 번째 인자를 위한 자리야"라고 표시하는 역할을 합니다.
<코드 예시>
// 3. 인자를 두 개 받는 일반적인 비교 함수 객체
template<typename T>
class CLess3 {
public:
bool operator()(const T& left, const T& right) {
return left < right;
}
};
void main()
{
...
// CLess3의 두 번째 인자를 5로 '묶어서' 인자 1개짜리 함수 객체로 변환
iCount = count_if(&data[0], &data[6],
std::bind(CLess3<int>(), std::placeholders::_1, 5));
printf("cLess3 + bind count = %d\n", iCount); // 출력: 4
}
<코드 설명>
std::bind 처리 부분의 설명입니다.
(1) 인자 두 개를 받는 CLess3 객체를 가져옵니다.
(2) 두 번째 인자는 5로 고정(bind)합니다.
(3) 첫 번째 인자는 std::placeholders::_1을 사용해 비워둡니다.
결과적으로 std::bind는 count_if`가 사용할 수 있는 "인자 하나를 받아서 그 인자와 5를 비교하는" 새로운 임시 함수 객체를 즉석에서 만들어 전달한 것입니다.
4. 람다(Lambda) 등장
std::bind가 있었지만, 문법이 복잡하고 placeholders 개념을 이해해야 해서 코드를 읽기 어려웠습니다.
C++11에서 람다가 등장하며 이 부분의 문제를 해결하게 되었습니다.
std::bind로 길게 작성했던 코드는 람다로 이렇게 표현됩니다.
// 람다를 사용한 가장 현대적인 방식
iCount = count_if(&data[0], &data[6], [](int i) { return i < 5; });
printf("lambda count = %d\n", iCount); // 출력: 4
std::bind와 함수 객체를 따로 만들 필요 없이, 코드가 필요한 그 위치에 훨씬 직관적이고 간결하게 로직을 표현할 수 있습니다.
이것이 바로 람다가 등장하며 std::bind의 쓰임새가 크게 줄어든 이유이며, 현대 C++ 개발에 사용하는 이유입니다.
<전체 코드>
#include <stdio.h>
#include <vector>
#include <functional> //for bind
bool bLess1( int i ) //function pointer
{
return i < 5;
}
class CLess2 //function object
{
public:
bool operator()(int left) {
return left < 5;
}
};
template<typename T>
class CLess3
{
public:
typedef const T first_argument_type;
typedef const T second_argument_type;
typedef bool result_type;
result_type operator() (first_argument_type& _left, second_argument_type& _right) {
return _left < _right;
}
};
template<typename T, typename U>
int count_if(T first, T last, U _predicate)
{
int ret = 0;
while (first != last)
{
if (_predicate(*first))
{
ret++;
}
++first;
}
return ret;
}
void main() {
int data[] = { 1,2,3,4,5,6 };
int iCount = 0;
iCount = count_if(&data[0], &data[_countof(data)], bLess1);
printf("bLess1 count = %d\n", iCount);
CLess2 cLess2 = CLess2();
iCount = count_if(&data[0], &data[_countof(data)], cLess2);
printf("cLess2 count = %d\n", iCount);
iCount = count_if(&data[0], &data[_countof(data)], std::bind(CLess3<int>(), std::placeholders::_1, 5));
printf("cLess2 count = %d\n", iCount);
iCount = count_if(&data[0], &data[_countof(data)], [](int i) { return i < 5; });
printf("lambda count = %d\n", iCount);
}//main
간단히 람다까지 발전하게 된 것을 보았는데 람다는 한번 더 다뤄 보겠습니다.
감사합니다.
<참고 사이트>
1. C++ lambda01 function pointer and function object
https://www.youtube.com/watch?v=cRG-wW5cUdw&list=PLrrTotxaO6khn83BjtBN-1HMDc9MZ__yt&index=20
'Programming > C, C++' 카테고리의 다른 글
[C++] const 객체를 선언 시 멤버 함수 에러 상황과 this 포인터 이해하기 (4) | 2025.07.18 |
---|---|
[C/C++] 구조체(struct) 기초 확인하기 (3) | 2025.07.12 |
[C++] 스마트 포인터 weak_ptr에 대해서 알아보기 (0) | 2025.07.05 |
[C++] if 조건문과 암시적 bool 형변환에 대해 이해하기 (2) | 2025.06.26 |
[C++] shared_ptr (1) - 필요성에 대해 알아보기 (0) | 2025.06.20 |