dh_0e

[C++] 콜백(Callback) 함수, using & typedef 본문

C++

[C++] 콜백(Callback) 함수, using & typedef

dh_0e 2026. 2. 11. 11:43

콜백(Callback) 함수

  • 정의: 나중에 호출될 함수를 의미하며, 함수를 실행할 때, 인자로 다른 함수를 전달하여 특정 이벤트가 발생하거나 작업이 완료되었을 때 그 함수가 실행되도록 하는 기법
  • 특징: 함수가 실행 주도권을 갖는 것이 아니라, 특정 시점에 호출되도록 등록하는 방식
  • 용도: 이벤트 처리, 서버 통신 완료 후 작업, 정렬 알고리즘의 비교 로직 등
  • 함수 포인터를 직접 쓰는 방식Modern C++의 std::function을 사용하는 방법이 있음

 

1. 전통적인 함수 포인터 방식 (typedef, using)

  • C 스타일의 방식으로, 함수가 저장된 메모리 주소를 직접 가리켜 전달
  • 문법은 복잡하지만 추가적인 오버헤드가 거의 없음
// 문법: typedef  반환형  (*별칭이름)(매개변수);
typedef void (*CallbackPtr)(int, double);

// 문법: using  별칭이름  =  반환형 (*)(매개변수);
using CallbackPtr = void (*)(int, double);
  • 반환형: void
  • 별칭 이름: CallbackPtr (반드시 *와 함께 괄호로 묶어야 함)
  • 매개변수: (int, double)
#include <iostream>

// 1. 선언
using ModernAlias = void (*)(int);

// 2. 콜백을 사용하는 함수
void RunLogic(int value, ModernAlias f) {
    std::cout << "로직 실행 중..." << std::endl;
    if (f) f(value); // 함수 주소를 찾아가 호출
}

// 3. 실제 전달할 함수
void OnFinished(int res) {
    std::cout << "전통적 방식 결과: " << res << std::endl;
}

int main() {
    RunLogic(100, OnFinished); // 함수 이름을 인자로 전달
    return 0;
}

 

2. 모던 C++ 방식 (std::function)

  • <functional> 헤더를 사용하는 방식으로 함수 포인터뿐만 아니라 람다, 함수 객체(Functor) 등 호출 가능한 모든 것을 담을 수 있음
  • 함수 포인터 특유의 (*) 기호가 사라져서 훨씬 깔끔하며 수학 함수 기호 $f(x)$와 비슷해서 이해하기 쉬움
#include <functional>

// 문법: using  별칭이름  =  std::function<반환형(매개변수)>;
using CallbackPtr = std::function<void(int, double)>;
  • 형태: using 함수이름 = std::function<return type(parameters)>

 

일반 함수, 람다 전달

#include <iostream>
#include <functional>

// 문법: using 별칭 = std::function<반환형(매개변수)>;
using ModernCallback = std::function<void(int, double)>;

void ExecuteModern(ModernCallback cb) {
    if (cb) cb(20, 6.28);
}

// 실제 콜백으로 쓰일 '일반 함수' 정의
void MyCallback(int a, double b) {
    std::cout << "일반 함수 콜백 호출됨! 값: " << a << ", " << b << std::endl;
}

int main() {
    // 1. 일반 함수 전달 가능
    ExecuteModern(MyCallback);

    // 2. 람다 전달 가능 (함수 포인터와의 결정적 차이!)
    ExecuteModern([](int a, double b) {
        std::cout << "모던 방식(람다): " << a << "와 " << b << std::endl;
    });

    return 0;
}

 

함수 객체(Functor) 전달

#include <iostream>
#include <functional>

// [Step 1] Functor 정의
class UpgradeTracker {
private:
    int count = 0; // 상태(강화 시도 횟수)를 저장할 수 있음!

public:
    // operator()를 오버로딩하면 객체를 함수처럼 쓸 수 있음
    void operator()(bool success, std::string name) {
        count++;
        std::cout << "[" << name << "] 현재까지 총 " << count << "번 시도 중..." << std::endl;
        if (success) std::cout << ">>> 강화 성공!" << std::endl;
    }
};

