나만의 작은 도서관
[C++][Callback] #1. 콜백 함수와 Callable, 그리고 std::function 본문
콜백 함수란?

- 콜백 함수란 다른 함수의 매개변수로 전달되어 특정 시점에 실행되는 함수를 의미한다.
콜백 함수 예제
- 아래 코드에서 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(); }); // 람다 표현식 사용
참고 자료
'C++ > 문법 및 메소드(STL)' 카테고리의 다른 글
[C++][Callback] #3. 함수 객체(functor) (0) | 2025.04.03 |
---|---|
[C++][Callback] #2. 함수 포인터(Function Pointer) (0) | 2025.04.02 |
[C++] 전방 선언(Forward Declaration) (0) | 2025.03.28 |
[C++] 캐스팅(Casting) (0) | 2025.03.28 |
[C++][Class] 클래스 관련 기타 내용(explicit, mutable, friend 키워드 등...) (0) | 2025.03.26 |