C++/C++

람다 lambda

Elan 2021. 3. 12. 22:07

Lambda


람다 표현식(Lambda expression) 또는 람다 함수(Lambda function), 익명 함수(anonymous function)로 불린다.

 

그 성질은 함수 객체(functor)와 동일하다. ( 함수포인터는 C 스타일이므로 언급 X )

 

익명함수라는 이름처럼 몸통은 있지만 이름이 없는 함수다.

 

요즘 대부분의 프로그래밍 언어들이 Lambda를 지원하고 있다.

 

Lambda 의 특징

 

- 코드의 길이가 줄어든다.

 : 함수객체를 생성하려면 class 나 struct는 정의해야하지만, Lambda는 함수 객체를 묵시적으로 정의하고 함수 객체를 생성한다.

 

- 인라인화가 가능하다. 

: 정확히 명시되어 있는 Lambda expression에 한하여 컴파일러는 이를 inline 처리합니다.( 콜 오버헤드를 없앨 수 있음 )

 

- 익명성을 가진다.

: Lambda로 생성된 함수 객체는 타입을 가지고 있긴 하지만 decltype이나 sizeof를 사용할 순 없다.

 

Lambda 문법

 

캡처[ capture ], 인자(parameters), 반환형(return type), 몸통(body)로 이루어져 있다.

caputure는 introducer 라고도 부름. body는 statement라고도 부름.

[captures](parameters) -> return type { body }

[ 캡쳐 ]

: 사용 시 외부 변수를 캡쳐해 람다의 몸통에서 사용가능

< 람다 Capture >

기본적으로 capture에  연산자 '='를 넣어 외부 변수를 갖고올 경우 복사하여 가져온다.

따라서 원본에는 영향을 미치지 않겠다는 뜻의 옵션이다.

int x = 10;
double y = 5.4;
string s = "hwan";


auto lam1 = [=]() {
	int cp_x = x;
	double cp_y = y;
	string cp_s = s;
	return x + y;
}();
//위 처럼 람다의 body 뒤에 바로 () 괄호가 붙으면 함수를 바로 실행하는 것을 의미한다.
//또한 람다가 매개변수를 필요로 할 경우 괄호 안에 매개변수를 전달해야한다.

auto lam2 = [=, x = x + 1](){
	cout << "l2함수의 x의 주소 값 : " << &x << endl;
	return x * x;
};


cout << "l1 = " << lam1 << endl;

cout << "l2 = " << lam2() << endl;
cout << "l2 = " << lam2() << endl;
cout << "x = " << x << endl;
cout << "main함수의 x 주소 값 : " << &x << endl;
	

< 출력 결과 >

lam1 에서 외부에서 선언한 변수 x를 가져와 사용하고 변경하는 부분이 없음을 볼 수 있다.

lam2 에서는 외부에서 선언한 변수 x를 가져왔지만 캡쳐 옵션에 '='를 사용하였기 때문에 x를 선언과 동시에 초기화 하겠다는 뜻이다.

때문에 내부에서 주소값을 조회해보면 외부의 x와 다른것을 알 수 있다.

 

또한 Capture블록 에서 초기화된 변수는 const로 선언이 됩니다.

따라서 초기화된 변수 x는 변경이 불가능 합니다

 

capture 으로 연산자 '&'를 넣으면 참조 형태로 가져오겠다는 것을 의미한다.

int x = 10;

auto l1 = [&]() {
	x = 5;
	return x;
};

auto l2 = [&, x = x + 100](){
	return x;
};

cout << l1() << endl;
cout << "main  x  : " << x << endl;
cout << l2() << endl;
cout << "main  x  : " << x << endl;

< 출력 결과 >

'=' 연산자와 다르게 '&' 연산자는 외부 변수의 값을 변경할 수 있습니다.

따라서 8행의 l1 람다는 x 값을 변경하게 되고, 외부에 선언된 변수 x는 10에서 5로 변경된 것을 볼 수 있다.

 

l2 에서 선언된 x는 람다 내부에서 선언한 변수이며 외부 변수 x에 영향을 미치지 않는다.

때문에 x = x + 100 을 하였지만 외부 변수 x의 값은 그대로 5인 것을 볼 수 있다.

 

외부변수를 사용하지 않는다면 다음과 같이 캡쳐 옵션은 생략할 수 있다.

auto l1 = [](int x) -> int{
    x++;
    return x;
}(5);

( 인자 )

