함수 객체
알고리즘들의 유연성을 더욱 높여주고, 사용자 정의 동작을 주입할 수 있게 해주는 것이 바로 함수 객체(Function Objects), 또는 줄여서 함수자(Functors) 입니다.
이번 장에서는 함수 객체가 무엇인지, 왜 필요한지, 그리고 STL 알고리즘과 함께 어떻게 활용되는지 자세히 알아보겠습니다.
또한 C++11 이후 도입된 람다(Lambda) 표현식이 함수 객체와 어떤 관계를 가지는지도 살펴보겠습니다.
함수 객체(Function Objects)란 무엇인가?
함수 객체는 operator()
연산자를 오버로딩하여 함수처럼 호출될 수 있는 객체를 의미합니다.
즉, 객체이면서 동시에 함수처럼 동작하는 개체입니다.
기본 아이디어: 일반 함수나 함수 포인터를 STL 알고리즘에 전달할 수 있지만, 여기에는 몇 가지 한계가 있습니다.
- 상태 저장 불가: 일반 함수나 함수 포인터는 상태(멤버 변수)를 가질 수 없습니다. 특정 값을 기억하여 연산에 활용해야 하는 경우, 전역 변수를 사용하거나 클로저를 구현해야 하는 등 번거로워집니다.
- 타입 정보 부족: 함수 포인터는 타입 정보가 부족하여 템플릿 인스턴스화 시 컴파일러 최적화에 불리할 수 있습니다.
함수 객체는 이러한 한계를 극복합니다.
함수 객체는 클래스의 인스턴스이므로 멤버 변수를 가질 수 있어 상태를 유지할 수 있습니다.
또한 함수처럼 호출될 때 operator()
연산자가 호출되므로 일반 함수와 동일한 방식으로 사용할 수 있습니다.
class MyFunctor {
public:
// operator() 연산자 오버로딩
반환타입 operator()(매개변수) const {
// 함수처럼 동작할 코드
return 결과;
}
// 멤버 변수를 통해 상태 저장 가능
// int someState;
};
// 사용 예시:
MyFunctor obj;
obj(인자); // obj.operator()(인자)와 동일하게 호출
함수 객체의 필요성 및 장점
함수 객체는 주로 STL 알고리즘에 사용자 정의 동작을 주입할 때 그 진가를 발휘합니다.
장점
- 상태 유지: 멤버 변수를 통해 상태를 저장하고 연산에 활용할 수 있습니다.
- 타입 안전성 및 최적화: 템플릿 인자로 전달될 때 컴파일러가 해당 타입을 명확히 알 수 있어 더 나은 최적화를 기대할 수 있습니다. (함수 포인터는 런타임에 주소를 찾는 오버헤드가 있을 수 있습니다.)
- 유연성: 다양한 방식으로 알고리즘의 동작을 커스터마이징할 수 있습니다.
- 강력함: 람다 표현식의 등장으로 함수 객체 작성이 더욱 간편해졌습니다.
컨테이너의 모든 요소에 특정 값을 더해서 출력하는 함수 객체를 만들어봅시다.
#include <iostream>
#include <vector>
#include <algorithm> // for_each
// 요소를 더하고 출력하는 함수 객체
class AddAndPrint {
private:
int _valueToAdd; // 상태 (멤버 변수)
public:
// 생성자를 통해 초기 상태를 설정
AddAndPrint(int val) : _valueToAdd(val) {}
// operator() 오버로딩: 각 요소에 대해 호출될 함수처럼 동작
void operator()(int n) const { // const를 붙여서 객체의 상태를 변경하지 않음을 명시
std::cout << n + _valueToAdd << " ";
}
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << "Original numbers: ";
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl;
std::cout << "Adding 10 and printing: ";
// AddAndPrint 객체를 생성하여 for_each에 전달
std::for_each(numbers.begin(), numbers.end(), AddAndPrint(10));
std::cout << std::endl; // 출력: 11 12 13 14 15
std::cout << "Adding 100 and printing: ";
std::for_each(numbers.begin(), numbers.end(), AddAndPrint(100));
std::cout << std::endl; // 출력: 101 102 103 104 105
return 0;
}
AddAndPrint
클래스는 _valueToAdd
라는 멤버 변수를 가지고 있어 std::for_each
가 각 요소를 처리할 때마다 이 값을 활용할 수 있습니다.
이는 일반 함수로는 직접적으로 하기 어려운 작업입니다.
STL이 제공하는 표준 함수 객체
STL은 <functional>
헤더에 미리 정의된 다양한 표준 함수 객체들을 제공합니다.
이들은 주로 연산자(+
, -
, *
, /
, <
, >
, ==
등)를 캡슐화한 형태입니다.
산술 연산 함수 객체
std::plus<T>
:a + b
std::minus<T>
:a - b
std::multiplies<T>
:a * b
std::divides<T>
:a / b
std::negate<T>
:-a
비교 연산 함수 객체
std::equal_to<T>
:a == b
std::not_equal_to<T>
:a != b
std::greater<T>
:a > b
std::less<T>
:a < b
std::greater_equal<T>
:a >= b
std::less_equal<T>
:a <= b
논리 연산 함수 객체
std::logical_and<T>
:a && b
std::logical_or<T>
:a || b
std::logical_not<T>
:!a
#include <iostream>
#include <vector>
#include <algorithm> // sort, transform
#include <functional> // std::greater, std::multiplies, std::plus
int main() {
std::vector<int> numbers = {5, 1, 4, 2, 8};
// std::sort와 std::greater를 사용하여 내림차순 정렬
std::sort(numbers.begin(), numbers.end(), std::greater<int>());
std::cout << "Sorted descending: ";
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl; // 출력: 8 5 4 2 1
std::vector<int> numbers2 = {1, 2, 3, 4, 5};
std::vector<int> result_mult(numbers2.size());
std::vector<int> result_plus(numbers2.size());
// std::transform과 std::multiplies를 사용하여 각 요소에 2를 곱하기
// (transform은 두 개의 입력 범위 또는 한 입력 범위와 한 상수값 사용 가능)
std::transform(numbers2.begin(), numbers2.end(), result_mult.begin(), std::bind(std::multiplies<int>(), std::placeholders::_1, 2));
// C++11 이전에는 std::bind1st, std::bind2nd 사용
// C++11 이후에는 람다가 훨씬 간편
std::cout << "Multiplied by 2: ";
for (int n : result_mult) {
std::cout << n << " ";
}
std::cout << std::endl; // 출력: 2 4 6 8 10
// 두 벡터의 요소 더하기 (std::plus 사용)
std::vector<int> v_a = {1, 2, 3};
std::vector<int> v_b = {4, 5, 6};
std::vector<int> v_sum(v_a.size());
std::transform(v_a.begin(), v_a.end(), v_b.begin(), v_sum.begin(), std::plus<int>());
std::cout << "Vector sum: ";
for (int n : v_sum) {
std::cout << n << " ";
}
std::cout << std::endl; // 출력: 5 7 9
return 0;
}
람다 표현식 (C++11 이후)
C++11부터 도입된 람다 표현식은 함수 객체를 훨씬 간결하게 만들 수 있는 문법적 설탕(Syntactic Sugar)입니다.
람다는 익명(anonymous) 함수 객체를 즉석에서 생성하는 방법이며, 특히 알고리즘의 세 번째 인자로 작은 함수 객체를 전달할 때 매우 유용합니다.
[캡처](매개변수) -> 반환타입 {
// 함수 본문
}
[캡처]
: 람다 외부의 변수를 람다 내부로 가져오는 방법을 지정합니다.[]
: 아무것도 캡처하지 않음[var]
:var
변수를 값으로 캡처 (복사)[&var]
:var
변수를 참조로 캡처[=]
: 람다 내부에서 사용되는 모든 외부 변수를 값으로 캡처[&]
: 람다 내부에서 사용되는 모든 외부 변수를 참조로 캡처[this]
: 현재 객체의this
포인터를 캡처
(매개변수)
: 람다 함수가 받을 매개변수 목록입니다.-> 반환타입
: 반환 타입입니다. 생략 가능하며, 컴파일러가 추론합니다.{ 함수 본문 }
: 람다 함수가 수행할 코드입니다.
#include <iostream>
#include <vector>
#include <algorithm> // for_each, sort
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int valueToAdd = 10; // 외부 변수
std::cout << "Adding " << valueToAdd << " and printing (lambda): ";
// 람다를 사용하여 AddAndPrint 함수 객체 역할을 대체
std::for_each(numbers.begin(), numbers.end(), [valueToAdd](int n){ // valueToAdd를 값으로 캡처
std::cout << n + valueToAdd << " ";
});
std::cout << std::endl; // 출력: 11 12 13 14 15
std::cout << "-------------------------------------------\n";
std::vector<std::string> names = {"Alice", "Bob", "Charlie", "David"};
// 길이가 5보다 긴 이름만 출력 (람다 사용)
std::cout << "Names longer than 5: ";
std::for_each(names.begin(), names.end(), [](const std::string& name){
if (name.length() > 5) {
std::cout << name << " ";
}
});
std::cout << std::endl; // 출력: Charlie David
// 복잡한 정렬 조건 (람다 사용)
std::vector<Person> people = {
{"Alice", 30},
{"Charlie", 25},
{"Bob", 35},
{"Amy", 25}
};
std::cout << "Original people:\n";
for (const auto& p : people) {
std::cout << p << " ";
}
std::cout << std::endl;
// 나이가 같으면 이름으로, 다르면 나이로 정렬
std::sort(people.begin(), people.end(), [](const Person& p1, const Person& p2){
if (p1.age != p2.age) {
return p1.age < p2.age; // 나이가 다르면 나이 기준
}
return p1.name < p2.name; // 나이가 같으면 이름 기준
});
std::cout << "Sorted by age then name:\n";
for (const auto& p : people) {
std::cout << p << " ";
}
std::cout << std::endl; // 출력: [Amy, 25] [Charlie, 25] [Alice, 30] [Bob, 35]
return 0;
}
람다는 특히 짧은 함수 객체가 필요한 경우에 매우 강력하고 편리합니다.
복잡한 함수 객체는 여전히 클래스로 정의하는 것이 더 명확할 수 있습니다.