C++ 중급 - Lambda Expression

타입·2024년 2월 26일
0

C++ 공부

목록 보기
17/17

Lambda Expression

C++11부터 제공
익명의 함수(객체)를 만드는 문법

  • lambda expression 기본 모양
    []: lambda Introducer

    [captures]<tparams> requires (params)
    			specs	requires { body }
  • lambda expression 활용

  1. std::sort() 등의 알고리즘에 인자로 전달
  2. auto 변수에 담아서 함수처럼 사용
  3. 람다 표현식을 반환하는 함수
  • capture local variable
    lambda expression 안에서 지역변수에 접근하려면 반드시 자역변수를 캡쳐해야함

    • [v1] : capture by value (Read Only)
    • [&v1] : capture by reference (Read/Write)
  • lambda expression return type

int main()
{
	auto f1 = [](int a, int b) -> int { return a + b; }; // 후위 반환 타입 표기
	auto f2 = [](int a, int b){ return a + b; }; // 타입 추론
	auto f3 = [](int a, int b){ if (a == 1) return a; return b; };
//	auto f4 = [](int a, double b){ if (a == 1) return a; return b; }; // 타입 추론 불가
	auto f4 = [](int a, double b) -> double { if (a == 1) return a; return b; };
}

Lambda Expression 원리

Lambda Expression: Closure를 만드는 표현식
C++의 Lambda Expression: 범위 밖의 변수를 캡쳐할 수 있는 이름 없는 함수 객체

  • 람다 표현식을 컴파일러가 함수 객체로 만들어줌
#include <algorithm>

int main()
{
	std::vector<int> v = {1, 3, 5, 7, 9, 2, 4, 6, 8, 10};

//	std::sort( v.begin(), v.end(), [](int a, int b) { return a < b;} );

	class CompilerGeneratedName
	{
	public:
		inline auto operator()(int a, int b) const
		{
			return a < b;
		}
		// ......
	};

	std::sort(v.begin(), v.end(), CompilerGeneratedName{}); // 최종적으로 임시 객체가 만들어짐
}

람다 표현식 자리가 임시 객체로 바뀌는데 그 임시 객체를 Closure Object라 불림
Closure Object: lambda expression이 만드는 unnamed function object

  • lambda expression은 prvalue
int main()
{
	auto  f1 = [](int a, int b) { return a + b;}; // ok
	// class CompilerGeneratedNameA{}; // 컴파일러에서 클래스를 만들고 괄호 연산자 재정의
	// auto  f1 = CompilerGeneratedNameA{}; // 임시 객체가 대입

	// lvalue reference
//	auto& f2 = [](int a, int b) { return a + b;}; // error

	// forwarding reference
	auto&& f3 = [](int a, int b) { return a + b;}; // ok

	// const lvalue reference
	const auto& f4 = [](int a, int b) { return a + b;}; // ok
}

Lambda Expression 원리 2

  • capture by value
    지역변수를 캡쳐하면 생성된 클래스에 멤버변수가 추가되고 생성자에서 멤버변수를 초기화
    임시 객체는 지역변수를 전달
int main()
{
	int v1 = 10, v2 = 20;

	auto f = [v1, v2](int a) { return a + v1 + v2;};
    
	/*
    class CompilerGeneratedName
    {
    	int v1;
        int v2;
    public:
    	CompilerGeneratedName(int& v1, int& v2) : v1{v1}, v2{v2} {}
        inline auto operator()(int a) const
        {
        	return a + v1 + v2;
            // ...
        }
    };
    
	auto f = CompilerGeneratedName{v1, v2};
    */
    
	std::cout << f(5) << std::endl; // 35
	std::cout << sizeof(f) << std::endl; // 8
}
  • mutable lambda expression
    operator() 멤버 함수를 non-const-member function으로 생성
int main()
{
	int v1 = 10, v2 = 20;


//	auto f = [v1, v2](int a) { v1 = a; v2 = a;}; // 컴파일 에러
	auto f = [v1, v2](int a) mutable { v1 = a; v2 = a;}; // mutable 람다 표현식
	
	/*
    class CompilerGeneratedName
    {
    	int v1;
        int v2;
    public:
    	CompilerGeneratedName(int& v1, int& v2) : v1{v1}, v2{v2} {}
        inline auto operator()(int a) // const 함수로 만들지 않음
        {
        	v1 = a; // 지역 변수가 아닌 멤버 변수를 수정
            v2 = a;
        }
    };
    
	auto f = CompilerGeneratedName{v1, v2};
    */

	f(3);

	std::cout << v1 << std::endl; // 10
	std::cout << v2 << std::endl; // 20
}
  • capture by reference
int main()
{
	int v1 = 10, v2 = 20;

//	auto f = [v1, v2](int a)         { v1 = a; v2 = a;}; // error
//	auto f = [v1, v2](int a) mutable { v1 = a; v2 = a;}; // 복사본 변경
	auto f = [&v1, &v2](int a) { v1 = a; v2 = a;};
    
	/*
    class CompilerGeneratedName
    {
    	int& v1;
        int& v2;
    public:
    	CompilerGeneratedName(int& v1, int& v2) : v1{v1}, v2{v2} {}
        inline auto operator()(int a) const
        {
        	v1 = a; // const 함수지만 v1이 가리키는 곳을 변경하는 것이 아닌
            v2 = a; // v1이 가리키는 곳의 값을 변경하는 것
            		// 즉, v1 자체인 참조가 변경되는 것이 아니므로 에러 아님
        }
    };
    
	auto f = CompilerGeneratedName{v1, v2};
    */
    
	f(3);
    
	std::cout << v1 << std::endl; // 3
	std::cout << v2 << std::endl; // 3
}
  • default capture
    • [=] : 모든 지역변수를 capture by value
    • [&] : 모든 지역변수를 capture by reference
