C++ Chapter 19.1 : 람다 함수, std::function, bind, placeholders
카테고리: Cpp
태그: Cpp Programming
인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!
chapter 19. 모던 C++ 필수 요소들
🔔 람다 함수
람다 함수
👉 이름이 없어서 익명 함수라고도 불린다.
- 함수 포인터처럼 많이 사용한다.
람다 함수의 정의와 실행
- 람다 함수는 익명 함수이기 때문에 정의만 하면 그 자체로 함수 주소를 리턴하고 어떤 임의의 함수 포인터에 람다 함수를 담아 두어야 한다.
- 뒤에
()
부분이 없다. func
는 함수 포인터가 되며 정의된 이 람다 함수를func
이름으로 실행시킬 수가 있게 된다.- “Hello World!”를 출력하는 기능을 하는 함수를
func
에 담은 것. 실행은func()
로 해야 “Hello World!”이 출력된다.auto func = []() -> void {cout << "Hello, World!\n"; }; // 람다 함수
- 뒤에
- 정의와 동시에 뒤에
()
을 붙여 실행까지 하면 이제 그 함수의 실행 내용 결과 값을 리턴하게 된다. (void 라면 리턴 되는 것 없음)- 익명 함수이므로 정의와 실행을 동시에 하면 1 회 실행 하고 사라진다.
- 정의와 동시에 “Hello World!”이 출력된다.
[]() -> void { cout << "Hello, World!\n"; }();
람다 함수의 형태
[]
대괄호
- 람다 함수 바깥에서 이미 정의되어 있는 어떤 변수나 상수 중에서 람다 함수 내부에서 사용 할 것을 적어주는 부분.
- 인수를 받는 것과는 다르다.
- 사용하지 않더라도 꼭 명시해주어야 한다. 람다 함수의 상징과도 같은 존재! 생략이 불가능 하다.
- 형태
[&]
&
만 써주면 외부에 정의 되어 있는 모든 변수들을 레퍼런스 형태로 람다 함수 내부에 가져 오겠다는 의미[&a]
외부에 정의 되어 있는a
라는 변수를 레퍼런스 형태로 람다 함수 내부에 가져 오겠다는 의미
[=]
=
만 써주면 외부에 정의 되어 있는 모든 변수들을 복사한 값으로 람다 함수 내부에 가져 오겠다는 의미[=a]
외부에 정의 되어 있는a
라는 변수를 복사한 값으로 람다 함수 내부에 가져 오겠다는 의미
[this]
- 클래스 내에서 람다 함수가 사용될 때 멤버 변수들을 람다 함수 내부에서 사용하고 싶을 때는
[this]
이런식으로 가져오기도 한다.
- 클래스 내에서 람다 함수가 사용될 때 멤버 변수들을 람다 함수 내부에서 사용하고 싶을 때는
int value(5);
[&value]() -> int { return value += 10; }(); // 람다 함수
람다 함수 바깥에서 정의된 변수인 value
는 5 라는 값을 가지고 있다. 이 value
변수를 레퍼런스 형태로 람다 함수 내부에서 사용한다. 따라서 이 람다 함수의 실행이 끝나면 value
의 값은 15가 되어 있다.
()
소괄호
- 매개 변수를 적는 부분
- 람다 함수 내부에서 정의되며 람다 함수 내부에서만 사용 가능
[]
와는 다르게 매개 변수가 없는 함수라면()
을 생략할 수 있다.- 매개 변수가 참조형이나 포인터가 아니라면 인수를 복사해서 Call by Value로 사용하게 된다.
int value(5);
auto func = [](int &input) -> int {return input += 10; };
func(value);
람다 함수 내부에서만 수명을 가지는 매개 변수인 input
은 레퍼런스 형태이기 때문에 인수를 참조한다. 이 람다 함수는 func
에 정의가 되어 있기 때문에 func(value)
로 실행해주면 매개 변수인 input
이 value
를 참조하게 되어 value
값이 15가 될 것이다.
()
에 매개 변수를 넣어 포인터나 레퍼런스 타입으로 바깥 변수를 참조하면 되는데 왜 굳이 [] 로 바깥 변수를 받는 것일까?
sort
, for_each
, fill
같은 STL 함수를 사용할 때는 함수 자체를 인수로 받게 되는데, 경우에 따라 원하는 변수를 함수 내부로 전달하고 싶어도 매개 변수로는 전달하지 못하는 경우가 생길 수 있다. 이때 인수로 넘기는 것 없이 람다 함수의 []
을 사용하여 바깥 변수를 전달할 수 있다.
->
- 함수의 리턴 타입을 정의한다.
-> void
,-> int
처럼 람다 함수의 리턴 타입을 같이 명시해주면 된다.void
의 경우->
을 생략할 수 있다.
{ }
- 함수 바디
;
로 처리들 구분
()
- 람다 함수의 맨 마지막에 붙으며, 함수를 실행한다.
- 이게 없으면 람다 함수를 정의만 하는 것으로 끝나는 것이고
- 다른 함수 포인터에 정의해두어 그 함수 포인터로 이 람다 함수를 사용해야 함
- 이게 있다면 람다 함수를 정의와 동시에 실행 하는 것이다.
- 익명 함수로서 실행 됐으므로 1 회 실행 하고 사라짐
- 이게 없으면 람다 함수를 정의만 하는 것으로 끝나는 것이고
예제 코드
auto func = []() -> void {cout << "Hello, World!\n"; };
func(); // "Hello, World! 출력
- 람다 함수를
func
함수 포인터에 정의 하였고 func
이름으로func()
함수를 실행함
int i = 5;
[&]{cout << ++i << '\n'; }(); // "6" 출력
[&]
바깥에 정의된 모든 변수를 레퍼런스로 참조하므로i
는 실행 후 6이 된다.- 정의와 동시에 실행을 하여 익명으로서 실행이 된 람다 함수이다.
auto func2 = [](int val) {cout << val << " "; }; // -> void 생략
std::for_each(vec.begin(), vec.end(), func2);
- 람다 함수를
func2
함수 포인터에 정의하였고 이를for_each
함수의 인수로 넘김
std::for_each(vec.begin(), vec.end(), [](int val) {cout << val << " "; }); // 위와 같은 형태
- 이렇게 그냥 인수에 람다 함수 정의를 넘길 수도 있다!
cout << []() -> int {return 5; }() << '\n'; // "5" 출력
🔔 람다 함수가 자주 사용되는 STL 함수들
std::for_each
#include <algorithm>
for_each(a, b, func)
👉 반복자 [a, b) 범위의 원소들에func
함수를 실행시킨다.
- 원소들이
func
의 인수로 들어가거나 람다 함수 정의 때 쓰인[]
로 참조되어 쓰인다.
vector<int> vec; // 벡터 vec에 10, 20이 원소로 들어있는 상황
vec.push_back(10);
vec.push_back(20);
auto func2 = [](int val) {cout << val << " "; }; // [](int val) -> void {cout << val << " "; }
std::for_each(vec.begin(), vec.end(), func2); // 10, 20 출력
vec
의 모든 원소들이 인수로 넘거ㅏ val
에 복사되고 func2
함수가 실행된다.
std::for_each(vec.begin(), vec.end(), [](int val) {cout << val << " "; }); // 10, 20 출력
수행할 기능을 함수로 따로 정의하고 그것을 함수 포인터로 만들고 하는 방식은 너무 번거롭다. 위와 같이 아예 익명으로 함수를 정의해서 넘기는 것이 더 편리하고 선호 된다.
std::function
#include <functional>
함수 포인터를 체계화시키는 기능을 한다.
- 함수 포인터를 변수처럼 주고 받고 할 수도 있다.
auto func2 = [](int val) {cout << val << " "; }; // func2에 정의된 람다 함수
std::function<void(int)> func3 = func2; // function pointer 하나 만들어서 대입함.
func3(123);
std::function<void(int)> func3 = func2;
std::function<void(int)>
타입의 func3 함수 포인터에 func2 포인터 (=함수 이름)을 대입.- void(int) 즉 아무것도 리턴하지 않고 int 1개 인수를 받는 형태.
func3(123);
std::bind
#include <functional>
특정 인수에 대해서만 함수를 실행시키고 싶을 때, 특정 인수와 특정 함수를 묶어 준다.
- 따라서
std::bind
을 사용하면 파라미터의 자료형으로부터 자유로워 진다. - std::funcion 타입을 리턴한다
auto func = [](int val) {cout << val << " "; };
std::function<void()> func2 = std::bind(func, 456); // 이렇게 하면 parameter 자료형도 쓸 필요 없이 bind 쓰면 된다.
func2(); // 456 출력
func2
는<void()>
타입의 std::function 포인터이다.- 즉, 리턴 하는게 없으며 인수를 받지 않는 함수만 받을 수 있는 포인터
std::bind(func, 456)
func
는 매개 변수(int val
)가 1 개 있으며 리턴은 하지 않는 람다 함수가 정의 되어 있는 포인터다.- ✨
456
이라는 특정 인수가 들어왔을 때의func
함수 처리를 묶어 이를func2
에 정의한다.
func2()
- 매개 변수가 없는 함수임에도 불구하고
func(456)
과 똑같은 내용을 실행하게 된다.- cout « 456 « ” “;
- 매개 변수가 없는 함수임에도 불구하고
auto func = [](int val) {cout << val << " "; };
std::function<void()> func3, func4;
func3 = std::bind(func, 'a');
func3(); // 97 출력 (a의 아스키코드 int)
func4 = std::bind(func, 3.141592);
func4(); // 3 출력 (리턴 타입이 int니까)
func3()
👉func(a)
와 같다.func4()
👉func(3.141592)
와 같다.
멤버 함수를 bind 할 때 주의사항
#include <functional>
#include <string>
#include <iostream>
using namespace std::placeholders;
using namespace std;
class Object
{
public:
void hello(const string& s) { cout << s << '\n'; } // hello는 멤버 함수
};
void goodbye(const string& str) { cout << str << '\n'; } // goodbye는 일반 함수
int main()
{
// 멤버 함수 포인터 실행
Object obj;
auto f1 = std::bind(&Object::hello, &obj, "Hi"); // 멤버함수 포인터가 실행이 되기 위해서는 이 멤버 함수를 실행시킬 객체의 포인터를 반드시 bind 함수에게 같이 알려주어야 함
f1();
// 일반 함수 포인터 실행
auto f2 = std::bind(goodbye, "Good bye");
f2();
}
💎출력💎
Hi
Good bye
멤버 함수를
bind
할 때는 해당 멤버 함수를 실행시킬 객체의 포인터를 반드시 bind 함수에 함께 넘겨 주어야 한다 이때 바인딩할 함수의 인수로 넘기는게 아님!!
f1
- 묶으려는 대상인 멤버 함수 포인터 👉
&Object::hello
- 멤버 함수의 포인터는 그냥 딸랑 함수 이름을 쓰는 일반 함수 포인터와 다르게 &클래스이름::멤버함수이름 형태이다.
&
도 꼭 붙여주어야 한다.- 멤버 변수와 다르게 멤버 함수는 객체로 메모리에 접근하는게 아니기 때문에 함수 포인터에 클래스 이름을 쓴다.
- 더 자세한건 이 포스트를 참고
- 멤버 함수의 포인터는 그냥 딸랑 함수 이름을 쓰는 일반 함수 포인터와 다르게 &클래스이름::멤버함수이름 형태이다.
- ⭐이 멤버 함수 Hello를 실행시킬 객체의 포인터인
&obj
를 반드시 같이 넘겨주어야 한다.⭐ - “Hi”는 멤버 함수 Hello의 고정 인수가 된다.
- 실행
f1()
실행은 곧obj.hello("Hi")
와 같다.
- 묶으려는 대상인 멤버 함수 포인터 👉
f2
- 일반 함수를 바인딩할 때는 멤버 함수처럼 객체의 포인터를 넘길 필요가 없다. 애초에 관계가 없으니.. 😮
f2()
실행은 곧goodbye("Good bye")
와 같다.
std::placeholders
#include <functional>
해당 자리를 지키고 있는 고정 argument
std::bind
로 묶을 함수의 파라미터를 여러개 받는데 고정 인수는 그보다 적게 두려고 할 때 사용되며 자료형으로부터 자유롭게 나머지 인수 자리들을 잡아두는 역할을 한다. 말그대로 place를 holding !
- placeholders::
_1
, placeholders::_2
, placeholders::_3
… 등등이 있다.- 숫자는 묶인 함수에서의 고정된 인수를 제외한 인자 번호를 나타낸다.
- 컴파일 오류가 나지 않게끔 그 자리를 임시로 지키고 있을 뿐, 나중에 함수가 적절한 인자와 함께 호출될 때 이 값은 오버랩되서 사라진다
std::bind
와 함께 쓰인다.bind
는 첫번째 인수로 묶을 대상이 되는 함수의 포인터를 받고- 두번째 인수로 묶을 대상이 되는 고정 인수를 받는다. 이렇게 첫번째 인수, 두번째 인수는 필수 인수다.
- 세번째 인수부터는
placeholders
들을 두어서 고정 인수가 아닌 어떤 자료형이 특정적으로 아직 정해지지 않은, 인수 자리만 만들어두는 역할을 하게 된다. - 예를 들어
auto func = bind(f, 123, std::placeholders::_1)
f
함수의 첫번째 인수는123
으로 고정된다.std::placeholders::_1
는 아직 뭐가 들어올지 모르지만f
함수의 두번째 인수 자리를 뜻한다. 두번째 인수 자리로 잡아 둠.- 실행시
func(777)
은 곧f(123, 777)
과도 같다.- 고정 인수를 제외하고 place holding 해둔 자리에 필요한 인수만 넘기면 된다.
- 설명하기 좀 어려우니 예제를 보며 이해해보자.
#include <functional>
#include <iostream>
using namespace std;
int multiply(int a, int b)
{
return a * b;
}
int main()
{
auto func = std::bind(multiply, 5, placeholders::_1);
for (int i = 0; i < 10; i++)
{
cout << "5 * " << i << " = " << func(i) << endl;
}
return 0;
}
💎출력💎
5 * 0 = 0
5 * 1 = 5
5 * 2 = 10
5 * 3 = 15
5 * 4 = 20
5 * 5 = 25
5 * 6 = 30
5 * 7 = 35
5 * 8 = 40
5 * 9 = 45
multiply
함수는(int, int)
2 개의 매개 변수를 두는 함수다.- 이 예제에서는 둘 중 하나만 고정 인수로 둘 것이기 때문에 나머지 한 자리는
placeholder
로 자리만 홀딩 해두어야 한다.
- 이 예제에서는 둘 중 하나만 고정 인수로 둘 것이기 때문에 나머지 한 자리는
func
는std::function <int (int, int)>
타입이 된다.bind
👉multiply
함수를 어떤 특정 인수와 함께 묶어준다.multiply
함수의 첫번째 인자를5
로 고정하고 있다.multiply
함수의 두번째 인자로 어떤 것이 들어올지 모르기에placeholders::_1
로 자리만 잡아두고 있다.
- 첫번째 인자는 5로 고정되어 있으므로 실행시 두번째 인자를 결정할 인수 1 개만 넘겨주면 된다.
func(i)
은multiply(5, i)
와 같다.
#include <functional>
#include <string>
#include <iostream>
using namespace std;
using namespace std::placeholders;
void show(const string& a, const string& b, const string& c)
{
cout << a << "; " << b << "; " << c << endl;
}
int main()
{
// x의 첫번째 인자는 show 함수의 첫번째 인자
// x의 두번째 인자는 show 함수의 두번째 인자
// x의 세번째 인자는 show 함수의 세번째 인자
auto x = bind(show, _1, _2, _3);
// y의 첫번째 인자는 show 함수의 세번째 인자
// y의 두번째 인자는 show 함수의 첫번째 인자
// y의 세번째 인자는 show 함수의 두번째 인자
auto y = bind(show, _3, _1, _2);
// function z의 경우 show 함수의 첫번째 인자는 "hello"로 고정된 상태
// z의 ✨첫번째 인자✨는 show 함수의 세번째 인자 ✨
// z의 ✨두번째 인자✨는 show 함수의 두번재 인자 ✨
auto z = bind(show, "hello", _2, _1);
x("one", "two", "three");
y("one", "two", "three");
z("one", "two"); // 인수를 2 개만 넘기면 됨. 첫번째 인수는 "hello"로 고정되어 있기 때문에!
return 0;
}
💎출력💎
one; two; three
three; one; two
hello; two; one
x("one", "two", "three")
👉show("one", "two", "three")
와 같다.y("one", "two", "three")
👉show("three", "one", "two")
와 같다.z("one", "two")
👉show("hello", "two", "one")
와 같다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기