C++ Chapter 19.1 : 람다 함수, std::function, bind, placeholders

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 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)로 실행해주면 매개 변수인 inputvalue를 참조하게 되어 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);
    • 이제 func3 이름으로도 func2 기능에 접근할 수 있게 되었다.
      • {cout « val « ” “; }; 이 실행 되어 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로 자리만 홀딩 해두어야 한다.
  • funcstd::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")와 같다.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

댓글남기기