멈추지 않고 끈질기게

[C++] 람다 표현식(lambda expression) 본문

C++

[C++] 람다 표현식(lambda expression)

sam0308 2023. 12. 5. 10:20

 

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

※ 해당 포스팅은 C++ 17 버전을 기준으로 작성되었습니다.

※ 해당 포스팅은 하기 출처들을 참조하였습니다.

- https://learn.microsoft.com/ko-kr/cpp/cpp/lambda-expressions-in-cpp?view=msvc-170

 

 

 

 이번 포스팅에서는 C++의 람다 함수에 대해 알아보겠습니다.

1. 람다 표현식이란

 람다 표현식(lambda expression)이란 함수를 정식 선언하지 않고 익명으로 작성하는 것을 말합니다(이하 람다 식). C++에서의 람다 식은 다음과 같은 형식으로 작성합니다. 

int main()
{
    // [캡쳐](매개변수)->반환형 {함수 내용}
    []()->void {};
    
    // 반환형 생략 버전
    // [캡쳐](매개변수) {함수 내용}
    [](int num) {
        cout << num << endl;
    };
}

 

 또한 사용할 일은 적지만, 선언과 함께 바로 호출할 경우 함수 내용 뒤에 ()를 붙이고 매개변수를 전달하여 실행할 수 있습니다.

#include <iostream>

using namespace std;

void print_num(int num)
{
    cout << num << endl;
}

int main()
{
    // [캡쳐](매개변수) {함수 내용}(매개변수);
    // print_num(100)과 동일한 동작
    [](int num) {
        cout << num << endl;
    }(100);

    print_num(100);
}

사진 1. 람다식 예시

 

 위 코드에서는 입력한 정수를 그대로 출력하는 람다식에 100을 인자로 넘겨 바로 실행하였습니다. 매개변수와 구현부가 동일한 print_num() 함수와 동일하게 동작하는 것을 확인할 수 있습니다.

 

 

2. 람다 식이 필요한 경우

 람다 식은 한번만 사용할 함수를 굳이 정의하지 않아도 된다는 점에서 편리합니다. 대표적으로 알고리즘 헤더의 sort() 함수 사용 시 정렬 조건을 지정할 때 이용할 수 있습니다. 

#include <iostream>
#include <algorithm>

using namespace std;

bool compare(int a, int b)
{
    return a > b;
}

int main()
{
    int arr[5] = { 5, 3, 2, 1, 4 };

    sort(arr, arr + 5); // 오름차순
    for (auto i : arr)
        cout << i << " ";
    cout << endl;

    sort(arr, arr + 5, compare); // 내림차순
    for (auto i : arr)
        cout << i << " ";
    cout << endl;
}

사진 2. sort() 예시

 

 sort() 함수를 이용하면 자료구조 내의 원소들을 정렬시킬 수 있는데, numeric 변수들의 경우 기본적으로 오름차순으로 정렬시킵니다. 정렬 조건을 바꾸고 싶다면, 3번째 원소로 정렬 기준이 될 함수를 직접 넘겨주어야 합니다. 위 예시의 경우 int형 배열의 정렬에 사용하기 위해 int형 매개변수 2개를 받고 bool 타입을 반환하는 비교함수 compare()를 선언하였습니다. 해당 함수를 3번째 매개변수로 넘겨주면 내림차순으로 정렬되는 것을 확인할 수 있습니다.

 

 다만 이 경우 compare() 함수는 오로지 sort() 함수의 인자로 사용하기 위해 선언되었으며, 이후로 해당 함수를 사용할 일이 없다면 다소 낭비라는 생각이 듭니다. 특히 직접 선언한 구조체나 클래스의 경우, 기본 정렬 연산자가 없으므로 정렬 기준이 될 함수를 반드시 넘겨주어야 하므로 이런 상황이 많이 발생합니다. 이런 경우에 람다 식을 활용하면 편리합니다.

 

#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
    int arr[5] = { 5, 3, 2, 1, 4 };

    sort(arr, arr + 5); // 오름차순
    for (auto i : arr)
        cout << i << " ";
    cout << endl;

    sort(arr, arr + 5, [](int a, int b) { return a > b; }); // 내림차순
    for (auto i : arr)
        cout << i << " ";
    cout << endl;
}

사진 3. sort() 예시(람다 식)

 

 위 코드는 compare() 함수를 선언하는 대신, 동일한 구조의 람다 식을 통해 내림차순 정렬을 실행한 모습입니다. 이처럼 경우에 따라 람다 식을 활용하면 불필요한 함수 선언을 피하고, 코드 구조를 간소화할 수 있습니다.

 

 

