나만의 작은 도서관

[C++][Callback] #1. 콜백 함수와 Callable, 그리고 std::function 본문

C++/문법 및 메소드(STL)

[C++][Callback] #1. 콜백 함수와 Callable, 그리고 std::function

pledge24 2025. 4. 2. 23:32

콜백 함수란?

콜백 함수의 콜백 함수의 콜백 함수의...

  • 콜백 함수다른 함수의 매개변수로 전달되어 특정 시점에 실행되는 함수를 의미한다.

 

 

콜백 함수 예제

  • 아래 코드에서 CallableType은 호출 가능한 타입을 의미하며(뒤에서 설명), 실제로 존재하는 타입 식별자는 아니다.
// 함수를 매개변수로 받는 함수
void process(int a, int b, CallableType callback) {
    int sum = a + b;
    callback(sum); // 콜백 함수 호출
}

콜백 함수를 사용하는 이유

  • 콜백 함수는 “동작(함수)을 직접 실행하지 않고 넘겨준다”는 것이 핵심이다. 이렇게 동작을 함수에게 넘겨줬을 경우 아래와 같은 장점을 가지게 된다.

 

유연성과 코드 재사용성 (Flexibility & Code Reusability)

  • 상황에 따라 대응하는 동작을 수행할 수 있는 특성을 “유연성”이라고 한다. 콜백 함수를 사용하면 함수는 동작을 동적으로 결정할 수 있게 되므로 높은 유연성을 갖게 된다.
  • 또한, 콜백 함수를 사용하면 각 동작에 대한 함수를 따로 만들 필요 없이 같은 함수에 다양한 콜백을 넘겨주어 사용할 수 있으므로 코드 재사용성이 높아진다.
  • 콜백 함수를 활용하여 유연성과 코드 재사용성을 높인 대표적인 예시로 STL 알고리즘 헤더의 함수들이 있다. std::sort()로 예를 들면 아래와 같다.
// 내림차순 정렬을 위한 비교 함수
bool descending(int a, int b) {
    return a > b;
}

// sort()함수 하나로 다양한 기준의 정렬을 수행할 수 있다(유연성, 코드 재사용성 증가)
std::sort(numbers.begin(), numbers.end()); // 기본 정렬: 내림차순
std::sort(numbers.begin(), numbers.end(), descending); // 오름차순(콜백 함수 사용)

 

 

제어의 역전(Inversion Of Control, IoC)

  • 제어의 역전(IoC)이란 프로그램의 실행 흐름을 사용자가 직접 제어하지 않고, 프레임워크나 시스템이 제어 흐름을 담당하도록 하는 설계 원칙을 의미한다.
    • 즉, 제어의 역전은 특정 행동에 대해 “외부화”하는 패턴이다.
    • 제어의 역전을 활용하면 프로그램의 1) 유연성이 증가하고 2) 재사용성이 높아지며, 3) 결합도를 낮출 수 있다.
  • 콜백 함수는 제어의 역전을 구현하는 대표적인 방법이다. 실행되어야 할 동작을 함수 내부에 정의하지 않고, 동작을 정의한 함수를 콜백 함수로 등록하여 실행 흐름의 컨트롤을 외부 대상에게 인가할 수 있다.
void process(function<void()> callback) {
    cout << "Processing data..." << endl;
    callback();  // 외부에서 전달된 함수를 실행
}

int main() {
    process([]() { cout << "Callback executed!" << endl; }); // 람다식 활용
}

 

제어의 역전의 대표적인 예시: 이벤트 기반 프로그래밍

  • 이벤트 기반 프로그래밍에서는 특정 이벤트(클릭, 키 입력 등)가 감지되었을 때 미리 등록된 핸들러(콜백 함수)가 실행되므로, 애플리케이션의 흐름을 개발자가 직접 제어하는 것이 아니라 이벤트 루프나 프레임워크가 제어하게 된다,
  • 아래 코드의 Click 이벤트는 개발자가 직접 호출하는 것이 아니라 버튼이 클릭될 때 UI 프레임워크가 알아서 호출한다. 즉, 애플리케이션의 흐름을 이벤트 루프가 제어하므로 제어의 역전이 적용된 형태라 할 수 있다.
button.Click += (sender, args) => { Consol.WriteLine("버튼 클릭됨!"); };

 

 

순서 보장

  • 비동기 작업끼리의 순서는 보장할 수 없다. 이러한 비동기 작업들의 순서를 맞추어 실행하고 싶은 경우, 다음 순서에 실행되어야 할 비동기 작업을 콜백 함수로 넘겨주어 실행 순서를 맞출 수 있다. (”callback”이라는 이름값을 하듯)
#include <iostream>
#include <functional>

// 비동기 작업을 시뮬레이션하는 함수
void asyncTask(std::function<void(int)> callback) {
    int result = 42;  // 예제 결과 값
    callback(result); // 결과를 콜백 함수로 전달
}

int main() {
    asyncTask([](int result) {
        std::cout << "비동기 작업 완료! 결과: " << result << std::endl;
    });
    return 0;
}

 

 

관심사 분리(Separation of Concerns, SoC)

  • 관심사 분리(SoC)프로그램을 관심사 별로 쪼개서 한 번에 한 가지 책임에만 집중하도록 하는 설계 원칙이다.
    • 관심사 분리를 활용하면 코드의 복잡성이 줄어들어 유지보수를 용이해진다.
  • 콜백 함수를 활용하면 실행 흐름에서 특정 동작(행동)을 분리하여 독립적으로 관리할 수 있다. 특히 콜백 함수를  작업(Job)의 형태로 넘겨주고 이를 비동기적으로 실행하도록 설계한다면, 생산자(Producer)와 소비자(Consumer)의 역할이 분리되어 작업 처리를 효율적으로 수행할 수 있다.
