Callable

Date:     Updated:

카테고리:

태그:

모두의 코드 씹어먹는 c++ 자료를 보고 정리한 내용입니다.


std::function

c++ 에서 Callable 이란 () 연산자를 붙여서 호출할 수 있는 모든 것을 뜻한다. 예를 들어 함수, () 연산자를 오버로딩한 클래스, 그리고 람다 함수 등이다.

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

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

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

std::function은 일반적인 Callable들을 쉽게 보관할 수 있지만, 멤버 함수의 경우 이야기가 조금 달라진다. 왜냐하면 멤버 함수는 기본적으로 this 포인터를 들고있는데, 멤버 함수를 바로 그냥 function에 넣게 된다면 this가 무엇인지 알 수 없기 때문이다. 따라서 아래와 같은 코드는 컴파일 오류가 발생한다.

파이썬에서 멤버 함수들이 def func(self) 와 같이 정의되는 것과 같다.

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

[!bug] 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;

std::function에 멤버 함수를 담기 위해서는 해당 객체의 주소를 넣어줘야 한다.

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

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

// 멤버 함수가 아닌 경우
std::function<int(const std::string&)> f1 = some_func1;

// 멤버 함수
std::function<int(A&)> f1 = &A::some_func;

참고로 멤버 함수의 경우 이전 함수들과 다르게 함수의 이름 만으로는 그 주소값을 전달할 수 없다. 이는 c++ 언어 규칙 때문인데, 멤버 함수가 아닌 모든 함수들의 경우 함수의 이름이 함수의 주소값으로 암시적 형변환이 일어난다. 하지만 멤버 함수의 경우 암시적 형변환이 발생하지 않으므로 & 연산자를 통해 명시적으로 함수의 주소값을 전달해줘야 한다.

멤버 함수를 함수 객체로 - mem_fn

vector들을 원소로 갖는 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;
  }
}

여기서 std::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;
}

문제는 unary_op에 멤버 함수가 들어왔다는 것이다. 따라서 본 코드는 컴파일 오류가 발생한다. 왜 그럴까?

std::transformUnaryOperation 템플릿 자리에 &vector<int>::size가 들어간다면 해당 함수를 호출하는 부분은 아래와 같이 변환된다.

unary_op(*first1);
&vector<int>::size(*first);

하지만 멤버 함수의 경우

(*first).(*&vector<int>::size)

first->(*&vector<int>::size)

위 두 형태 중 하나로 호출해야 한다.

이런 경우 std::mem_fn 함수를 이용할 수 있다. mem_fn은 이름 그대로, 전달된 멤버 함수를 function 객체로 예쁘게 만들어 리턴해준다.

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

참고로 mem_fn은 람다 함수로도 완벽하게 동일한 작업을 수행할 수 있다. 위 코드의 경우 mem_fn(&vector<int>: :size) 대신 [](const auto& v){ return v.size() }로 대체될 수 있다.

mem_fn을 사용하기 위해서는 <functional> 헤더를 추가해야 하지만 람다 함수는 그냥 쓸 수 있으니 좀 더 편리한 면도 있다. 물론, 코드 길이 면에서는 mem_fn을 이용하는 것이 좀 더 깔끔한 편이다.

std::bind

재미있게도 함수 객체 생성 시에 인자를 특정한 것으로 지정할 수도 있다.

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

bind 함수는 이름 그대로 원래 함수에 특정 인자를 붙여준다.

auto add_with_2 = std::bind(add, 2, std::placeholders::_1);

위 예시의 경우 add 라는 함수에 첫 번째 인자로 2를 bind 시켜주고, 두 번째 인자로는 새롭게 만들어진 함수 객체의 첫 번째 인자를 전달해준다.

auto negate = std::bind(subtract, std::placeholders::_2, std::placeholders::_1);

위 예시의 경우 negate 함수는 첫 번째 인자와 두 번째 인자의 순서를 바꿔서 suubtract 함수를 호출하게 된다.

bind 함수를 사용할 때 한 가지 주의할 점은, 레퍼런스를 인자로 받는 함수들의 경우이다.

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

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

호출 메시지를 확인해보면 bind 함수로 인자가 복사되어 전달된다. 따라서 레퍼런스를 넘겨주고 싶다면 명시적으로 s1의 레퍼런스를 전달해줘야 한다.

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

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



맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기