본문 바로가기

CS연구소👨‍💻

[C++] 람다 함수

들어가며

람다 함수는 익명함수이다. 한마디로 함수에 이름이 없이 필요한 자리에서 정의되는 함수라고 볼 수 있다.

람다 함수 하면 우리 들은 보통 쓸일이 많지는 않다. 일반적으로 재사용성 높은 코드를 짜는게 좋다고 배우기 때문에 남용해서는 안좋다고 한다.

그런데 이제 알고리즘 문제들을 풀다보면 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의 차이

그래서 결국 이 둘의 차이는 람다함수가 클래스로 변환되면서 처음 한번만 호출하는 멤버변수로 넣을 것이냐, 아니냐의 차이라고 할 수 있다. 우리가 보기에는 람다가 함수로 표현되어있기 때문에 그 차이를 아는 것이 어려웠으나 이렇게 공부하고 나니 명확해졌다.