// 전역 작업 큐 JobQueue(thread-safe 하다고 가정)

// 작업을 추가하는 생산자(Producer)
void Producer() {
    while(true){
        /* 실행해야할 작업 선택 */
        // ...
        // =================================
        
        // 콜백 함수를 작업의 형태로 추가
	JobQueue.push([]() { std::cout << "Task " << rand() % 100 << " 실행\\n"; });
    }
}

// 작업을 실행하는 소비자(Consumer)
void Consumer() {
    while (true) {
        /* queue에 데이터가 있을때까지 대기 */
        // ...
        // =================================

        // 작업을 pop + 콜백 함수 실행
        auto Job = std::move(JobQueue.front());
        JobQueue.pop();
        
        Job(); // 함수 실행
    }
}

int main() {
    std::thread producerThread(Producer); // Job Push
    std::thread consumerThread(Consumer); // Job Pop

    producerThread.join();  
    consumerThread.join();
    
    return 0;
}

 


호출 가능한 개체, Callable

CallableType callable; // callable 개체 선언
callable(); // callable 개체 호출
  • C++에서 “Callable”이란 호출 가능한 개체를 의미하며, () 연산자를 통해 호출할 수 있는 모든 개체들은 Callable에 포함된다. 대표적인 Callable들은 아래와 같다.

 

Callable 종류

  • 함수
  • 함수 포인터
  • 함수 객체(functor, 연산자 오버로딩을 사용한 클래스)
  • 람다 표현식

std::function

std::function<리턴_타입(매개변수들)> 변수_이름;
  • std::function는 래퍼 클래스(Wrapper Class)로, Callable을 “객체”의 형태로 보관하는 클래스이다.
  • std::function은 람다, 일반 함수, 함수 객체 등 모든 Callable 한 개체를 저장할 수 있기 때문에 굉장히 유용하다.

 

std::function 예시

#include <functional>
#include <iostream>
#include <string>

int some_func1(const std::string& a) {
  std::cout << "Func1 호출! " << a << std::endl;
  return 0;
}

struct S {
  void operator()(char c) { std::cout << "Func2 호출! " << c << std::endl; }
};

int main() {
 /* 함수 저장 */std::function<int(const std::string&)> f1 = some_func1;
 /* functor 저장 */std::function<void(char)> f2 = S();
 /* 람다식 저장 */std::function<void()> f3 = []() { std::cout << "Func3 호출! " << std::endl; };

  f1("hello");
  f2('c');
  f3();
}

 

실행 결과

Func1 호출! hello
Func2 호출! c
Func3 호출!

 


std::function에 멤버 함수 저장하기

  • 멤버 함수는 구현상 암묵적으로 자신을 호출한 객체를 인자로 받고 있기 때문에 일반 함수와 같은 방식으로 function에 저장하려고 시도하면 컴파일 오류가 발생한다. (this에 대한 정보를 알 수 없기 때문)
  • 따라서, 멤버 함수를 저장하려면 멤버 함수를 호출한 객체를 받는 인자를 추가적으로 전달해줘야 한다.
    • 일반 함수는 A&로, 상수 함수는 const A& 형태로 인자를 넘겨준다.
  • 추가로 조심해야 할 점은 함수 이름과 달리 멤버 함수는 함수의 주소값으로 “암시적 변환”이 발생하지 않으므로, ‘&’ 연산자를 통해 명시적으로 변환해줘야 한다.
#include <functional>
#include <iostream>
#include <string>

class A
{
public:
    A(int c) : c(c) {}
    void some_func() { std::cout << "비상수 함수: " << ++c << std::endl; }
    void some_const_function() const { std::cout << "상수 함수: " << c << std::endl; }
private:
    int c;
};

int main()
{
    A a(5);
    //std::function<void()> f1 = a.some_func; // 컴파일 오류: 비-정적 멤버 함수 'int A::some_func()'는 안됨
    std::function<void(A&)> f1 = &A::some_func; // 일반 멤버 함수 저장
    std::function<void(const A&)> f2 = &A::some_const_function; // 상수 멤버 함수 저장

    f1(a); // 비상수 함수: 6
    f2(a); // 상수 함수: 6
}

 

 

mem_fn함수

  • mem_fn함수멤버 함수 주소를 인자로 넣으면 function 객체를 리턴하는 함수로, 위와 같이 매번 function 객체를 따로 만들어서 전달해야 하는 귀찮음을 해소하기 위해 추가된 함수이다.
  • mem_fn함수가 하는 작업을 람다 표현식으로 대체하여 수행할 수 있기 때문에 그리 자주 쓰이진 않는다.
template <typename T>
void someFunc(T callback){}

std::function<size_t(const std::vector<int>&)> f1 = &std::vector<int>::size;
someFunc(f1); // std::function 사용
someFunc(std::mem_fn(&std::vector<int>::size));  // mem_fn 함수 사용
someFunc([](const auto& v){ return v.size(); }); // 람다 표현식 사용

 


참고 자료

https://www.reddit.com/r/learnjavascript/comments/u3f78o/can_someone_explain_the_advantage_of_using_a/?rdt=60635

https://en.wikipedia.org/wiki/Inversion_of_control

https://modoocode.com/254