Effective Modern C++ 책에서 람다 파트를 읽던 중 std::function과 std::bind가 언급되어서 공부하고 가면 좋을 것 같아 포스트를 써보려고 한다.
Callable이란, 이름 그대로 호출이 가능한 것을 의미한다. 대표적으로 함수가 있다.
C++에서는 ()을 붙여 호출할 수 있는 모든 것을 Callable이라고 정의한다.
#include "bits/stdc++.h"
using namespace std;
class Game
{
public:
void operator()(int a, int b) { cout << "a = " << a << endl; cout << "b = " << b << endl; }
};
int main()
{
Game game;
game(10, 20);
return 0;
}
다음을 컴파일하고 실행해보면 a = 10, b = 20이 출력된다.
하지만 game은 함수처럼 호출할 수 있지만 함수는 아니고 클래스 Game의 한 객체이다.
auto f = [](int a, int b) { cout << "a = " << a << endl; cout << "b = " << b << endl; };
f(10, 20);
f 역시 일반적인 함수의 꼴을 하고 있지는 않지만, ()를 통해 호출할 수 있기에 Callable이다.
C++에서 이러한 Callable 들을 객체의 형태로 보관할 수 있는 std::function 이라는 클래스를 제공한다.
<functional> 헤더에 정의되어 있으며 다형성 함수 래퍼를 나타내는 템플릿 클래스C++11 표준에 추가되었으며 함수 포인터에 대한 보다 유연하고 안전한 대안을 제공하려는 목적으로 도입되었다.
C++11 이전에는 개발자가 유사한 기능을 달성하기 위해 함수 포인터나 사용자 정의 함수 개체를 사용했지만 C++11 이후 std::function로 동일한 기능을 할 수 있다.
#include "bits/stdc++.h"
using namespace std;
class Game
{
public:
void operator()(int a, int b) { cout << "a = " << a << endl; cout << "b = " << b << endl; }
};
void Add(int a, int b) { cout << "a + b = " << a + b << '\n'; }
int main()
{
Game game;
game(10, 20);
auto f = [](int a, int b) { cout << "a = " << a << endl; cout << "b = " << b << endl; };
f(10, 20);
function<void (int, int)> f1 = Add;
function<void(int, int)> f2 = game; // game을 호출 가능한 객체(펑터)로 처리하고 f2가 초기화됨
function<void()> f3 = []() { cout << "std::function<void()>" << endl; };
f1(10, 20);
f2(20, 40);
f3();
return 0;
}
function 객체는 템플릿 인자로 전달 받을 함수의 타입을 가지게 됨
std::function은 C++의 모든 Callable을 마음대로 보관할 수 있는 유용한 객체이다. 만약 함수 포인터로 이를 구현하려고 했다면 Functor와 같은 경우를 성공적으로 보관하기 힘들었음
Functor : 함수 객체function은 일반적인 Callable들을 쉽게 보관할 수 있었지만, 멤버 함수는 이야기가 달라진다.
왜냐하면, 멤버 함수 내에서 this의 경우 자신을 호출한 객체를 의미하기 때문에, 만일 멤버 함수를 그냥 function에 넣게 된다면 this가 무엇인지 알 수 없는 문제가 생긴다
class Game
{
public:
Game(int InLevel) : level(InLevel) {}
void operator()(int a, int b) { cout << "a = " << a << endl; cout << "b = " << b << endl; }
void Print() { cout << level << endl; }
private:
int level;
};
int main()
{
Game game(5);
function<void()> f4 = game.Print; // 컴파일 에러
return 0;
컴파일 하면 오류 C3867 'Game::Print': 비표준 구문입니다. '&'를 사용하여 멤버 포인터를 만드세요. 라는 에러가 난다.
이는 함수 입장에서 자신을 호출하는 객체가 무엇인지 알 길이 없기 때문에 level이 어떤 객체의 level인지 알 수 없기 때문에 f4에 game에 관한 정보도 추가해야 한다.
멤버 함수들은 구현 상 자신을 호출한 객체를 인자로 암묵적으로 받고 있기 때문에 다음과 같이 바꿔주면 된다.
function<void(Game&)> f4 = &Game::Print;
위와 같이 원래 인자에 추가적으로 객체를 받는 인자를 전달해야 한다. 멤버 함수가 const이면 상수 형태로 인자를 받아야 한다.
이전 함수들과 다르게 함수의 이름만으로는 function에 함수의 이름 만으로는 그 주소값을 전달할 수 없다.
C++ 언어 규칙 때문에, 멤버 함수가 아닌 모든 함수의 경우 함수의 이름이 함수의 주소값으로 암묵적 변환이 일어나지만, 멤버 함수들의 경우 암묵적 변환이 일어나지 않으므로 & 연산자를 통해 명시적으로 주소값을 전달해야 한다
따라서 아래와 같이 호출하고자 하는 객체를 인자로 전달하면 해당 객체의 멤버 함수를 호출한 것과 같은 효과를 낼 수 있다
f4(game);
bind 역시 C++11에서 도입되었으며, <functional> 헤더의 일부이다.
인수를 함수나 Callable에 바인딩하여 Callable을 만드는데 사용된다. 주로 function과 함께 사용된다.
함수 객체를 생성할 때 인자를 특정한 것으로 지정할 수 있
다. bind 함수의 반환은 function이다.
bind 함수를 사용하면 나중에 실제 함수가 호출될 때 제공될 수 있는 인수에 대한 자리 표시자가 있는 함수 객체를 생성할 수 있다.
void Add(int a, int b) { cout << "a + b = " << a + b << '\n'; }
void Minus(int a, int b) { cout << "a - b = " << a - b << '\n'; }
int main()
{
function<void(int)> func_add = bind(Add, placeholders::_1, 10);
auto func_minus = bind(Minus, 15, placeholders::_1);
func_add(15); // 10 + 15
func_minus(20); // 15 - 20
}
func_minus의 15와 placeholders::_1의 순서를 바꾸면 결과 값이 달라진다.
placeholders_숫자 : 내가 넣고 싶은 매개변수의 순서이다. 29까지 정의가능