들어가며
람다 함수는 익명함수이다. 한마디로 함수에 이름이 없이 필요한 자리에서 정의되는 함수라고 볼 수 있다.
람다 함수 하면 우리 들은 보통 쓸일이 많지는 않다. 일반적으로 재사용성 높은 코드를 짜는게 좋다고 배우기 때문에 남용해서는 안좋다고 한다.
그런데 이제 알고리즘 문제들을 풀다보면 sort를 할 때나 완전 그 떄 한번만 사용하면 되는 조건의 경우에 이 람다식을 사용한다고한다. 그런데 람다식은 생긴게 좀 특이해서 쓸 때마다 까먹는 일이 종종있다. 그래서 이참에 정리하기로 했다.
C++ 람다 구조
[capture-list](parameters) -> return_type {
// function body
};
일단 람다함수는 이런 구조로 되어있다 설명을 전부 떼고 보면
[](){};
대, 소, 중괄호가 모두 나와있는 형태다 뭐 필요하다면 대소중으로 외우면 될듯
각각의 기능은
“[]” : capture-list, 외부에서 변수를 가져오는 칸
“()” : paramete, 함수처럼 변수들을 입력하는 칸
“{}” : return을 포함한 함수 내용을 적는 칸
그리고 return_type은 oprtional하게 사용해도 되고 안해도 된다.
capture-list 에 대해서
parameters와 중괄호 부분은 일반 함수에도 있는 파트다. 그런데 저 capture-list 부분이 특이해서 한 번 더 찾아봤다.
capture-list의 기능
capture-list는 람다함수가 주변 스코프에 저장된 변수들에 접근할 수 있게 해주는 기능이다. parameter는 내부에서 선언된 변수들을 호출할때의 값으로 초기화 하는 거라면 capture-list는 외부 변수 자체에 접근을 돕는다.
특이한점은 내부 스코프의 변수들과 다른 외부 스코프, 예를들어 전역변수나, 클래스의 멤버함수 바깥에 있는 변수들이 다르게 접근돼야 한다는 것이었다.
멤버함수 내부에 있는 변수
class Solution {
public:
vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
unordered_map<int, int> hashTable;
for (int index = 0; index < arr2.size(); index++)
{
hashTable[arr2[index]] = index;
}
sort(arr1.begin(), arr1.end(), [&hashTable](int arg1, int arg2) {
if (hashTable.find(arg1) == hashTable.end() && hashTable.find(arg2) == hashTable.end())
{
return arg1 < arg2;
}
else if (hashTable.find(arg1) == hashTable.end())
{
return false;
}
else if (hashTable.find(arg2) == hashTable.end())
{
return true;
}
return hashTable[arg1] < hashTable[arg2];
}
);
return arr1;
}
};
멤버함수 외부에 있는 변수
class Solution {
unordered_map<int, int> hashTable;
public:
vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
for (int index = 0; index < arr2.size(); index++)
{
hashTable[arr2[index]] = index;
}
sort(arr1.begin(), arr1.end(), [&hashTable = hashTable](int arg1, int arg2) {
if (hashTable.find(arg1) == hashTable.end() && hashTable.find(arg2) == hashTable.end())
{
return arg1 < arg2;
}
else if (hashTable.find(arg1) == hashTable.end())
{
return false;
}
else if (hashTable.find(arg2) == hashTable.end())
{
return true;
}
return hashTable[arg1] < hashTable[arg2];
}
);
return arr1;
}
};
두가지 경우를 비교하면 위의 경우엔 hashTable이 멤버함수 안에 접근되어있고 이럴때는 &변수 이름으로 capture 할 수 있다. 그런데 외부변수의 경우에는 설령 같은 이름이더라도 초기화하는 = 코드가 들어가줘야한다.
일단 왜그런지를 찾아보진 못했지만 저 조항을 지키지 못할경우 컴파일 에러가 난다는 점에서 컴파일 때 뭔가 잘못된다고 볼 수 있다.
람다 함수가 컴파일 때 불러지는 과정
이런 생각을 하고 나니까, 람다가 컴파일되고 실행될때 어떤 방식으로 사용되는지가 알고싶었다.
람다 함수는 익명 클래스로 저장된다
일단 C++의 메모리구조에서 함수, 클래스는 코드 세그먼트에 저장된다. 그럼 람다함수도 그럴까? 해서 찾아봤는데, 람다함수는 컴파일 될때 어떤 익명 클래스의 인스턴스로 변환되며, 여기서 operator을 오버로딩해서 표현식을 사용한다고 한다.
#include <iostream>
#include <functional>
int main() {
int x = 10;
auto lambda = [x]() {
return x + 5;
};
std::function<int()> func = lambda;
std::cout << lambda() << std::endl;
std::cout << func() << std::endl;
return 0;
}
이런 람다함수가 있다면
class __Lambda { int x; int operator()() const { return x + 5; } };
이런 식으로 변환된다고한다.
컴파일러는 이 람다함수를 클래스로 바꾸고 그걸 기계어로 바꾼뒤에 코드 세그먼트에 저장한다. 이것들은 컴파일 시간에 일어난다.
그러면 그때 컴파일러는 캡쳐리스트에 있는 변수들을 클래스의 멤버변수화 해서 넣는다. 그리고 람다함수가 실제 호출될 때는 operator() 가 호출되는 거라고 하였는데 이때 파라미터 안에 있는 변수들이 사용되는 것이다.
람다 객체는 표현식이 처음 실행될 떄 클래스 인스턴스화 되어서 스택 메모리에 저장된다. (가끔 std::function 에 할당 될 경우엔 힙메모리에 저장되는 경우도 있다고한다.)
그리고 이 람다 객체가 실제로 호출할 떄마다 operator 함수가 실행되고 이 함수가 변수들과 함께 스택에 저장된다.
이런 구조로 람다함수는 실행되는 것이다.
capture-list 와 parameters의 차이
그래서 결국 이 둘의 차이는 람다함수가 클래스로 변환되면서 처음 한번만 호출하는 멤버변수로 넣을 것이냐, 아니냐의 차이라고 할 수 있다. 우리가 보기에는 람다가 함수로 표현되어있기 때문에 그 차이를 아는 것이 어려웠으나 이렇게 공부하고 나니 명확해졌다.
'CS연구소👨💻' 카테고리의 다른 글
[네트워크] IP프로토콜 분석 (1) | 2024.06.16 |
---|---|
[OS] 멀티프로세스, 멀티스레딩 (1) | 2024.06.13 |
[C++] Code Splitting, Forward Declaration : #ifndef, #endif 가 뭘까? (1) | 2024.06.07 |
[AWS] 책 AWS 구조와 서비스 읽기 : Chapter 3 - 1 (1) | 2024.06.07 |
[AWS] 책 AWS 구조와 서비스 읽기 : Chapter 2 (0) | 2024.06.01 |