#20. [모던 C++ STL] function, mem_fn, reference_wrapper, ref, cref, bind, invoke(C++11, C++17, C++20)
- (C++11~) function이 추가되어
()
로 호출 가능한 함수자를 저장할 수 있습니다.- (C++11~) function에서
()
을 호출할 대상이 없을 때 bad_function_call 예외를 방출합니다.- (C++11~) mem_fn()이 추가되었습니다. 인자가 있는 멤버 함수도 호출하는 함수자를 좀 더 간편하게 만들어 줍니다.
- (C++11~) reference_wrapper가 추가되어 복사 생성이나 복사 대입이 안되는 참조자를 래핑할 수 있습니다.
- (C++11~) ref(), cref()가 추가되어 reference_wrapper 개체를 좀더 간편하게 생성할 수 있습니다.
- (C++11~) bind()가 추가되어
placeholders::_1
(GCC의 경우_1
,_2
,_3
, …_29
가 정의됨)와 같은 자리 표시자와 조합하여 특정 인자만을 사용하는 함수자를 생성할 수 있습니다.
- (C++11~) is_bind_expression는 bind()로 생성한 함수인지 검사합니다.
- (C++11~) is_placeholder가 추가되어 자리 표시자를 사용했는지 검사할 수 있습니다.
- (C++11~) 함수자 타입 특성 클래스(unary_function, binary_function등), 바인더(bind1st(), bind2nd()등), 어뎁터와 부정자(mem_fun(), mem_fun_ref(), ptr_fun(), not1(), not2(), unary_negate(), binary_negate(), pointer_to_unary_function(), pointer_to_binary_function()등)가 람다 표현식, function, bind(), mem_fn()등으로 대체되어 deprecate 되었습니다.
- (C++17~) invoke()가 추가되어 일반 함수와 멤버 함수를 동일한 방식으로 호출할 수 있습니다.
- (C++20~) bind_front()가 추가되었습니다. 인자들을 순서대로 배치하므로, 인자의 순서 변경이 없다면 간편하게 사용할 수 있습니다.
개요
기존에는 함수처럼 호출하는 개체를 만들기 위해 함수자를 이용했는데요,
1
2
3
4
5
6
7
8
9
10
11
// 7보다 작은지 검사하는 함수
class Less_7 : std::unary_function<int, bool> {
public:
result_type operator ()(argument_type val) {
return val < 7 ? true : false;
}
};
std::vector<int> v{7, 4, 3};
auto itr = std::find_if(v.begin(), v.end(), Less_7{});
EXPECT_TRUE(*itr == 4);
unary_function 이나 binary_function등의 함수자 타입 특성 클래스, 바인더, 어뎁터와 부정자를 사용하는 방식이 좀 까다로웠습니다.
C++11 부터는 좀 더 간단한 람다 표현식을 사용할 수 있는데요,
1
2
3
4
5
6
std::vector<int> v{7, 4, 3};
std::find_if(
v.begin(),
v.end(),
[](int val) -> int {return val < 7 ? true : false;}
);
이에 따라 C++11 부터 STL 에서는 기존의 까다로웠던 함수자 타입 특성 클래스, 바인더, 어뎁터와 부정자를 deprecate 하고, function 개체를 추가하여 ()
로 호출 가능한 함수자들을 다룰 수 있게 했습니다.
항목 | (~C++11) | (C++11~) | 내용 | |
---|---|---|---|---|
함수자 정의 | 함수자 타입 특성 클래스(unary_function, binary_function) | 람다 표현식등 활용. function 개체로 저장 |
기존은 불필요한 제약이었습니다. | |
바인더 | bind1st(), bind2nd() | bind() | 다수의 인자를 지원합니다. | |
어뎁터와 부정자 | mem_fun(), mem_fun_ref(), ptr_fun(), not1(), not2() | mem_fn() | 인자 있는 멤버 함수를 지원합니다. | |
참조 타입 인자 지원 | X | reference_wrapper ref(), cref() |
기존에 지원하지 못했던 참조 타입 인자를 지원합니다. |
function
function 개체는 다음 개체들을 저장할 수 있습니다.
1
2
3
4
5
6
7
8
9
std::function<int(int)> func{[](int val) -> int {return val < 7 ? true : false;}};
std::vector<int> v{7, 4, 3};
auto itr{std::find_if(
v.begin(),
v.end(),
func)
};
EXPECT_TRUE(*itr == 4);
bad_function_call
function에서 ()
을 호출할 대상이 없을 때 bad_function_call 예외를 방출합니다.
1
2
3
4
5
6
7
std::function<void()> func{nullptr};
try {
func();
}
catch (std::bad_function_call&) {
}
mem_fn()
기존의 어뎁터인 mem_fun() 이나 mem_fun_ref()는 f(x)
의 호출을 x->f()
나 x.f()
로 호출해 줬는데요, 사실 함수의 인수를 전달할 방법이 없어 사용상의 제약이 있었습니다.
C++11 부터는 mem_fn()이 추가되었습니다. 인자가 있는 멤버 함수도 호출하는 함수자를 좀 더 간편하게 만들어 줍니다.
1
2
3
4
5
6
7
8
9
10
class T {
public:
int Add(int a, int b) const {return a + b;}
};
T obj;
auto add{std::mem_fn(&T::Add)};
EXPECT_TRUE(add(obj, 1, 2) == 3); // obj.Add(1, 2);를 호출합니다.
reference_wrapper
reference_wrapper는 복사 생성이나 복사 대입이 안되는 참조자를 래핑하여 복사/대입이 가능하게 하고, 참조성을 유지시켜 줍니다.
get()
으로 관리하는 참조자를 구할 수 있으며, 다른 참조자 생성시 operator T&() const
을 이용한 암시적 형변환을 통해 참조자를 대입합니다.(암시적 형변환이 필요하여 {}
는 안됩니다.)
1
2
3
4
5
6
7
8
9
int a{0};
std::reference_wrapper<int> rw(a);
rw.get() = 10; // reference_wrapper가 관리하는 참조자에 값 대입
EXPECT_TRUE(a == 10);
int& b = rw; // 참조자 생성시 암시적 형변환을 통해 reference_wrapper가 관리하는 참조자 대입
b = 20;
EXPECT_TRUE(a == 20);
특히, 참조자를 컨테이너 요소로 사용할 때 유용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a{1};
int b{2};
int& ref1{a}; // 참조자 타입으로 만드려고 억지로 정의했습니다. ref()나 cref()가 더 낫습니다.
int& ref2{b};
// std::vector<int&> v; // (X) 컴파일 오류.
std::vector<std::reference_wrapper<int>> v;
v.push_back(ref1); // 참조자를 벡터에 추가할 수 있습니다.
v.push_back(ref2);
EXPECT_TRUE(v[0] == 1 && v[1] == 2);
a = 10;
b = 20;
EXPECT_TRUE(v[0] == 10 && v[1] == 20); // 참조자를 저장했으므로 vector의 요소로 확인해도 값이 바껴있습니다.
ref(), cref()
ref(), cref()가 추가되어 reference_wrapper 개체를 좀더 간편하게 생성할 수 있습니다.
항목 | 내용 |
---|---|
ref(T) (C++11~) |
reference_wrapper<T> 생성 |
cref(T) (C++11~) |
reference_wrapper<const T> 생성 |
1
2
3
4
5
6
7
8
9
10
11
int a{1};
int b{2};
std::vector<std::reference_wrapper<int>> v;
v.push_back(std::ref(a)); // std::reference_wrapper를 생성합니다.
v.push_back(std::ref(b));
EXPECT_TRUE(v[0] == 1 && v[1] == 2);
a = 10;
b = 20;
EXPECT_TRUE(v[0] == 10 && v[1] == 20);
bind()
기존에는 이항 함수를 단항 함수로 만들기 위해 bind1st(op, x)
나 bind2nd(op, y)
와 같은 바인더를 이용했는데요,
C++11 부터 STL 에서는 bind()함수를 추가하여, 다수의 인자(GCC의 경우 _1
~_29
범위에서 지원됨)를 가진 함수도 지원합니다.
bind()가 추가되어 placeholders::_1
(GCC의 경우 _1
, _2
, _3
, … _29
가 정의됨)와 같은 자리 표시자와 조합하여 특정 인자만을 사용하는 함수자를 생성할 수 있습니다.
다음 코드에서,
func1()
은Sum()
의 모든 값을 지정했습니다.func2()
는Sum()
의b
인자만 자리 표시자를 사용합니다.func3()
은Sum()
의b
인자,c
인자에 자리 표시자를 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int Sum(int a, int b, int c) {return a + b + c;}
// Sum 함수의 인자를 1, 2, 3으로 고정합니다.
std::function<int()> func1{
std::bind(Sum, 1, 2, 3)
};
EXPECT_TRUE(func1() == 1 + 2 + 3);
// Sum 함수의 2번째 인자(b)를 함수 호출시의 1번째 인자로 사용합니다.
std::function<int(int)> func2{
std::bind(Sum, 1, std::placeholders::_1, 3)
};
EXPECT_TRUE(func2(4) == 1 + 4 + 3);
// Sum 함수의 2번째 인자(b)를 함수 호출시의 1번째 인자로 사용하고,
// 3번째 인자(c)를 함수 호출시의 2번째 인자로 사용합니다.
std::function<int(int, int)> func3{
std::bind(Sum, 1, std::placeholders::_1, std::placeholders::_2)
};
EXPECT_TRUE(func3(4, 5) == 1 + 4 + 5);
auto를 사용하면 좀더 간결하게 표현할 수 있습니다.
1
2
3
4
auto func3{
std::bind(Sum, 1, std::placeholders::_1, std::placeholders::_2)
};
EXPECT_TRUE(func3(4, 5) == 1 + 4 + 5);
사실 bind() 보다는 람다 표현식이 훨씬 더 직관적이니, bind()보다는 람다 표현식을 사용하시는게 좋습니다.(더 직관적인 람다 표현식이 있는데, 도대체 왜 bind()를 표준에 넣었을까 찾아보니, 2005년 표준의 TR1에 이미 bind()가 포함되었다고 하네요. 기존 코드 호환 용도 정도로만 사용하라고 제공하는 듯 합니다.)
1
2
3
4
auto func3{
[](int b, int c) {return Sum(1, b, c);}
};
EXPECT_TRUE(func3(4, 5) == 1 + 4 + 5);
(C++20~) bind_front()가 추가되었습니다. 인자들을 순서대로 배치하므로, 인자의 순서 변경이 없다면 간편하게 사용할 수 있습니다.
bind() 와 인수의 참조성 유지
bind()는 인수를 복사하기 때문에, 만약 참조자를 사용하는 함수라면 오동작하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 인자들의 값을 1씩 증가시킵니다.
void Add(int& a, int& b, int& c) {
++a;
++b;
++c;
}
int a{1};
int b{2};
int c{3};
std::function<void()> add{
std::bind(Add, a, b, c) // a, b, c를 복제합니다.
};
add();
// (X) 오동작. bind()에서 인수를 복제했기 때문에 참조성이 깨졌습니다.
EXPECT_TRUE(a == 1 && b == 2 && c == 3);
이런 경우 복제되더라도 참조성을 잃지 않도록 reference_wrapper를 전달하여야 하며, ref()나 cref()를 이용합니다.
1
2
3
4
5
6
7
8
9
10
int a{1};
int b{2};
int c{3};
std::function<void()> add{
std::bind(Add, std::ref(a), std::ref(b), std::ref(c)) // 참조자 래퍼를 대입합니다.
};
add();
// (O) 참조자 래퍼를 대입하여 값이 수정되었습니다.
EXPECT_TRUE(a == 2 && b == 3 && c == 4);
is_bind_expression
is_bind_expression는 bind()로 생성한 함수인지 검사합니다. 단, function 개체에 저장한 것을 사용하면, bind() 관련 속성을 잃어버리기 때문에 오동작 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
int Sum(int a, int b, int c) {return a + b + c;}
template<typename T>
bool IsBind(const T&) {
return std::is_bind_expression<T>::value;
}
EXPECT_TRUE(IsBind(std::bind(Sum, 1, 2, 3)) == true);
auto func1{std::bind(Sum, 1, 2, 3)};
EXPECT_TRUE(IsBind(func1) == true);
std::function<int()> func2{std::bind(Sum, 1, 2, 3)}; // (X) 오동작. function 개체에 저장하면, bind에 대한 관련 속성을 잃어버립니다.
EXPECT_TRUE(IsBind(func2) == false);
is_placeholder
is_placeholder가 추가되어 자리 표시자를 사용했는지 검사할 수 있습니다. 단, function 개체에 저장한 것을 사용하면, bind() 관련 속성을 잃어버리기 때문에 오동작 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
int Sum(int a, int b, int c) {return a + b + c;}
template<typename T>
bool IsBind(const T&) {
return std::is_bind_expression<T>::value;
}
EXPECT_TRUE(IsPlaceholder(std::bind(Sum, 1, std::placeholders::_1, 3)) == true);
auto func1{std::bind(Sum, 1, std::placeholders::_1, 3)};
EXPECT_TRUE(IsPlaceholder(func1) == true);
std::function<int(int)> func2{std::bind(Sum, 1, std::placeholders::_1, 3)}; // (X) 오동작. function 개체에 저장하면, bind에 대한 관련 속성을 잃어버립니다.
EXPECT_TRUE(IsPlaceholder(func2) == false);
(C++17~) invoke()
C++17 부터는 invoke()가 추가되어 일반 함수와 멤버 함수를 동일한 방식으로 호출할 수 있습니다. 일반 함수인 경우 Func(params...)
를 호출하고, 멤버 함수인 경우 params[1].Func(params[2]...)
을 호출합니다.
1
2
3
4
5
6
7
8
9
10
11
class T {
public:
int Sum(int a, int b, int c) {return a + b + c;} // 멤버 함수
};
int Sum(int a, int b) {return a + b;} // 일반 함수
T t;
EXPECT_TRUE(std::invoke(T::Sum, t, 1, 2, 3) == 1 + 2 + 3); // 멤버 함수를 호출합니다.
EXPECT_TRUE(std::invoke(Sum, 1, 2) == 1 + 2); // 일반 함수를 호출합니다.
(C++20~) bind_front()
C++11 부터 bind()가 추가되어 특정 인자들을 재구성할 수 있었는데요(bind() 참고),
C++20 부터는 좀더 간편한 bind_front()가 추가되었습니다. 인자들을 순서대로 배치하므로, 인자의 순서 변경이 없다면 간편하게 사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
int Sum(int a, int b, int c) {return a + b + c;}
auto func1{
std::bind(Sum, 1, std::placeholders::_1, std::placeholders::_2) // placeholders를 지정해야 합니다.
};
EXPECT_TRUE(func1(2, 3) == 1 + 2 + 3);
auto func2{
std::bind_front(Sum, 1) // 앞쪽 인자부터 순서대로 적용하기 때문에 placeholders를 지정할 필요가 없습니다.
};
EXPECT_TRUE(func2(2, 3) == 1 + 2 + 3);
댓글남기기