// [Step 2] 콜백을 받는 함수
void AttemptUpgrade(std::string item, std::function<void(bool, std::string)> callback) {
    bool result = (rand() % 2 == 0);
    callback(result, item); // 객체지만 함수처럼 호출됨
}

int main() {
    UpgradeTracker myTracker; // 객체 생성

    // [Step 3] 함수 객체를 콜백으로 전달
    AttemptUpgrade("집행검", myTracker); 
    AttemptUpgrade("집행검", myTracker); 
    
    return 0;
}

 

Thread 예시

void ThreadManager::Launch(function<void(void)> callback)
{
	LockGuard guard(_lock);

	_threads.push_back(thread([=]()
		{
			InitTLS();
			callback();
			DestroyTLS();
		}));
}
  • 다음과 같이 람다 함수를 사용하여 스레드가 callback 함수를 실행하기 전후로 다른 함수를 실행하게끔 해줄 수도 있음
구분 함수 포인터 (*) std::function
선언 문법 void (*)(int, double) std::function<void(int, double)>
람다 캡처 불가능 (상태를 가질 수 없음) 가능 (외부 변수를 가져와 쓸 수 있음)
성능 매우 빠름 (오버헤드 없음) 약간 느림 (객체 생성 및 호출 비용 발생)
유연성 낮음 (일반 함수만 가능) 높음 (함수 객체, 람다 등 모두 가능)

 

typedef & using

  • 두 키워드 모두 복잡한 타입에 별칭(Alias)을 붙여주는 역할을 하지만, 기능과 직관성에서 큰 차이가 있음
    • using은 typedef가 할 수 있는 모든 것을 할 수 있으며, 그 이상의 기능을 제공함

 

typedef (Type Definition)

  • 정의: C언어 시절부터 사용된 전통적인 타입 정의 방식
  • 특징: 변수 선언문 앞에 typedef를 붙여서 만드며, 이름이 문장 중간에 끼어 있어 가독성이 떨어지는 경우가 많음
// 문법: typedef 반환형 (*별칭이름)(매개변수);
typedef void (*CallbackPtr)(int, double);

void MyFunc(int a, double b) { /* 로직 */ }

int main() {
    CallbackPtr cb = MyFunc; // 별칭을 타입처럼 사용
    cb(10, 3.14);
    return 0;
}

 

using (Type Alias)

  • 정의: C++11부터 도입된 현대적인 타입 별칭 방식
  • 특징: using 별칭 = 타입; 형태를 취하며 대입 연산자(=)를 사용해 가독성이 좋음
// 문법: using 별칭이름 = 반환형 (*)(매개변수);
using CallbackPtr = void (*)(int, double);

void MyFunc(int a, double b) { /* 로직 */ }

int main() {
    CallbackPtr cb = MyFunc; // 변수 선언하듯 사용
    cb(20, 6.28);
    return 0;
}

 

typedef vs using

  • typedef와 using의 가장 큰 차이는 템플릿 별칭을 만들 수 있는가
    • using은 템플릿을 사용하여 새로운 타입을 정의할 수 있음
    • typedef는 직접적으로 템플릿화할 수 없어, 구조체 안에 넣어서 꺼내 쓰는 편법이 필요
/*---using---*/
template <typename T>
using Point = std::pair<T, T>; // T 타입의 좌표 쌍 정의

Point<int> p1;  // 간단하게 사용 가능
Point<double> p2;


/*---typedef---*/
template <typename T>
struct PointWrapper {
    typedef std::pair<T, T> type;
};

PointWrapper<int>::type p1; // 사용 시마다 ::type을 붙여야 함 (번거로움)

'C++' 카테고리의 다른 글

[C++] Smart Pointer  (0) 2026.02.09
[C++] lambda, 타입 추론(auto), 일반화 함수(template), Singleton in C++  (3) 2025.08.05
[C++] memset, isupper/islower  (0) 2025.02.13
[C++] stringstream  (2) 2024.07.22
[C++] max_element, min_element, find  (0) 2024.07.12