3. 캡쳐

 C#과 C++의 람다 식에서 다른 부분으로, 사실상 이번 포스팅을 작성하게 된 계기에 해당하는 부분입니다. 람다식 외부의 변수를 참조하는 방식을 정하는 정의하는 부분으로 []안에 참조할 외부 변수를 직접 입력할 경우 값 참조, 변수 이름 앞에 &를 붙일 경우 주소 참조로 접근하게 됩니다. []안에 =만 작성할 경우 모든 변수를 값 참조, &만 작성할 경우 모든 변수를 주소로 참조합니다.

 

 이러한 캡쳐 부분이 필요한 이유는, 기본적으로 람다 식 외부에서 선언된 변수를 람다 식 내부에서 접근할 수 없기 때문입니다. 

사진 4. 람다 식 외부 변수의 접근

 

 다음과 같이 람다식 외부의 변수를 별도의 캡쳐 선언 없이 사용하려고 하면 컴파일 에러가 발생합니다. 이를 해결하려면 다음 예시와 같이 캡쳐 구문을 사용해야 합니다.

 

int main()
{
    int a = 100;

    auto add_vref = [=](int num) { cout << "a in lambda: " << a + num << endl; };
    auto add_aref = [&](int num) { a += num; cout << "a in lambda: " << a << endl; };

    add_vref(200); // 값 참조
    cout << "a in main: " << a << endl;
    add_aref(200); // 주소 참조
    cout << "a in main: " << a << endl;
}

사진 5. 값 참조 vs 주소 참조

 

 위 코드에서는 외부 변수를 값 참조하는 add_vref() 함수와 주소 참조하는 add_aref() 함수를 선언하였습니다. int형 인자를 하나 받아 a에 더하고, 람다 식 내부에서의 값을 출력합니다. 각 람다 식을 실행하고 외부에서 a 값을 출력하여 값을 비교하도록 했습니다. 우선 add_vref() 에서는 a를 값으로 참조하므로 a값의 변경이 애초에 불가능하며, 실행 후 메인 함수의 a값은 당연히 100으로 동일합니다. add_aref() 에서는 주소로 참조하므로 a += num 이 가능하며, 실행 후 메인 함수에서 확인해보면 a값이 300으로 변경된 것을 확인할 수 있습니다.

 

 

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct Player
{
    Player() : _hp(100), _atk(10), _def(10) {}
    Player(int hp, int atk, int def) : _hp(hp), _atk(atk), _def(def) {}
    void Print() { cout << "HP: " << _hp << ", Atk: " << _atk << ", Def: " << _def << endl; }

    int _hp;
    int _atk;
    int _def;
};

int main()
{
    vector<Player> players(3, Player());
    players[0]._atk = 30;
    players[1]._def = 50;
    players[2]._hp = 200;

    // [], [=]로 캡쳐 시 컴파일 에러
    [&players](Player p) {
        players.push_back(p);
    }(Player(150, 30, 20));

    for (auto p : players)
        p.Print();
}

 

사진 4. 캡쳐 예시

 

 위 코드는 람다 식으로 벡터에 원소를 추가하는 예시입니다. 람다 식 외부에서 선언된 players 벡터를 람다 식 내부에서 접근하므로, 캡쳐 영역에서 [&players]로 해당 벡터를 주소 참조하거나 아예 [&]로 모든 외부 변수를 주소 참조하도록 선언해야 합니다. 별도로 캡쳐하지 않거나([]) 벡터를 값 참조하려고 할 경우([=]) 컴파일 에러가 발생합니다. 주소 참조이므로 메인 함수의 players 벡터에 원소가 추가되었음을 확인할 수 있습니다.


 람다 식은 C#에서도 많이 사용해왔고, C++에서도 큰 어려움 없이 사용하고 있었는데 캡쳐 구문에서 약간 혼동이 있어 별도로 정리해보았습니다. 알고 있다고 생각해 온 내용이라도 한번 정리해보는 것이 큰 도움이 되는 듯 합니다.

 

 

 

 

 

'C++' 카테고리의 다른 글

[C++][메모용] map 관련 정리  (0) 2024.08.01
[C++] std::endl vs "\n"  (2) 2024.07.23
[C++] 스마트 포인터(Smart Pointer)  (0) 2023.11.23
[C++] 가상 함수(virtual function)  (0) 2023.03.22
[C++] 타입 변환 연산자(Type Casting Operator)  (0) 2023.03.19