: 매개변수 명시

 

함수 호출 시 전달 될 인자를 정의하면 된다.

필요 없다면 비워두면된다.

 

-> 반환 타입

: 반환 타입 명시

double num = 10;

auto l1 = [](double a, int b) ->int {

	return a + b;
};

//반환 타입은 auto도 가능하다
auto l2 = [](int a, int b)->auto {
	a++;
	b--;

	return a + b;
}(num, 5);


int(*fc_1)() = []() -> int {
	return 100;
};

//반환 타입을 생략하는 것도 가능하다
int(*fc_2)() = []() {
	return 10000;
};

cout << l1(10.1, 5) << endl;
cout << 12 << endl;
cout << fc_1() << endl;
cout << fc_2() << endl;

< 출력 결과 >

 

{  몸통  }

: 함수의 내용 정의

 

template <class T>
void simple_sort(int *arr, int n, T cmp) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = i + 1; j < 5; j++) {
            if (cmp(arr[i], arr[j]))
                arr[i] ^= arr[j] ^= arr[i] ^= arr[j];
        }
    }
}
 
void sort_print(int *arr, int n) {
    for (int i = 0; i < 5; i++)
        cout << arr[i] << " ";
    cout << endl;
}
 
int main(void) {
    int arr[5] = { 10, 5, 41, 100, 2 };
 
    simple_sort(arr, 5, [](int a, int b) {
        return (a < b ? true : false); });
    sort_print(arr, 5);
 
    simple_sort(arr, 5, [](int a, int b) {
        return (a > b ? true : false); });
    sort_print(arr, 5);
 
 
    return 0;
}

main문에서 simple_sort 함수 호출부에서 3번째 인자를 주목하자.

 

capture [ ] 부분은 비어있고, parameters 는 int a, 와 int b로 정의하였다.

body { } 부분을 보면 a<b 에 대한 참/거짓 여부를 반환한다.

 

함수를 인자로 전달하는 코드임에도 불구하고 매우 간결하고 편리하다.

이렇게 간결하고 명확한 코드는 컴파일러가 자동으로 inline화 시켜준다는 장점이 있다.

 

하지만 단점도 존재한다.

 

각 simple_sort 호출 시마다 body 부분이 다른 것을 볼 수 있다.

  [ ] ( int a, int b ) {  return  ( a < b ? true : false ) ;  } 

 

하지만 body가 길어진다면 가독성이 떨어진다.

 

또한 똑같은 Lambda expression 이 코드 여러군데 존재한다면 함수를 따로 정의하는 것이 좋다.

 

그러므로 상황에 따라 잘 사용해야 한다.

 

자동 inline 화가 되지 않는 Lambda 정의

 

람다는 익명 객체이다.

 

즉, 이름은 없지만 컴파일 시에 람다라는 객체가 생성된 후에 inline 화 된다.

 

그러므로 람다를 담을 변수가 상수화 되지 않는다면 ( 컴파일 시에 알 수 없다면 )  일반함수와 다를게 없어진다.

auto f1 = [](int a, int b) {                    //인라인화 OK!!
	return a + b;
};

int(*f2)(int, int) = [](int a, int b) {            //인라인화 Fail!!
	return a + b;
};

std::function<int(int, int)> f3 = [](int a, int b) {    //인라인화 Fail!!
	return a + b;
};

첫번째, auto는 컴파일 시에 그 내용을 알 수 있다는 것을 의미합니다.

 

두번째, 포인터라는 것은 언제든지 다른 것을 가리킬 수 있다.

때문에 함수 포인터를 사용해 간접 전달되거나, 람다를 담을 변수가 상수화 되지 않으면 일반 함수와 다를게 없어진다.

 

세번째, std::function<Type> 은 모든 함수를 담을 수 있는 템플릿 객체이다.

함수, 함수 객체, 람다 등을 담을 수 있도록 복잡하게 만들어져 있다.

 

즉, 언제든지 다른것으로 바꿔 담을 수 있다는 것을 의미한다.

 

따라서 람다를 담을 순 있지만 컴파일 시에 익명 객체로 정의되는 람다로 인식하지는 않는다는 것.

 

다시 말해서 컴파일러가 인라인하는 기준은 변경이 불가능한 상수 변수이냐 아니냐에 따른다.

 

stackoverflow.com/questions/16729941/are-stdfunctions-inlined-by-the-c11-compiler
hwan-shell.tistory.com/84