'C++' Lambda expression using 'auto'

토스트·2024년 12월 15일
0

'C++' basic

목록 보기
7/35

Lambda expression을 설명하기에 앞서 auto 키워드를 간략히 다뤄보겠습니다.

auto

컴파일러가 변수의 타입을 자동으로 추론하도록 해주는 기능입니다.

장점

  • 반복자나 긴 타입 이름 대신 사용하여 코드를 간결하게 유지하여 가독성을 높일 수 있습니다.
  • 반환 타입을 미리 알 수 없거나, 타입이 유동적인 경우 auto는 타입 추론을 자동으로 처리해주어 코드가 간결해지고, 템플릿 함수 작성이 쉬워집니다.

주의사항

  • 초기화 필요 : auto를 사용할 때는 반드시 초기화가 필요합니다. 초기화 값이 없으면 컴파일러는 타입을 추론할 수 없기 때문에 오류가 발생합니다.
    초기화 외에도 추론할 정보가 부족할 때 또한 오류가 발생합니다.

예시

auto x = 10; // x는 int로 추론
auto y = 3.14; // y는 double로 추론
auto z = "Hello"; // z는 const char *로 추론

auto [a, b] = make_pair(3, 4.5); // a는 int로 추론, b는 double로 추론

auto a; // Error 

단점

컴파일러가 예상치 못한 타입으로 추론하여 의도한 것과 다른 타입이 될 수 있고, 타입 정보가 숨겨져 있어서 코드의 명시성이 떨어질 수 있습니다.

람다 표현식(Lambda expression)

주로 함수의 인수로 호출되거나 전달되는 위치에서 익명 함수(Anonymous Function) 또는 람다 함수(Lambda Function)를 정의하는 편리한 방법으로, 이름이 없는 함수 객체를 간단하고 직관적인 방식으로 표현합니다. 이는 일회성 함수 정의가 필요할 때 유용하게 사용됩니다.

람다 표현식

[Capture](Parameters) mutable Exception-Specification -> Return type { Statement };

  • Capture : 람다의 외부 변수를 캡처하는 방식입니다.
  • Parameters : 람다가 받을 인자 목록입니다.
  • mutable <생략 가능> : 람다 내부에서 '값으로 캡처한 변수'를 수정 가능할 수 있게 합니다.
  • Exception-Specification <생략 가능> : 해당 람다 함수가 예외를 던지지 않음을 명시할 때 'noexcept'을 사용할 수 있습니다.
  • -> Return type <생략 가능> : 람다가 반환할 값의 타입을 명시합니다.
  • Statements : 람다의 본문입니다.

주로 사용되는 람다식

[Capture](Parameters) { Statement };

쉬운 예시

#include <iostream>

using namespace std;

int main() {
    // 람다 함수 정의
    auto add = [](int x, int y) { return x + y; };

    // 람다 함수 호출
    cout << "3 + 5 = " << add(3, 5) << endl;  // 3 + 5 = 8

    return 0;
}

인자로 사용되는 람다식

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

using namespace std;

int main() {
    vector<int> numbers = {10, 2, 3, 15, 7};

    // 람다를 사용하여 내림차순으로 정렬
    sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; });

    // 정렬된 결과 출력
    for (const int & num : numbers) {
        cout << num << " "; // 15 10 7 3 2
    }

    return 0;
}

auto 사용 이유

람다 함수의 타입은 컴파일 타임에 결정되므로, auto를 사용하여 타입을 자동으로 추론하도록 하는 것이 매우 편리합니다. 이를 통해 람다의 타입을 명시적으로 지정할 필요 없이 간단하게 사용할 수 있습니다.

auto로 선언

#include <iostream>

using namespace std;

int main() {
    auto lambda = [](int a, int b) { return a + b; };
    
    return 0;
}

명시적으로 타입 지정

#include <iostream>
#include <functional>  // std::function 사용을 위한 헤더

using namespace std;

int main() {
    function<int(int, int)> lambda = [](int a, int b) { return a + b; };
    // 단순한 int형이 아님. 
    return 0;
}

Capture

람다 표현식에서 외부 변수를 람다 함수 내부로 가져오는 방식을 말합니다.

Capture 종류

