function
은 std 네임스페이스
에 속한 함수 객체
이다.
함수 객체
는 함수는 아니지만 함수처럼 호출할 수 있도록 정의한 객체를 말한다.
함수 포인터
는 포인터를 활용하여 함수를 호출하는 방법이다. 함수 호출은 메모리에서 함수의 주소값을 찾아가는 명령이므로 함수 포인터로 함수의 주소값을 찾아 가는 동작도 함수 호출과 동일한 결과를 가진다.
C
와 C++
언어에서 함수 포인터는 콜백
을 구현할 때 자주 사용한다.
콜백(callback)
이란 다시 부른다는 의미로, 프로그램에서 다른 함수에 의해 특정한 실행이 필요할 때 호출되는 함수를 뜻한다.
예를 들어 퀵 정렬 함수 qsort
는 다음처럼 선언되어 있다.
// qsort 함수 원형
void qsort (
void* base,
size_t num,
size_t size,
int (*compare)(const void*, const void*)
);
qsort
함수의 원형에서 네 번째 매개변수 compare
는 함수 포인터
이다. compare
는 비교 함수를 전달받는 함수 포인터
이면서 qsort
에 의해 호출되는 콜백 함수
이다.
함수 포인터
는 콜백
을 구현할 때 사용할 수 있을 뿐만 아니라, 여러 함수를 구조체나 배열에 포인터로 보관한 후 필요한 함수를 적절히 선택하는 알고리즘을 구현할 때 등 다양한 방식으로 사용될 수 있다. 다만, 함수 포인터를 남발하면 가독성이 떨어지고 유지/보수가 어려워지므로 적절하게 사용하는 것이 좋다.
함수 포인터는 함수의 주소를 저장하는 변수이다.
C++
에서 함수는 정적 함수
와 멤버 함수
로 나눌 수 있다. 여기서 정적 함수
로는 전역 함수
, namespace 내의 전역 함수
, static 멤버 함수
가 해당된다. 멤버 함수
는 다시 객체와 주소로 각각 호출할 수 있으므로 함수 호출은 세 가지가 있다.
다음은 세 가지 방식의 함수 호출 예이다.
#include <iostream>
using namespace std;
void Print()
{
cout << "정적 함수 Print()" << endl;
}
class Point
{
public:
void Print()
{
cout << "멤버 함수 Print()" << endl;
}
};
int main()
{
Point pt;
Point* p = &pt;
Print(); // 정적 함수 호출
pt.Print(); // 객체로 멤버 함수 호출
p->Print(); // 포인터로 멤버 함수 호출
return 0;
}
정적 함수 포인터
는 함수 시그니처만 알면 쉽게 선언할 수 있따.
정적 함수
인 전역 함수
, namespace 내의 전역 함수
, static 멤버 함수
는 모두 함수 호출 규약
이 같아서 함수 포인터가 같다.
다음은 전역 함수
, namespace 내의 전역 함수
, static 멤버 함수
의 정적 함수 포인터 예이다.
#include <iostream>
using namespace std;
void Print()
{
cout << "전역 함수 Print()" << endl;
}
namespace A
{
void Print()
{
cout << "namespace A의 전역 함수 Print()" << endl;
}
}
class Point
{
public:
static void Print()
{
cout << "Point 클래스의 정적 멤버 함수 Print()" << endl;
}
};
int main()
{
void (*pf)(); // 함수 포인터 선언
Print();
A::Print();
Point::Print();
pf = Print;
pf(); // 함수 포인터를 통해 Print() 호출
pf = A::Print;
pf(); // 함수 포인터를 통해 A::Print() 호출
pf = Point::Print;
pf(); // 함수 포인터를 통해 Point::Print() 호출
return 0;
}
/* 결과
전역 함수 Print()
namespace A의 전역 함수 Print()
Point 클래스의 정적 멤버 함수 Print()
전역 함수 Print()
namespace A의 전역 함수 Print()
Point 클래스의 정적 멤버 함수 Print()
*/
결과에서 보듯이 정적 함수
는 모두 같은 함수 포인터 pf
를 사용한다.
📌 함수 호출 규약
함수 호출 규약
은 함수 호출 시 전달되는 인자의 순서나 함수가 종료될 때 함수의 스택을 정리하는 시점을 약속한 것이다.
대표적인 함수 호출 규약으로stdcall
,cdecl
,thiscall
,fastcall
등이 있다.
C++
언어의 정적 함수 기본 함수 호출 규약은cdecl
이다. 또한 멤버 함수는thiscall
을 사용한다. 그래서정적 함수 포인터
와멤버 함수 포인터
를 각기 다르게 선언한다.
멤버 함수 포인터
는 함수 포인터 선언에 어떤 클래스의 멤버 함수를 가리킬 것인지 클래스 이름을 지정해야 한다.
시그니처가 void Point::Print(int n)
인 멤버 함수의 포인터는 void (Point::*pf)(int)
처럼 선언한다.
함수 호출은 다음과 같이 멤버 함수 호출 방법에 따라 다르다.
*
객체로 멤버 함수 호출 시에는 .*
연산자를 이용한다. 예를 들면 (객체.*pf)(10)
처럼 사용한다.*
주소로 멤버 함수 호출 시에는 ->*
연산자를 이용한다. 예를 들면 (주소->*pf)(10)
처럼 사용한다.#include <iostream>
using namespace std;
class Point
{
public:
explicit Point(int x, int y) : x(x), y(y) {}
void Print() const { cout << "Point(" << x << ", " << y << ")" << endl; }
void PrintInt(int n) { cout << "Point(" << x << ", " << y << ") with int: " << n << endl; }
private:
int x, y;
};
int main()
{
Point pt(2, 3);
Point* p = &pt;
void (Point:: * pf1)() const;
pf1 = &Point::Print;
void (Point:: * pf2)(int);
pf2 = &Point::PrintInt;
pt.Print();
pt.PrintInt(5);
cout << endl;
(pt.*pf1)();
(pt.*pf2)(5);
cout << endl;
(p->*pf1)();
(p->*pf2)(5);
return 0;
}
/* 결과
Point(2, 3)
Point(2, 3) with int: 5
Point(2, 3)
Point(2, 3) with int: 5
Point(2, 3)
Point(2, 3) with int: 5
*/
어떤 기능이나 서비스를 제공하는 코드 측을 서버 코드(서버)
라 한다. 그 기능을 제공받는 코드 측을 클라이언트 코드(클라이언트)
라 한다. 일반적으로 서버는 하나지만 서버 코드를 사용하는 클라이언트는 여러 개이다.
#include <iostream>
using namespace std;
// Server
void PrintHello()
{
cout << "Hello from the server!" << endl;
}
// Client
int main()
{
PrintHello();
return 0;
}
PrintHello()
함수는 출력 기능을 제공하므로 서버
이다. main()
함수는 PrintHello()
함수를 호출해 출력 기능을 제공받으므로 클라이언트
이다.
일반적으로 클라이언트 코드 측에서 서버를 호출하고 기능을 사용하지만, 때때로 서버가 클라이언트를 호출해야 하는 경우도 있다.
이처럼 클라이언트
가 서버
를 호출하면 콜(call)
이라 하고, 서버
가 클라이언트
를 호출하면 콜백(callback)
이라고 한다.
콜백 매커니즘
을 이용하면 알고리즘 정책을 클라이언트에서 유연하게 바꿀 수 있게 서버를 더욱 추상화
할 수 있다. 또한, 대부분 GUI
의 강력한 이벤트 기능도 콜백 메커니즘
으로 구현된다.
STL
의 많은 알고리즘도 콜백
을 이용해 클라이언트 정책을 반영한다. 윈도우의 모든 프로시저(procedure)
는 시스템이 호출하는 콜백 함수
이다.
다음은 서버에서 클라이언트를 호출하는 콜백 함수
예제이다.
#include <iostream>
using namespace std;
// Client
void Client()
{
cout << "Hello from the client!" << endl;
}
// Server
void PrintHello()
{
cout << "Hello from the server!" << endl;
Client(); // 서버에서 클라이언트 코드 호출
}
int main()
{
PrintHello();
return 0;
}
/* 결과
Hello from the server!
Hello from the client!
*/
PrintHello()
함수인 서버에서 클라이언트 측 코드인 Client()
함수를 호출한다. 이때 Client()
함수를 콜백 함수
라고 한다.
위의 예제는 콜백 메커니즘
을 보여주기 위한 예제이다. 실제로 서버 코드를 이렇게 구현할 수는 없다. 서버는 여러 클라이언트에 의해 호출되면 클라이언트의 존재를 알지 못한다. 그래서 위의 예제처럼 서버가 미리 Client()
함수를 알고 호출하는 것은 불가능하다. 따라서 클백 매커니즘
을 구현하려면 클라이언트가 서버를 호출할 때 서버에 클라이언트의 정보를 제공해야 한다.
서버에 클라이언트 정보를 제공하는 방법 중 대표적인 방법이
함수 포인터
매개변수를 이용해, 콜백 함수의주소
를 전달하는 방법이다. 그 외에 함수 객체, 대리자, 전략 패턴 등을 사용한다.
다음은 함수 포인터
를 이용한 콜백 매커니즘
을 예제이다. 여기서 서버 함수 For_each()
는 정수형 배열의 원소를 begin부터 end까지 이동하며 클라이언트 콜백 함수를 호출한다. 서버는 배열의 원소에 대해 반복적인 작업을 수행할 뿐 꾸체적인 작업은 알지 못한다. 구체적인 작업은 클라이언트에서 콜백 함수 Print1()
, Print2()
, Print3()
를 이용해 수행한다.
#include <iostream>
using namespace std;
using FuncPtr = void (*)(int);
// Server
// 배열의 모든 원소에 반복적인 작업을 수행하게 추상화됨 (구체적인 작업은 없음)
void For_each(int* begin, int* end, FuncPtr pf)
{
while (begin != end)
{
pf(*begin++);
}
}
// Client
void Print1(int n)
{
cout << n << " ";
}
void Print2(int n)
{
cout << n * n << " ";
}
void Print3(int n)
{
cout << "정수: " << n << endl;
}
int main()
{
int arr[5] = { 10, 20, 30, 40, 50 };
For_each(arr, arr + 5, Print1); // Print1() 콜백 함수의 주소 전달
cout << endl << endl;
For_each(arr, arr + 5, Print2); // Print2() 콜백 함수의 주소 전달
cout << endl << endl;
For_each(arr, arr + 5, Print3); // Print3() 콜백 함수의 주소 전달
return 0;
}
/* 결과
10 20 30 40 50
100 400 900 1600 2500
정수: 10
정수: 20
정수: 30
정수: 40
정수: 50
*/
클라이언트는 서버 함수 For_each()
를 세 번 호출한다. 하지만, 세 번의 출력 결과는 클라이언트에 의해 결정된다. 출력 정책은 클라이언트만이 알고 있다.
다음은 위의 예제를 STL
의 for_each
알고리즘을 사용하여 다시 작성한 예이다.
#include <algorithm>
#include <iostream>
using namespace std;
using FuncPtr = void (*)(int);
// Client
void Print1(int n)
{
cout << n << " ";
}
void Print2(int n)
{
cout << n * n << " ";
}
void Print3(int n)
{
cout << "정수: " << n << endl;
}
int main()
{
int arr[5] = { 10, 20, 30, 40, 50 };
for_each(arr, arr + 5, Print1); // Print1() 콜백 함수의 주소 전달
cout << endl << endl;
for_each(arr, arr + 5, Print2); // Print2() 콜백 함수의 주소 전달
cout << endl << endl;
for_each(arr, arr + 5, Print3); // Print3() 콜백 함수의 주소 전달
return 0;
}
/* 결과
10 20 30 40 50
100 400 900 1600 2500
정수: 10
정수: 20
정수: 30
정수: 40
정수: 50
*/
STL
의 표준 라이브러리 함수 for_each
를 사용한 것 외에 다른 것은 없다.
펑터(functor)
는 함수 객체(function object)
라고 한다. 함수 객체
를 풀어서 설명하면 함수처럼 호출할 수 있는 객체(callable object)
를 말한다.
#include <iostream>
using std::cout;
using std::endl;
struct bomb {
void operator()() {
cout << "bomb" << endl;
}
void operator()(int range) {
cout << "bomb range: " << range << endl;
}
};
int main()
{
bomb mine;
mine();
mine(30);
return 0;
}
실행 결과
bomb
bomb range: 30
구조체에서 펑터
를 정의하려면 함수 호출 연산자 operator()
를 오버로딩
한다. 그러면 구조체 객체를 선언한 후 객체 자체를 함수처럼 사용할 수 있다. 이처럼 펑터
는 연산자 오버로딩으로 정의하므로 구조체뿐만 아니라 클래스에 사용할 수도 있다.
펑터
는 함수 포인터
와 유사한 목적으로 사용된다. 호출 가능한 객체를 다른 함수에 전달하거나 클래스 객체 안에 저장할 수 있다. 함수 포인터
는 포인터로만 전달할 수 있지만, 펑터
는 객체
이므로 객체
, 포인터
, 참조자
등 다양한 형태로 저장과 복사, 호출할 수 있다.
C++
에서는 함수 포인터, 펑터, 람다까지 객체를 함수처럼 활용하는 다양한 방법이 있다. 모던 C++
에서는 이처럼 다양한 호출 객체를 통일된 형식으로 사용할 수 있도록 funciton 클래스
를 제공한다.
function
은 클래스 템플릿으로 정의되었으며 function
으로 선언한 객체에는 함수
, 펑터
, 람다
그리고 클래스의 멤버 함수를 저장하고 호출할 수 있다.
// function 클래스
// 함수를 저장하는 function 클래스
function<return_data_type(param0, param1)> func_name = function;
// 클래스, 구조체 멤버 함수를 저장하는 function 객체
function<return_data_type(object&, param0, param1)> func_name = &class|struct::target_method;
function
객체를 생성할 때는 함수처럼 반환값과 매개변수의 데이터 형식을 지정해 줘야 한다. 그리고 펑터
나 멤버 함수
를 function
에 저장할 때는 대상(객체)의 주소
를 함께 전달해야 한다.
다음은 function
을 사용해 다양한 객체를 함수처럼 호출하는 예이다.
#include <iostream>
#include <functional>
#include <string>
using std::cout;
using std::endl;
void function_pointer(int input) {
cout << "Function pointer: " << input << endl;
}
struct functor {
void operator()(char functor_prefix) {
cout << "Functor: " << functor_prefix << endl;
}
};
class class_object {
public:
class_object(std::string init_string) : class_object_name(init_string) {}
void std_function_call_member(std::string contents) {
cout << "클래스 멤버 함수 객체화 (" << class_object_name << "): " << contents << endl;
}
private:
std::string class_object_name;
};
int main()
{
class_object class_obj("호출 객체를 가지고 있는 클래스");
functor functor_obj;
std::function<void(int)> func_pointer = function_pointer;
std::function<void(functor&, char)> functor_func = &functor::operator();
std::function<void(double)> lambda_func = [](double input) {cout << "lambda function: " << input << endl; };
std::function<void(class_object&, std::string)> member_func = &class_object::std_function_call_member;
func_pointer(10);
functor_func(functor_obj, 'A');
lambda_func(0xa8);
member_func(class_obj, "출력");
return 0;
}
실행 결과
Function pointer: 10
Functor: A
lambda function: 168
클래스 멤버 함수 객체화 (호출 객체를 가지고 있는 클래스): 출력
function
은 클래스 템플릿으므로 컨테이너처럼 함수 형식을 <>
사이에 넣는다. 여기에는 함수
, 펑터
, 멤버 함수
, 람다 함수
등의 데이터 형식을 넣을 수 있다.