[모던 C++ #8] 구식 function object와 std::bind를 버리고 람다와 std::function로!

과거 C++98/03 시절에는 함수 객체(function object) 클래스를 직접 정의하거나, std::bind를 통해 함수 호출을 커스터마이징하는 패턴이 흔했습니다. 하지만 이러한 방법은 코드가 장황해지고 의도를 분명히 드러내지 못하는 경우가 많았습니다. 호출 대상이 어디에 있는지, 어떤 인자를 바인딩했는지 일일이 추적해야 했으며, 이는 코드 가독성과 유지보수성에 악영향을 끼쳤습니다.

모던 C++에서 이 문제를 해결하기 위해 람다(lambda) 표현식과 std::function을 사용할 수 있습니다. 람다는 익명 함수 객체를 간결한 문법으로 정의할 수 있고, 캡처를 통해 외부 변수를 자연스럽게 접근할 수 있습니다. 또한 std::function은 범용 함수 포인터 래퍼로, 다양한 호출 대상(람다, 함수 객체, 함수 포인터 등)을 하나의 타입 안정한 콜러블로 다룰 수 있게 합니다.

관련 참고 자료:

과거: 함수 객체와 std::bind 사용의 번거로움

C++98/03 시절 함수 객체를 사용하려면, 호출 연산자(operator())를 오버로드하는 클래스를 직접 정의해야 했습니다. 또한 특정 함수에 일부 인자를 바인딩하기 위해 std::bind를 사용하면 코드가 더욱 복잡해졌습니다.

#include <functional>
#include <iostream>

struct Adder {
    int addend;
    Adder(int x) : addend(x) {}
    int operator()(int y) const {
        return addend + y;
    }
};

int main() {
    Adder add_five(5);
    std::cout << add_five(10) << "\n"; // 15 출력

    // std::bind를 이용해 특정 함수에 인자를 고정
    auto bound_func = std::bind(std::plus<int>(), 10, std::placeholders::_1);
    std::cout << bound_func(5) << "\n"; // 15 출력

    return 0;
}

이 예제는 간단해 보일 수 있으나, 복잡한 조건에서 다양한 함수 객체와 std::bind를 조합하면 코드가 난해해집니다. 또한 캡처링 기능이 없어 외부 변수를 함수 객체로 자연스럽게 전달하기도 어렵습니다.

현재: 람다 표현식과 std::function

람다 표현식: 간결하고 직관적인 코드

C++11 이후 람다는 별도의 클래스 정의 없이도 익명 함수 객체를 작성할 수 있게 합니다. 외부 스코프 변수를 캡처하여 사용할 수 있으며, 문법도 간결합니다.

#include <iostream>
#include <functional>

int main() {
    int base = 5;
    // base를 캡처한 람다
    auto add_base = [base](int x) {
        return base + x;
    };

    std::cout << add_base(10) << "\n"; // 15 출력

    // 인자를 바인딩하는 대신 람다 안에서 직접 처리할 수 있음
    auto fixed_add = [=](int val) {
        return base + val;
    };
    std::cout << fixed_add(5) << "\n"; // 10 출력

    return 0;
}

위 코드에서 람다는 base 변수를 캡처하고, 이를 함수 본문 안에서 자연스럽게 사용할 수 있습니다. 별도의 함수 객체 정의나 std::bind 호출 없이도 필요한 로직을 명확하게 표현할 수 있습니다.

std::function: 범용 호출 대상 래퍼

std::function은 특정 함수 시그니처를 가진 콜러블(callable) 객체를 추상화하는 템플릿 클래스입니다. 람다, 함수 포인터, 함수 객체 등 다양한 호출 대상을 하나의 std::function 변수를 통해 다룰 수 있으며, 이는 코드 모듈성을 높여줍니다.

#include <iostream>
#include <functional>

int main() {
    std::function<int(int)> func;

    // 람다 할당
    func = [](int x){ return x * 2; };
    std::cout << func(10) << "\n"; // 20 출력

    // 다른 람다로 재할당 가능
    int base = 5;
    func = [base](int x){ return base + x; };
    std::cout << func(10) << "\n"; // 15 출력

    return 0;
}

std::function을 사용하면 코드 중간에 호출 대상이 달라져도 동일한 인터페이스를 유지하면서 구현을 교체하기 쉽습니다.

왜 이런 변화가 필요한가?

  1. 간결성 및 가독성 향상
    람다를 사용하면 간단한 함수 객체를 정의하기 위해 긴 클래스를 작성할 필요가 없고, 외부 변수를 자연스럽게 접근 가능하므로 코드가 더 직관적입니다.
  2. 유연한 함수 바운딩
    std::bind 없이도 람다 본문에서 필요한 로직을 명확히 표현할 수 있고, std::function을 통해 다양한 콜러블을 동일한 타입으로 취급할 수 있어, 코드 확장성과 유지보수성이 개선됩니다.
  3. 표준화된 함수 객체 처리
    모던 C++은 함수 포인터, 함수 객체, 람다를 모두 std::function으로 다룰 수 있어, 호출 대상이 변경되어도 호출자 코드를 최소 변경으로 유지할 수 있습니다.
반응형