외부 변수 x, y

  1. 캡처하지 않음 '[]' : 모든 외부 변수를 캡처하지 않음을 의미합니다. 즉, 람다 함수 내부에서 외부 변수에 접근할 수 없으며, 오로지 람다 내부에서 정의된 변수들만 사용할 수 있습니다.

  2. 값으로 캡처(By Value) '[x]' : 외부 변수를 값으로 캡처하면, 람다 함수가 호출될 때 변수의 값이 복사되어 람다 내부에서 사용됩니다. 이 경우, 외부 변수가 변경되어도 람다 내부의 값은 변경되지 않으며 않습니다.

  3. 참조로 캡처(By Reference) '[&x]' : 외부 변수를 참조로 캡처하면, 람다 함수가 호출될 때 변수의 값이 참조되어 람다 내부에서 사용됩니다. 즉, 람다 내부에서 해당 변수를 수정하면 외부 변수에도 영향을 미칩니다.

  4. 모든 변수를 값으로 캡처 '[=]' : 모든 외부 변수를 값으로 캡처합니다.

  5. 모든 변수를 참조로 캡처 '[&]' : 모든 외부 변수를 참조로 캡처합니다.

  6. 혼합 캡처 '[x, &y]' : 일부 변수는 값으로, 일부 변수는 참조로 캡처합니다.

#include <iostream>

using namespace std;

int main() {
	int x = 5, y = 10;
	
	auto lambda1 = []() { cout << "No Capture" << endl; };
    auto lambda2 = [x]() { cout << "Captured by value : " << x << endl; };
    auto lambda3 = [&x]() { cout << "Captured by reference : " << x << endl; };
    auto lambda4 = [=]() { cout << "All captured by value : " << x << ", " << y << endl; };
    auto lambda5 = [&]() { cout << "All captured by reference : " << x << ", " << y << endl; };
    auto lambda6 = [x, &y]() { cout << "x is captured by value : " << x << "/n y is captured by reference : " << y << endl; };

	return 0;
}

mutable

C++ 람다 함수에서 '값으로 캡처된 변수의 값'을 수정할 수 있게 해주는 특수한 키워드입니다.

기본적으로 값으로 캡처된 외부 변수는 읽기 전용으로 취급되기 때문에, 람다 함수 내부에서 값을 변경할 수 없습니다.
하지만 mutable 키워드를 사용하면, '값으로 캡처된 변수'에 한하여 값을 수정할 수 있도록 const 제약을 해제해주는 역할을 합니다.

#include <iostream>

using namespace std;

int main() {
    int x = 10;
    
    auto lambda1 = [x]() {  // x를 값으로 캡처
        x += 5;  // Error : x는 읽기 전용이라 수정할 수 없음
    };
	
    auto lambda2 = [x]() mutable {
    	x += 5;
    	cout << x << endl; // 출력 : 15
    };
    
    lambda2();
    
    cout << "After running lambda2 x : " << x << endl; // 출력 : 10
    
    auto lambda3 = [&x]() { // 참조로 캡처하였기 때문에 x는 읽기 전용이 아님
    	x += 5;
    };
    
    lambda3();
    
    cout << "After running lambda3 x : " << x << endl; // 출력 : 15
    
    return 0;
}

예외 사양 (Exception-Specification)

함수가 예외를 던질 수 있는지 여부를 명시하는 방식입니다. 기존의 throw 기반 예외 사양은 C++11에서 noexcept로 대체되었으며, 이는 함수가 예외를 던지지 않음을 보장하는 방식입니다.

예외 처리 과정 중 noexcept 함수에서 예외 발생 시

컴파일러가 이를 컴파일 오류로 처리합니다. 이런 상황에서는 프로그램이 실행되기 전에 컴파일 단계에서 오류가 발생합니다.

예외 처리 과정이 아닐 때 noexcept 함수에서 예외 발생 시

이 경우 컴파일 타임에 예외가 던져질 수 없다는 것이 보장되기 때문에 예외가 발생할 경우 std::terminate() 함수가 호출되어 프로그램이 강제 종료됩니다.

이점

  • 성능 최적화 : 예외를 던지지 않음을 컴파일러에 알려주어, 예외 처리에 필요한 코드가 생략되고, 컴파일러가 더 나은 최적화를 수행할 수 있습니다.
  • 코드 명확성 : 함수가 예외를 던지지 않음을 명시하여, 코드를 읽는 사람이 해당 함수의 동작을 쉽게 이해하고, 예외 처리에 대해 걱정할 필요가 없음을 알려줍니다.

0개의 댓글