모두의 코드
씹어먹는 C ++ - <14. 함수를 객체로! (C++ std::function, std::mem_fn, std::bind)>
이번 강좌에서는
Callable 의 정의
std::function
std::mem_fn
std::bind
에 대해 다룹니다.
안녕하세요 여러분! 지난 강좌에서 배우신 스마트 포인터 삼형제 (unique_ptr
, shared_ptr
, weak_ptr
) 들은 어떠셨나요? 스마트 포인터를 도입함으로써 C++ 에서 메모리 제어를 훨씬 더 쉽게 수행할 수 있습니다.
이번 강좌에서는 C++ 에서 호출 가능한 모든 것을 포괄해서 나타내는 Callable 에 대해서 알아보도록 하겠습니다. 이번 강좌는 다음 강좌에서 쓰레드를 배우기 전에 짧게 거쳐가는 징검다리라고 보시면 됩니다.
Callable
Callable 이란, 이름 그대로 나타내듯이 호출(Call) 할 수 있는 모든 것을 의미합니다. 대표적인 예시로 함수를 들 수 있겠지요.
하지만 C++ 에서는 ()
를 붙여서 호출할 수 있는 모든 것을 Callable 이라고 정의합니다. 예를 들어서
#include <iostream> struct S { void operator()(int a, int b) { std::cout << "a + b = " << a + b << std::endl; } }; int main() { S some_obj; some_obj(3, 5); }
성공적으로 컴파일 하였다면
실행 결과
a + b = 8
와 같이 나옵니다. 그렇다면 여기서 same_obj
는 함수 일까요? 아닙니다. 하지만 same_obj
클래스 S 의 객체이죠. 하지만, same_obj
는 마치 함수 처럼 ()
를 이용해서 호출할 수 있습니다. (실제로는 same_obj.operator()(3, 5)
를 한 것이죠.)
또 다른 예시로 람다 함수를 생각해봅시다.
#include <iostream> int main() { auto f = [](int a, int b) { std::cout << "a + b = " << a + b << std::endl; }; f(3, 5); }
성공적으로 컴파일 하였다면
실행 결과
a + b = 8
와 같이 나옵니다. f 역시 일반적인 함수의 꼴을 하고 있지는 않지만, ()
를 통해서 호출할 수 있기에 Callable
이라 할 수 있습니다.
std::function
C++ 에서는 이러한 Callable
들을 객체의 형태로 보관할 수 있는 std::function
이라는 클래스를 제공합니다. C 에서의 함수 포인터는 진짜 함수들만 보관할 수 있는 객체라고 볼 수 있다면 이 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; 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<int(const string&)> f1 = some_func1; std::function<void(char)> f2 = S(); std::function<void()> f3 = []() { std::cout << "Func3 호출! " << std::endl; };
일단 위와 같이 function
객체를 정의하는 부분부터 살펴봅시다. function
객체는 템플릿 인자로 전달 받을 함수의 타입을 갖게 됩니다. 여기서 함수의 타입이라 하면, 리턴값과 함수의 인자들을 말합니다.
따라서 예를 들어 some_func1
의 경우 int
를 리턴하며, 인자로 const string&
을 받기 때문에 위와 같이 std::function<int(const string&)>
의 형태로 정의 됩니다.
한편 Functor
인 클래스 S
의 객체의 경우 단순히 S
의 객체를 전달해도 이를 마치 함수 인양 받게 됩니다. S
의 경우 operator()
가 인자로 char
을 받고 리턴타입이 void
이므로 std::function<void<char)>
의 꼴로 표현할 수 있게 됩니다.
마지막으로 람다 함수의 경우 마찬가지로 리턴값이 없고 인자를 받지 않기 때문에 std::function<void()>
로 정의되겠지요.
이렇든 std::function
은 C++ 의 모든 Callable
을 마음대로 보관할 수 있는 유용한 객체 입니다. 만약에 함수 포인터로 이를 구현하려고 했다면 Functor
와 같은 경우를 성공적으로 보관할 수 없었겠지요.
멤버 함수를 가지는 std::function
앞서 function
은 일반적인 Callable
들을 쉽게 보관할 수 있었지만, 멤버 함수들의 경우 이야기가 조금 달라집니다. 왜냐하면, 멤버 함수 내에서 this
의 경우 자신을 호출한 객체를 의미하기 때문에, 만일 멤버 함수를 그냥 function
에 넣게 된다면 this
가 무엇인지 알 수 없는 문제가 발생하게 됩니다.
아래의 예시를 보실까요.
#include <functional> #include <iostream> #include <string> class A { int c; public: A(int c) : c(c) {} int some_func() { std::cout << "내부 데이터 : " << c << std::endl; } }; int main() { A a(5); std::function<int()> f1 = a.some_func; }
컴파일 한다면 아래와 같은 컴파일 오류가 나게 됩니다.
컴파일 오류
test2.cc: In function 'int main()': test2.cc:17:26: error: invalid use of non-static member function 'int A::some_func()' std::function<int()> f1 = a.some_func; ~~^~~~~~~~~ test2.cc:10:9: note: declared here int some_func() { ^~~~~~~~~
왜냐하면 f1
을 호출하였을 때, 함수의 입장에서 자신을 호출하는 객체가 무엇인지 알 길이 없기 때문에 c
를 참조 하였을 때 어떤 객체의 c
인지를 알 수 없겠지요. 따라서 이 경우 f1
에 a
에 관한 정보도 추가로 전달해야 합니다.
그렇다면 이를 어떻게 할까요? 사실 멤버 함수들은 구현 상 자신을 호출한 객체를 인자로 암묵적으로 받고 있었습니다.
따라서 이를 받는 function
은 아래와 같은 형태로 나타나야 합니다.
#include <functional> #include <iostream> #include <string> class A { int c; public: A(int c) : c(c) {} int some_func() { std::cout << "비상수 함수: " << ++c << std::endl; return c; } int some_const_function() const { std::cout << "상수 함수: " << c << std::endl; return c; } static void st() {} }; int main() { A a(5); std::function<int(A&)> f1 = &A::some_func; std::function<int(const A&)> f2 = &A::some_const_function; f1(a); f2(a); }
성공적으로 컴파일 하였다면
실행 결과
비상수 함수: 6 상수 함수: 6
와 같이 나옵니다.
std::function<int(A&)> f1 = &A::some_func; std::function<int(const A&)> f2 = &A::some_const_function;
위와 같이 원래 인자에 추가적으로 객체를 받는 인자를 전달해주면 됩니다. 이 때 상수 함수의 경우 당연히 상수 형태로 인자를 받아야 하고 (const A&
), 반면에 상수 함수가 아닌 경우 단순히 A&
의 형태로 인자를 받으면 되겠습니다.
참고로 이전의 함수들과는 다르게 &A::some_func
와 같이 함수의 이름 만으로는 그 주소값을 전달할 수 없습니다. 이는 C++ 언어 규칙에 때문에 그런데, 멤버 함수가 아닌 모든 함수들의 경우 함수의 이름이 함수의 주소값으로 암시적 변환이 일어나지만, 멤버 함수들의 경우 암시적 변환이 발생하지 않으므로 &
연산자를 통해 명시적으로 주소값을 전달해줘야 합니다.
따라서 아래와 같이 호출하고자 하는 객체를 인자로 전달해주면 마치 해당 객체의 멤버 함수를 호출한 것과 같은 효과를 낼 수 있습니다.
f1(a); f2(a);
위와 같이 말이지요.
멤버 함수들을 함수 객체로 - mem_fn
예를 들어서 vector
들을 가지는 vector
가 있을 때, 각각의 vector
들의 크기들을 벡터로 만들어주는 코드를 생각해봅시다.
#include <algorithm> #include <functional> #include <iostream> #include <vector> using std::vector; int main() { vector<int> a(1); vector<int> b(2); vector<int> c(3); vector<int> d(4); vector<vector<int>> container; container.push_back(b); container.push_back(d); container.push_back(a); container.push_back(c); vector<int> size_vec(4); std::transform(container.begin(), container.end(), size_vec.begin(), &vector<int>::size); for (auto itr = size_vec.begin(); itr != size_vec.end(); ++itr) { std::cout << "벡터 크기 :: " << *itr << std::endl; } }
transform 함수는 <algorithm> 라이브러리에 있는 함수인데, 각 원소들에 대해 인자로 전달된 함수를 실행시킨 다음 그 결과를 전달된 컨테이너에 넣어줍니다. 함수 정의를 살짝 살펴보면 아래와 같습니다.
template <class InputIt, class OutputIt, class UnaryOperation> OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first, UnaryOperation unary_op) { while (first1 != last1) { *d_first++ = unary_op(*first1); first1++; } return d_first; }
여기서 문제는 해당 함수를 아래와 같이 호출한다는 점입니다.
*d_first++ = unary_op(*first1);
unary_op
가 멤버 함수가 아닐 경우 위와 같이 호출해도 괜찮습니다. 하지만 문제는 unary_op
가 멤버함수 일 경우 입니다.
사실 위 코드를 컴파일 하면 아래와 같은 컴파일 오류가 나게 됩니다.
컴파일 오류
In file included from /usr/include/c++/7/algorithm:62:0, from test2.cc:5: /usr/include/c++/7/bits/stl_algo.h: In instantiation of '_OIter std::transform(_IIter, _IIter, _OIter, _UnaryOperation) [with _IIter = __gnu_cxx::__normal_iterator<std::vector<int>*, std::vector<std::vector<int> > >; _OIter = __gnu_cxx::__normal_iterator<int*, std::vector<int> >; _UnaryOperation = long unsigned int (std::vector<int>::*)() const noexcept]': test2.cc:21:85: required from here /usr/include/c++/7/bits/stl_algo.h:4306:24: error: must use '.*' or '->*' to call pointer-to-member function in '__unary_op (...)', e.g. '(... ->* __unary_op) (...)' *__result = __unary_op(*__first); ~~~~~~~~~~^~~~~~~~~~
왜 그럴까요? 이 역시 전달된 size
함수가 멤버 함수여서 발생하는 문제 입니다. 위 템플릿에 &vector<int>::size
가 들어간다면 해당 unary_op
를 호출하는 부분은 아래와 같이 변환됩니다.
unary_op(*first1);
가
&vector<int>::size(*first);
꼴로 되는데, 멤버 함수의 경우
(*first).(*&vector<int>::size)
혹은
first->(*&vector<int>::size)
와 같이 호출해야 하기 때문 입니다. (이는 C++ 의 규칙 이라 생각하시면 됩니다. 위 컴파일러 오류 메세지를 읽어보세요!) 따라서 이를 위해서는 제대로 std::function
으로 변환해서 전달해줘야 합니다.
#include <algorithm> #include <functional> #include <iostream> #include <vector> using std::vector; int main() { vector<int> a(1); vector<int> b(2); vector<int> c(3); vector<int> d(4); vector<vector<int>> container; container.push_back(a); container.push_back(b); container.push_back(c); container.push_back(d); std::function<size_t(const vector<int>&)> sz_func = &vector<int>::size; vector<int> size_vec(4); std::transform(container.begin(), container.end(), size_vec.begin(), sz_func); for (auto itr = size_vec.begin(); itr != size_vec.end(); ++itr) { std::cout << "벡터 크기 :: " << *itr << std::endl; } }
성공적으로 컴파일 했다면
실행 결과
벡터 크기 :: 1 벡터 크기 :: 2 벡터 크기 :: 3 벡터 크기 :: 4
와 같이 잘 나옴을 알 수 있습니다.
하지만 매번 위 처럼 function
객체를 따로 만들어서 전달하는 것은 매우 귀찮습니다. 따라서 C++ 개발자들은 라이브러리에 위 function
객체를 리턴해버리는 함수를 추가하였습니다.
#include <algorithm> #include <functional> #include <iostream> #include <vector> using std::vector; int main() { vector<int> a(1); vector<int> b(2); vector<int> c(3); vector<int> d(4); vector<vector<int>> container; container.push_back(a); container.push_back(b); container.push_back(c); container.push_back(d); vector<int> size_vec(4); transform(container.begin(), container.end(), size_vec.begin(), std::mem_fn(&vector<int>::size)); for (auto itr = size_vec.begin(); itr != size_vec.end(); ++itr) { std::cout << "벡터 크기 :: " << *itr << std::endl; } }
성공적으로 컴파일 하였다면
실행 결과
벡터 크기 :: 1 벡터 크기 :: 2 벡터 크기 :: 3 벡터 크기 :: 4
와 같이 잘 나옵니다. mem_fn
함수는 이름 그대로, 전달된 멤버 함수를 function
객체로 예쁘게 만들어서 리턴해줍니다.
주의 사항
참고로 mem_fn
은 그리 자주 쓰이지는 않는데, 람다 함수로도 동일한 작업을 수행할 수 있기 때문입니다. 위 코드의 경우 mem_fn(&vector<int>::size)
대신에 [](const auto& v){ return v.size()}
를 전달해도 동일한 작업을 수행합니다.
mem_fn
을 사용하기 위해서는 <functional>
헤더를 추가해야 하지만 람다함수는 그냥 쓸 수 있으니 좀 더 편리한 면이 있습니다. 물론, 코드 길이 면에서는 mem_fn
을 사용하는 것이 좀더 깔끔한 편입니다.
std::bind
재미있게도 함수 객체 생성 시에 인자를 특정한 것으로 지정할 수 도 있습니다. 아래 예제를 보실까요.
#include <functional> #include <iostream> void add(int x, int y) { std::cout << x << " + " << y << " = " << x + y << std::endl; } void subtract(int x, int y) { std::cout << x << " - " << y << " = " << x - y << std::endl; } int main() { auto add_with_2 = std::bind(add, 2, std::placeholders::_1); add_with_2(3); // 두 번째 인자는 무시된다. add_with_2(3, 4); auto subtract_from_2 = std::bind(subtract, std::placeholders::_1, 2); auto negate = std::bind(subtract, std::placeholders::_2, std::placeholders::_1); subtract_from_2(3); // 3 - 2 를 계산한다. negate(4, 2); // 2 - 4 를 계산한다 }
성공적으로 컴파일 하였다면
실행 결과
2 + 3 = 5 2 + 3 = 5 3 - 2 = 1 2 - 4 = -2
와 같이 나옵니다.
bind
함수는 이름 그대로 원래 함수에 특정 인자를 붙여(bind) 줍니다. 예를 들어서
std::bind(add, 2, std::placeholders::_1);
위 예시의 경우 add 라는 함수에 첫 번째 인자로 2 를 bind
시켜주고, 두 번째 인자로는 새롭게 만들어진 함수 객체의 첫 번째 인자를 전달해줍니다. 따라서;
add_with_2(3);
를 하였을 때, 원래 add 함수의 첫 번째 인자로는 2 가 들어가게 되고, 두 번째 인자로는 add_with_2
의 첫 번째 인자인 3 이 들어가겠지요. 만약에
add_with_2(3, 4);
처럼 인자를 여러개 전달하더라도 뒤에 것들은 무시 됩니다.
auto negate = std::bind(subtract, std::placeholders::_2, std::placeholders::_1);
위 경우는 어떨까요? negate
함수는 첫 번째 인자와 두 번째 인자의 순서를 바꿔서 subtract
함수를 호출하게 됩니다. 즉 negate(3, 5)
를 호출할 경우 실제로는 subtract(5, 3)
이 호출되겠지요.
placeholders
의 _1, _2
들은 일일히 정의된 객체들 입니다. 그 개수는 라이브러리 마다 다른데, libstdc++
의 경우 (g++ 에서 사용하는 C++ 라이브러리 입니다.) _1
부터 _29
까지 정의되어 있습니다.
한 가지 주의할 점은, 레퍼런스를 인자로 받는 함수들의 경우 입니다.
#include <functional> #include <iostream> struct S { int data; S(int data) : data(data) { std::cout << "일반 생성자 호출!" << std::endl; } S(const S& s) { std::cout << "복사 생성자 호출!" << std::endl; data = s.data; } S(S&& s) { std::cout << "이동 생성자 호출!" << std::endl; data = s.data; } }; void do_something(S& s1, const S& s2) { s1.data = s2.data + 3; } int main() { S s1(1), s2(2); std::cout << "Before : " << s1.data << std::endl; // s1 이 그대로 전달된 것이 아니라 s1 의 복사본이 전달됨! auto do_something_with_s1 = std::bind(do_something, s1, std::placeholders::_1); do_something_with_s1(s2); std::cout << "After :: " << s1.data << std::endl; }
성공적으로 컴파일 하였다면
실행 결과
일반 생성자 호출! 일반 생성자 호출! Before : 1 복사 생성자 호출! After :: 1
와 같이 나옵니다.
보시다시피 do_something
함수의 경우 첫 번째 인자의 data
를 두 번째 인자의 data + 3
으로 만들어주지만, 실제로 do_something_with_s1
함수를 실행하였을 때 첫 번째 인자로 s1
을 전달했음에도 불구하고 s1
의 data
가 바뀌지 않음을 알 수 있습니다.
그 이유는 위 생성자 호출 메세지에서 확인할 수 있듯이 bind
함수로 인자가 복사 되서 전달되기 때문입니다. 따라서 이를 해결 하기 위해서는 명시적으로 s1
의 레퍼런스를 전달해줘야 합니다.
#include <functional> #include <iostream> struct S { int data; S(int data) : data(data) { std::cout << "일반 생성자 호출!" << std::endl; } S(const S& s) { std::cout << "복사 생성자 호출!" << std::endl; data = s.data; } S(S&& s) { std::cout << "이동 생성자 호출!" << std::endl; data = s.data; } }; void do_something(S& s1, const S& s2) { s1.data = s2.data + 3; } int main() { S s1(1), s2(2); std::cout << "Before : " << s1.data << std::endl; // s1 이 그대로 전달된 것이 아니라 s1 의 복사본이 전달됨! auto do_something_with_s1 = std::bind(do_something, std::ref(s1), std::placeholders::_1); do_something_with_s1(s2); std::cout << "After :: " << s1.data << std::endl; }
성공적으로 컴파일 하였다면
실행 결과
일반 생성자 호출! 일반 생성자 호출! Before : 1 After :: 5
와 같이 실제로 s1
의 값이 잘 바뀌었음을 알 수 있습니다. ref
함수는 전달받은 인자를 복사 가능한 레퍼런스 로 변환해줍니다. 따라서 bind
함수 안으로 s1
의 레퍼런스가 잘 전달 될 수 있게 됩니다.
참고로 const
레퍼런스의 경우 cref
함수를 호출하면 됩니다.
그럼 이것으로 이번 강좌를 마치도록 하겠습니다. function, mem_fn, bind
들을 적재 적소에 잘 쓴다면, C++ 의 강력한 라이브러리를 좀 더 풍요롭게 사용할 수 있을 것입니다.
댓글을 불러오는 중입니다..