int main()
{
	int v1 = 10, v2 = 10;

	auto f1 = [v1, &v2]() {}; // v1은 값으로, v2는 참조로 캡쳐

	auto f2 = [=]() {}; // 모두 값으로 캡쳐
	auto f3 = [&]() {}; // 모두 참조로 캡쳐

	auto f4 = [=, &v1]() {}; // v1만 참조로 캡쳐
	auto f5 = [&,  v1]() {}; // v1만 값으로 캡쳐
//	auto f6 = [&, &v1]() {}; // error

	// x에 v1을 값으로 캡쳐, y에 v2를 참조로 캡쳐
	auto f7 = [x = v1, &y = v2](int a) { y = a + x; };

	std::string s = "hello";
	auto f8 = [ msg = std::move(s) ]() {};
	std::cout << s << std::endl; // ""
}

함수 포인터로의 변환

람다 표현식을 함수 포인터에 담을 수 있음

int main()
{
	auto            f1 = [](int a, int b) { return a + b;};
	int(*f2)(int, int) = [](int a, int b) { return a + b;};
    
	std::cout << f2(1, 2) << std::endl;
}

람다 표현식의 결과는 이름 없는 함수 객체(임시 객체)인데 어떻게 함수 포인터로 암시적 형변환 되는 건가
-> 컴파일러가 만드는 클래스에 함수 포인터로 변환될 수 있는 변환 연산자 함수를 제공

  • lambda expression이 함수 포인터로 변환되는 원리
    • operator()와 동일한 기능을 수행하는 static member function이 생성되고
    • 함수 포인터로의 변환을 위한 변환 연산자 함수가 제공
int main()
{
//	int(*f)(int, int) = [](int a, int b) { return a + b;};

	class CompilerGeneratedName
	{
	public:
    	// 멤버 함수 포인터가 일반 함수 포인터로 반환될 수 없음
		inline int operator()(int a, int b) const // () 연산자 함수는 static 멤버 함수가 될 수 없음
		{
			return a + b;
		}
		static int _invoke(int a, int b) // static 함수는 const 멤버 함수가 될 수 없음
		{
			return a + b;
		}
        
		using FP = int(*)(int, int);
		operator FP() const { return &CompilerGeneratedName::_invoke; }
	};

	int(*f)(int, int) = CompilerGeneratedName{};
	 				//	CompilerGeneratedName{}.operator int(*)(int, int);

	std::cout << f(1,2) << std::endl; // 3
}
  • 캡쳐하지 않은 람다 표현식만 함수 포인터로 변환될 수 있음
    캡쳐를 사용한 람다 표현식은 함수 포인터로 변환될 수 없음
int main()
{
	int v1 = 10;

	int(*f1)(int, int) = [  ](int a, int b) { return a + b;};      // ok
//	int(*f2)(int, int) = [v1](int a, int b) { return a + b + v1;}; // error

	class CompilerGeneratedName
	{
		int v1;
	public:
		CompilerGeneratedName(int& v1) : v1(v1) {}

		inline int operator()(int a, int b) const {	return a + b + v1; } // ok		

//		static int _invoke(int a, int b) 	      { return a + b + v1; } // error
	};
}

static 멤버 함수 내에선 멤버 변수에 접근할 수 없음
컴파일러가 static 멤버 함수를 만들 수 없어 함수 포인터로의 변환 함수를 만들 수 없음!


Generic Lambda Expression

  • generic lambda expression (C++14)
    lambda expression의 인자로 auto를 사용하는 기술
    • operator() 함수를 템플릿으로 제공
      각 타입 별로 함수가 인스턴스화 됨
int main()
{
	auto add = [](auto a, auto b) { return a + b;};
    /*
    template<class T1, class T2>
    inline auto operator()(T1 a, T2 b) const { return a + b; }
    */

	std::cout << add(1,   1)   << std::endl; // inline auto operator()(int    a, int    b) const { ... }
	std::cout << add(1.1, 1.2) << std::endl; // inline auto operator()(double a, double b) const { ... }
	std::cout << add(1,   1.4) << std::endl; // inline auto operator()(int    a, double b) const { ... }
}
  • generic function (C++20)
    일반 함수에도 인자에 auto를 쓸 수 있음
auto add(auto a, auto b) // 결국 템플릿으로 변환
{
	return a + b;
}

int main()
{
	std::cout << add(1,   1)   << std::endl;
	std::cout << add(1.1, 1.2) << std::endl;
	std::cout << add(1,   1.4) << std::endl;
}

Template Lambda Expression

lambda expression을 만들 때 template 문법 사용 가능 (C++20)

int main()
{
	int n1 = 10;
	int n2 = 20;
	double d1 = 1.1;
	double d2 = 2.2;

//	auto swap = [](int& a, int& b) { auto tmp = a; a = b; b = tmp;};
//	auto swap = [](auto& a, auto& b) { auto tmp = a; a = b; b = tmp;};

	auto swap = []<typename T>(T& a, T& b) { auto tmp = a; a = b; b = tmp;};

	swap(n1, n2); // 같은 타입끼리 스왑 괜찮음
	swap(d1, d2);
//	swap(n1, d2); // int, double 서로 다른 타입인데 스왑하면 문제 발생!
}

generic lambda expression은 인자가 각각 독립적인 템플릿 인자를 사용하게 됨
같은 타입을 사용하도록 제한하기 위해 template lambda expression을 사용

profile
주니어 언리얼 프로그래머

0개의 댓글