[C++] 함수
함수는 이전 글(함수 기본)에서도 다루었던 주제입니다.
지난 글에서는 함수 그 자체의 기능 (디폴트 파라미터, 인라인 등...)에 대해 다루었습니다.
이번 글에서는 함수를 객체지향적으로 다루는 것과 관련이 있는 기능에 대해 살펴보도록 하겠습니다.
1. 함수 포인터
포인터는 주소를 저장하는 변수 입니다.
그리고, 이 주소에는 함수 또한 포함됩니다.
함수 또한 변수와 마찬가지로 자기만의 주소를 가지고 있습니다.
포인터를 통해, 함수의 주소를 가리키고 사용하는 것이 가능합니다.
예제를 살펴보겠습니다.
#include <iostream>
using namespace std;
int test_func() {
return 12;
}
int main() {
int (*func)() = test_func;
cout << func();
return 0;
}
12
위와 같이, 함수 또한 포인터로 가리키고 사용할 수 있습니다.
이것을 조금 응용해서, 함수에 매개변수로 함수를 전달하는 것이 가능해집니다.
예제를 살펴보겠습니다.
#include <iostream>
using namespace std;
void test(int* arr, int (*cmp)(int, int)) {
int tmp;
for (int i = 0; i < sizeof(arr); ++i) {
for (int j = i; j < sizeof(arr) + 1; ++j) {
if (cmp(arr[i], arr[j]) < 0) {
tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
}
void print(int* arr) {
for (int i = 0; i < sizeof(arr) + 1; i++) {
cout << arr[i] << " ";
}
cout << "\n";
}
int sort_desc(int a, int b) {
return a - b;
}
int sort_asce(int a, int b) {
return b - a;
}
int main() {
int arr[5] = { 10,50,20,40,30 };
test(arr, sort_desc);
print(arr);
test(arr, sort_asce);
print(arr);
return 0;
}
50 40 30 20 10
10 20 30 40 50
위 예제는 배열을 정렬하는 함수의, 정렬에 사용되는 함수를 매개변수로 받는 함수입니다.
위 예제와 같이 확장성을 고려하여 사용자에게 길을 열어주는 목적으로 함수 포인터를 사용할 수 있습니다.
2. 함수 객체 (Functor)
함수 객체(Functor)란 함수 호출 연산자 ()를 다중 정의한 클래스 입니다.
함수 호출 연산자를 다중 정의함으로써, 객체를 함수 템플릿처럼 이용할 수 있습니다.
함수 템플릿과의 차이점은 객체가 지원하는 형식을 특정할 수 있다는 것 입니다.
예제를 살펴보겠습니다.
#include <iostream>
using namespace std;
class Add {
public:
int operator()(int a, int b) {
cout << "(int) Add : ";
return a + b;
}
double operator()(double a, double b) {
cout << "(double) Add : ";
return a + b;
}
};
int main() {
Add add;
cout << add(5, 10) << "\n";
cout << add(1.1, 2.2) << "\n";
//cout << add(5, 1.1) << "\n";
return 0;
}
(int) Add : 15
(double) Add : 3.3
중간에 주석처리 된 코드와 같이 클래스에서 제공하는 구조 외의 매개변수를 이용한다면 컴파일 에러가 발생합니다.
이런 식으로 설계된 클래스는, 함수의 매개변수가 될 수 있습니다.
#include <iostream>
using namespace std;
class Add {
public:
int operator()(int a, int b) {
cout << "(int) Add : ";
return a + b;
}
double operator()(double a, double b) {
cout << "(double) Add : ";
return a + b;
}
};
void test(Add& add) {
cout << add(5, 6) << "\n";
}
int main() {
Add add;
test(add);
return 0;
}
(int) Add : 11
위와 같은 방식 입니다.
함수를 다른 함수에서 사용하는 것은 함수 포인터를 사용할 수도 있고
여러 형식에 대응하는 함수가 필요한 경우라면 함수 템플릿을 이용할 수도 있지만
특정 형식에만 대응하는 함수로 사용자 코드에 제한을 두어야 한다면, 함수 객체를 이용해야 합니다.
함수를 클래스화 했으므로, 클래스의 특성을 적용시킬 수 있습니다.
예제를 살펴보겠습니다.
#include <iostream>
using namespace std;
class CompareBase {
public:
virtual int operator()(int a, int b) const = 0;
};
class Data {
public:
Data() {
arr[0] = 10;
arr[1] = 50;
arr[2] = 20;
arr[3] = 40;
arr[4] = 30;
}
void print() {
for (auto& i : arr) {
cout << i << " ";
}
cout << "\n";
}
void sort(const CompareBase& cmp) {
int tmp;
for (int i = 0; i < 4; ++i) {
for (int j = i; j < 5; ++j) {
if (cmp(arr[i], arr[j]) < 0) {
tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
}
private:
int arr[5];
};
class CompareDesc : public CompareBase {
public:
int operator()(int a, int b) const { return a - b; }
};
class CompareAsce : public CompareBase {
public:
int operator()(int a, int b) const { return b - a; }
};
int main() {
Data data;
CompareDesc desc;
data.sort(desc);
data.print();
CompareAsce asce;
data.sort(asce);
data.print();
return 0;
}
50 40 30 20 10
10 20 30 40 50
Data클래스의 멤버인 int 배열을 정렬하는 예제입니다.
정렬에는 CompareBase라는 객체를 매개변수로 받습니다
이 객체는 함수 호출 연산자가 순수 가상 함수로 정의되어 있습니다.
mian함수에서는 CompareBase를 상속한 Desc, Asce를 Data에 매개변수로 전달합니다.
위와 같은 방식으로, 같은 함수이지만 매개변수에 따라서 내림차순, 오름차순으로 바꿀 수 있습니다.
구조가 조금 복잡해진다는 단점이 있지만, 잘 설계한다면 유연하고 확장성이 뛰어난 기능입니다.
3. 콜백 함수
콜백(Callback)이란 역호출 이라고도 불립니다.
특정 기능, 문법이라기보다는 설계 구조와 관련된 용어입니다.
사용자와 상호작용 하는 측 (OS, 서버 등)에서 특정 상황에 호출될 함수를 지정할 수 있는데, 이것을 콜백 함수라고 합니다.
호출자가 사용자가 아닌 OS, 서버측에서 호출되는 것이기 때문에 콜백이라는 이름이 붙었습니다.
예제를 살펴보겠습니다.
#include <iostream>
using namespace std;
void server_logic(void (*function)(void)) {
for (int i = 0; i < 10; i++) {
if (i % 3 == 0) {
function();
}
else {
cout << "Logic execute... " << i << "\n";
}
}
}
void log() {
cout << "Something notice\n";
}
int main() {
server_logic(log);
return 0;
}
Something notice
Logic execute... 1
Logic execute... 2
Something notice
Logic execute... 4
Logic execute... 5
Something notice
Logic execute... 7
Logic execute... 8
Something notice
위와 같이, 일정한 로직을 수행하는 함수가 존재한다고 가정했을 때
특정한 상황 (값 변경, 시간 등...)에 대응하여 수행할 함수를 등록해주었습니다.
위와 같은 구조를 콜백 구조라고 합니다.
콜백이 사용되는 대표적인 예시가 라이브러리 입니다.
같은 라이브러리를 사용한다고 해도, 입력이 사용자마다 다르고 그에 따른 경우가 많습니다.
이 경우를 개발자가 전부 대응할 수는 없고, 대응한다고 해도 퍼포먼스의 손실이 커질 것입니다.
그러한 상황에서 개발자가 콜백을 사용하면 사용자 측에서 알맞는 대응을 할 수 있습니다.
서버-클라이언트 구조에서, 특정 라이브러리를 사용할때 등
함수는 여러가지 방향으로 사용될 수 있으며, 각각의 특징들이 존재합니다.
이번 글에서는 사용자가 1차적으로 호출하는것이 아닌, 특정 함수, 클래스에 넘긴 후
넘겨받은 곳에서 사용할 수 있도록 하는 방법들을 살펴보았습니다.
그런데, 이렇게 함수를 넘기는 방법 중 조금 더 간편하고, 특이한 문법이 존재합니다.
다음 글에서는 람다 표현식으로 찾아뵙겠습니다.
감사합니다.