KoreanFoodie's Study

C++ 기초 개념 14 : 함수를 객체로 사용하기 (std::function, std::mem_fn, std::bind) 본문

Tutorials/C++ : Beginner

C++ 기초 개념 14 : 함수를 객체로 사용하기 (std::function, std::mem_fn, std::bind)

GoldGiver 2022. 4. 19. 10:23

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

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);
    
    auto a = [&](int a, int b){ std::cout << "a + b = " << a + b << std::endl; };
    a(3, 5);
}

same_obj 는 클래스의 객체이지만, 함수처럼 ( ) 를 호출할 수 있다. 

마찬가지로, a 는 람다 함수이지만, ( ) 를 통해 호출할 수 있기에 Callable 이라고 할 수 있다.

 

 

std::function

C++에서는 이러한 Callable 들을 객체의 형태로 보관할 수 있는 std::function 이라는 클래스를 제공한다. C 에서의 함수 포인터는 진짜 함수들만 보관할 수 있는 객체라고 볼 수 있다면, 이 std::function 의 경우 함수 뿐만이 아니라 모든 Callable 들을 보관할 수 있는 객체이다.

#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();
}

 

문법을 보면 <리턴타입(인자 타입)> 으로 사용하는 것을 알 수 있다. Functor 인 클래스 S 의 객체의 경우 단순히 S 의 객체로 전달해도 이를 마치 함수인 양 받게 된다.

마지막으로 람다 함수의 경우 리턴값이 없고 인자를 받지 않으므로 std::function<void(char)> 로 정의했다.

std::function 은 C++ 의 모든 Callable을 마음대로 보관할 수 있는 유용한 객체이다. 만약에 함수 포인터로 이를 구현하려고 했다면 Functor 와 같은 경우를 성공적으로 보관할 수 없다.

 

 

멤버 함수를 가지는 std::function

function 은 일반적인 Callable 들을 쉽게 보관할 수 있었지만, 멤버 함수들의 경우 상황이 약간 다르다. 왜냐하면 엠버 함수 내에서 this 내에서 this 의 경우 자신을 호출한 객체를 의미하기 때문에, 만일 멤버 함수를 그냥 function 에 넣게 된다면 this 가 무엇인지 알 수 없는 문제가 발생한다.

#include <iostream>
#include <functional>
#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 에 관한 정보를 전달해 주어야 한다.

사실 멤버 함수들은 인자는 자신을 호출한 객체를 암묵적으로 받고 있다. 이는 파이썬에서 멤버 함수들이 def func(self) 이런 식으로 정의되는 것과 마찬가지이다.

 

따라서 이를 받는 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);
}

 

위의 코드처럼, 원래 인자에 추가적으로 객체를 받는 인자를 전달해주면 된다. 이때 상수 함수의 경우 상수 형태로 인자를 받아야 하고, 상수 함수가 아닌 경우 단순히 A& 의 형태로 인자를 받으면 된다.

std::function<int(A&)> f1 = &A::some_func;
std::function<int(const A&)> f2 = &A::some_const_function;

 

참고로 이전의 함수들과는 달리 &A::some_func 와 같이 함수의 이름만으로는 그 주소값을 전달할 수 없다. C++ 언어 규칙에 따르면, 멤버 함수가 아닌 모든 함수들의 경우 함수의 이름이 함수의 주소값으로 암시적 변환이 일어나지만, 멤버 함수들의 경우 암시적 변환이 발생하지 않으므로 & 연산자를 통해 명시적으로 주소값을 전달해줘야 한다.

따라서 아래와 같은 코드는 해당 객체의 멤버 함수를 호출한 것과 같은 효과를 낸다.

f1(a);
f2(a);

 

 

멤버 함수들을 함수 객체로 - mem_fn

vector 의 vector 를 가지는 container 에 대해, 각 벡터의 길이를 저장하는 벡터를 생성한다고 해 보자. 이때 멤버 함수를 함수 객체처럼 사용하면,

#include <iostream>
#include <functional>
#include <algorithm>
#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 함수의 정의는 다음과 같다.

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;
}

 

즉, 위에서 transform 함수를 실행 시키면, &vector<int>::size(*first1) 같은 식으로 unary_op 함수가 대체되어 실행이 된다.

하지만 사실 이 코드는 transform 부분의 마지막 인자에서 에러가 발생한다. 왜냐하면 unary_op 에서 사용하는 함수인 &vector<int>::size 가 vector 클래스의 멤버 함수이기 때문이다. 따라서 std::function으로 변환해서 전달해 주어야 한다.

std::function<size_t(vector<int>)> fs = &vector<int>::size;

 

하지만 매번 함수 객체를 만들어 주는 것은 매우 귀찮은 일이므로, 다음과 같이 mem_fn 을 사용하여 표현을 간략화시킬 수 있다!

  std::transform(container.begin(), container.end(), size_vec.begin(), std::mem_fn(&vector<int>::size));

 

추가로, 람다 함수를 사용하면 더 편리하게 같은 작업을 수행할 수 있다. 따라서 람다를 사용하는 것을 더 익히도록 하자 ㅎㅎ

[](const auto& v) { return v.size(); }

 

 

std::bind

함수 객체 생성 시에 인자를 특정한 것으로 지정할 수도 있다! (자바스크립트의 bind 와 같다)

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>

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 를 계산한다.
}

 

placeholders 의 _1, _2 들은 일일히 정의된 객체들이다. 그 개수를 라이브러리마다 다른데, libstdc++ (g++) _1 부터 _29 까지 정의되어 있다.

 

레퍼런스를 인자로 받는 함수들의 경우에는 주의할 점이 있다.

#include <iostream>
#include <functional>

struct S {

  int data;

  S(int data) : data(data) { std::cout << "일반 생성자 호출!" << std::endl; }

  S(const S& s) : data(s.data) { std::cout << "복사 생성자 호출!" << std::endl; }

  S(S&& s) : data(s.data) { std::cout << "이동 생성자 호출!" << std::endl; };
};

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;
}

 

위 코드를 실행하면 do_something_with_s1 에 들어가는 s1이 복사되어 들어가기 때문에, 다음과 같은 출력이 나오게 된다.

일반 생성자 호출!
일반 생성자 호출!
Before : 1
복사 생성자 호출!
After : 1

 

따라서 이를 해결하기 위해서는 명시적으로 s1 의 레퍼런스를 전달해 주어야 한다.

  auto do_something_with_s1 = std::bind(do_something, std::ref(s1), std::placeholders::_1);

ref 함수는 전달받은 인자를 복사 가능한 레퍼런스로 변환해준다. 참고로 const 레퍼런스의 경우 cref 함수를 호출하면 된다!

 

 

 

Comments