icon
12장 : 표준 템플릿 라이브러리

함수 객체

알고리즘들의 유연성을 더욱 높여주고, 사용자 정의 동작을 주입할 수 있게 해주는 것이 바로 함수 객체(Function Objects), 또는 줄여서 함수자(Functors) 입니다.

이번 장에서는 함수 객체가 무엇인지, 왜 필요한지, 그리고 STL 알고리즘과 함께 어떻게 활용되는지 자세히 알아보겠습니다.

또한 C++11 이후 도입된 람다(Lambda) 표현식이 함수 객체와 어떤 관계를 가지는지도 살펴보겠습니다.


함수 객체(Function Objects)란 무엇인가?

함수 객체operator() 연산자를 오버로딩하여 함수처럼 호출될 수 있는 객체를 의미합니다.

즉, 객체이면서 동시에 함수처럼 동작하는 개체입니다.

기본 아이디어: 일반 함수나 함수 포인터를 STL 알고리즘에 전달할 수 있지만, 여기에는 몇 가지 한계가 있습니다.

  1. 상태 저장 불가: 일반 함수나 함수 포인터는 상태(멤버 변수)를 가질 수 없습니다. 특정 값을 기억하여 연산에 활용해야 하는 경우, 전역 변수를 사용하거나 클로저를 구현해야 하는 등 번거로워집니다.
  2. 타입 정보 부족: 함수 포인터는 타입 정보가 부족하여 템플릿 인스턴스화 시 컴파일러 최적화에 불리할 수 있습니다.

함수 객체는 이러한 한계를 극복합니다.

함수 객체는 클래스의 인스턴스이므로 멤버 변수를 가질 수 있어 상태를 유지할 수 있습니다.

또한 함수처럼 호출될 때 operator() 연산자가 호출되므로 일반 함수와 동일한 방식으로 사용할 수 있습니다.

함수 객체 기본 형식
class MyFunctor {
public:
    // operator() 연산자 오버로딩
    반환타입 operator()(매개변수) const {
        // 함수처럼 동작할 코드
        return 결과;
    }
    // 멤버 변수를 통해 상태 저장 가능
    // int someState;
};

// 사용 예시:
MyFunctor obj;
obj(인자); // obj.operator()(인자)와 동일하게 호출

함수 객체의 필요성 및 장점

함수 객체는 주로 STL 알고리즘에 사용자 정의 동작을 주입할 때 그 진가를 발휘합니다.

장점

  1. 상태 유지: 멤버 변수를 통해 상태를 저장하고 연산에 활용할 수 있습니다.
  2. 타입 안전성 및 최적화: 템플릿 인자로 전달될 때 컴파일러가 해당 타입을 명확히 알 수 있어 더 나은 최적화를 기대할 수 있습니다. (함수 포인터는 런타임에 주소를 찾는 오버헤드가 있을 수 있습니다.)
  3. 유연성: 다양한 방식으로 알고리즘의 동작을 커스터마이징할 수 있습니다.
  4. 강력함: 람다 표현식의 등장으로 함수 객체 작성이 더욱 간편해졌습니다.

컨테이너의 모든 요소에 특정 값을 더해서 출력하는 함수 객체를 만들어봅시다.

std::for_each와 함수 객체 사용 예시
#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;
}

람다는 특히 짧은 함수 객체가 필요한 경우에 매우 강력하고 편리합니다.

복잡한 함수 객체는 여전히 클래스로 정의하는 것이 더 명확할 수 있습니다.