람다 lambda
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에 연산자 '='를 넣어 외부 변수를 갖고올 경우 복사하여 가져온다.
따라서 원본에는 영향을 미치지 않겠다는 뜻의 옵션이다.
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