람다 표현식 (Lambda Expression)
람다 표현식(Lambda Expression)이란 C++11부터 도입된 기능입니다. 함수형 프로그래밍을 구성하기 위한 함수식이며, 간단하게 익명 함수(anonymous function)를 생성할 수 있도록 하는 문법입니다.
객체를 만들 때 필요한 지점에서 메서드 타입, 메서드 이름, 매개변수 타입 등을 쓰지 않고, 생략해서 객체를 만들 수 있게 해 줍니다.
[capture](parameter_list) -> return_type {
// 함수 본문
}
람다식을 처음 봤을 때는 지금까지 사용하였던 함수와는 달라 헷갈릴 수도 있습니다. 하지만 함수 객체나 함수 포인터를 대신하여 코드를 간결하게 작성을 하게 해주는 문법이기에 알아둔다면 도움이 될 것입니다.
화살표 표기법 (Arrow notation)
람다식에서 반환 타입을 명시적으로 표현할 때 이 화살표가 사용됩니다, 생략될 경우 컴파일러가 반환 타입을 추론하게 됩니다. 따라서 화살표 표기법은 람다식의 반환 타입을 명시하는 데 사용되며, 코드의 가독성을 높이기 위한 요소 중 하나입니다.
예를 들어, 반환 타입이 'int'인 람다식은 다음과 같이 표현됩니다.
[](int x) -> int {
return x * x;
}
하지만 대부분의 경우에는 반환 타입을 명시적으로 지정하지 않고 생략하는 것을 권장합니다. 컴파일러가 자동으로 추록할 수 있기 때문에 코드를 좀 더 간결하게 만들 수 있습니다. 아래는 반환 타입을 생략한 예제입니다.
[](int x) {
return x * x;
}
즉, 람다식에서 화살표는 반환 타입을 지정하는 역할을 하며, 생략될 경우 컴파일러가 반환 타입을 추론합니다.
그럼 독자분들께선 의문점이 생길 겁니다.
" 코드를 좀 더 간결하게 만드는 건 이해했어 그런데 반환 타입을 적었을 때 다른 사람이 봤을 때 가독성도 좋아지고 컴파일러가 추론할 필요 없이 알려주면 좀 더 빨라질 수 있지 않아? "
그것도 맞는 말입니다. 그렇지만 명시적인 반환 타입을 생략하고 자동으로 추론하는 것이 코드를 간결하게 만들고 가독성을 높일 수 있는 경우가 많습니다. 이에는 여러 이유가 있습니다.
- 간결성과 변의성 : 람다식은 주로 간단한 연산이나 작은 코드 블록을 나타내는 데 사용됩니다. 이 경우에는 반환 타입을 명시하지 않고 생략하는 것이 코드를 더 간결하게 만들어주며, 코드를 빠르게 이해하고 작성할 수 있게 해 줍니다.
- 유연성 : 반환 타입을 명시하지 않으면 코드를 수정할 때 더 유연하게 대처할 수 있습니다. 반환 타입이 자동으로 추론되기 때문에 함수의 반환값을 변경하더라도 코드를 변경하지 않아도 됩니다.
- 자동 추론의 정확성 : 현대의 C++ 컴파일러는 매우 강력한 자동 타입 추론을 제공합니다. 따라서 컴파일러는 대부분의 경우에 올바른 반환 타입을 정확하게 추론할 수 있습니다.
- 중복 방지 : 명시적으로 반환 타입을 지정하는 것은 코드 중복의 가능성을 키울 수 있습니다. 함수 본문에서 계산되는 타입이 명시되면, 코드를 수정할 때마다 반환 타입도 일일이 수정해야 할 수 있습니다.
다만, 가독성이나 코드의 복잡성이 증가하는 상황에서는 반환 타입을 명시적으로 지정하는 것이 좋을 수 있습니다. 이는 특히 복잡한 람다식이나 템플릿에서 더 중요할 수 있으므로, 여러 상황에 따라 적절한 선택을 해야 합니다.
그리고, 반환 타입을 명시적으로 지정하거나 생략하는 것은 컴파일러 성능에 직접적인 영향을 미치지는 않습니다.
컴파일러는 대부분의 경우에 타입을 추론하는 데 매우 뛰어난 기능을 가지고 있으며, 타입 추론은 코드를 간결하게 만들어주고 작정하는 데에 편의성을 제공합니다.
그러나, 명시적인 반환 타입을 사용했을 때 다음과 같은 몇 가지 장점이 있습니다.
- 문서화와 가독성 : 코드의 가독성을 높일 수 있습니다. 특히 큰 프로젝트나 협업 환경에서는 명시적인 반환 타입이 함수의 의도를 명확히 나타내고 문서화에 도움이 됩니다.
- 코드 에디터 지원 : 명시적인 반환 타입은 코드 에디터의 자동 완성 및 타입 정보 툴팁과 같은 기능을 활용할 수 있게 해 줍니다. 이는 코드를 작성하는 동안 개발자에게 추가적인 도움을 제공할 수 있습니다.
- 오류 메시지 향상 : 명시적인 반환 타입을 사용하면 함수가 의도한 대로 작성되지 않았을 때 더 명확하고 유용한 오류 메시지를 제공할 수 있습니다. 코드를 이해하기 어려운 상황에서는 명시적인 반환 타입을 지정함으로써 컴파일러가 오류를 보다 정확하게 감지할 수 있습니다.
성능 면에서는 명시적인 반환 타입을 지정하거나 생략하는 것이 큰 차이를 만들지 않습니다. 앞서 말한 것처럼 코드의 가독성과 유지보수성을 고려하여 적절한 선택을 하는 것이 중요합니다.
캡처 (Capture)
캡처(Capture)는 외부 변수를 람다 함수 내부로 가져오는 방법을 나타냅니다. 람다식은 함수 객체를 생성하는데, 이 함수 객체가 외부 변수를 참조할 때 해당 변수를 캡처합니다.
캡처 방식은 크게 값 캡처, 참조 캡처, 그리고 혼합 캡처로 나누어져 있습니다.
값 캡처 (Value Capture):
- [var]: var라는 외부 변수의 값을 람다 함수 내부로 복사합니다.
- [=]: 모든 외부 변수를 값으로 캡처합니다.
auto valueCapture = [x, y]() {
std::cout << "Captured values: " << x << ", " << y << std::endl;
};
valueCapture();
참조 캡처 (Reference Capture):
- [&var]: var라는 외부 변수에 대한 참조를 람다 함수 내부로 가져옵니다.
- [&]: 모든 외부 변수에 대한 참조를 가져옵니다.
auto referenceCapture = [&x, &y]() {
std::cout << "Captured references: " << x << ", " << y << std::endl;
};
referenceCapture();
혼합 캡쳐 (Mixed Capture):
- [&, var]: var를 값으로, 나머지 변수들은 참조로 캡처합니다.
- [=, &var]: var를 참조로, 나머지 변수들은 값으로 캡처합니다.
auto mixedCapture = [=, &y]() {
std::cout << "Mixed capture: " << x << " (by value), " << y << " (by reference)" << std::endl;
};
mixedCapture();
캡처하지 않음:
- []: 어떤 외부 변수도 캡처하지 않습니다. 람다 함수 내부에서 외부 변수를 사용할 수 없습니다.
auto noCapture = []() {
std::cout << "No capture." << std::endl;
};
noCapture();
캡처의 범위
캡처된 변수를 사용할 때 해당 변수가 선언된 스코프에서 캡처를 수행합니다. 따라서 외부 람다식에서 캡처한 변수를 내부 람다식에서 사용하고나 할 때, 그 변수는 외부 람다식의 스코프에서 선언되어 있어야 합니다.
먼저, 'main' 함수의 스코프에서 'x', 'y'를 선언한 경우를 보았을 때,
int x = 10;
int y = 20;
auto outerLambda = [x, y]() {
std::cout << "Outer Lambda: " << x << ", " << y << std::endl;
auto innerLambda = [x, y]() {
std::cout << "Inner Lambda: " << x << ", " << y << std::endl;
};
innerLambda();
};
outerLambda();
이렇게 하면 외부 람다식 'outerLambda'에서 'x'와 'y'를 캡처하고, 내부 람다식 'innerLambda'에서도 'x'와 'y'를 사용할 수 있습니다.
여기서, 'outerLambda'에서 'x'만 캡처하였을 때, 'innerLambda'가 'y' 를 캡처할 수 있을까?
그러면 예상했던 대로 컴파일 오류가 나오게 됩니다.
error C3493: 'y' cannot be implicitly captured because no default capture mode has been specified
캡처된 변수의 범위는 람다식이 선언된 스코프에 한정되어 있기 때문에, 'innerLambda' 가 선언된 스코프 안에는 'y'가 없고, 'outerLambda'에서 캡처로 가져오지도 않았기에 오류가 나오는 것입니다.
mutable
mutable 키워드는 람다 함수 내에서 캡처된 변수의 값을 변경할 수 있도록 하는 특성을 나타냅니다. 일반적으로 람다 함수는 캡처된 변수를 상수(const)로 취급하여 수정할 수 없습니다. 그러나 mutable 키워드를 사용하면 해당 변수를 수정할 수 있게 됩니다.
람다 함수의 기본 구조에서 mutable 을 사용하는 기본 구조입니다.
[capture](parameters) mutable -> return_type {
함수 본문
}
여기서 mutable은 람다 함수 내에서 캡처된 변수를 수정할 수 있도록 하는 키워드입니다.
예를 들어, 다음은 mutable을 사용하여 람다 함수 내에서 캡처된 변수를 변경하는 예제입니다.
#include <iostream>
int main() {
int x = 5;
// mutable 키워드를 사용하여 x를 수정할 수 있도록 함
auto lambda = [x]() mutable {
std::cout << "이전 x의 값: " << x << std::endl;
x = 10; // x의 값을 변경
std::cout << "변경된 x의 값: " << x << std::endl;
};
lambda(); // 람다 함수 호출
std::cout << "최종 x의 값: " << x << std::endl; // 원래 변수 x의 값은 변경되지 않음
return 0;
}
위 예제에서 'mutable' 키워드를 사용하여 'x'를 람다 함수 내에서 수정할 수 있게 하였습니다. 따라서 람다 함수가 호출되면서 'x'의 값을 변경하고, 그 결과를 출력합니다. 하지만 최종적으로 'x'의 값은 변경되지 않습니다.
그럼, 'mutable'은 값을 수정할 수 있게 해주는 키워드인데, 어떻게 이것이 가능할까요?
람다식은 실제로 함수 객체의 인스턴스로 간주됩니다. 람다식을 정의하면 컴파일러가 해당 람다 함수를 클로저(Closer) 객체로 변환합니다. 이 클로저 객체는 'operator()' 함수를 가지고 있으며, 람다 함수 내의 코드가 'operator()'에 대응됩니다.
간단한 람다 함수의 예입니다.
auto myLambda = [](int x) {
std::cout << "람다 함수가 호출되었습니다. 값: " << x << std::endl;
};
위의 코드에서 'myLambda'는 람다 함수를 나타내는 함수 객체입니다. 내부적으로 컴파일러는 이 람다 함수를 클로저(Closer) 객체로 변환 됩니다.
class UniqueClassName {
public:
void operator()(int x) const {
std::cout << "람다 함수가 호출되었습니다. 값: " << x << std::endl;
}
};
앞에서 말한 캡처된 변수를 상수로 취급하여 수정할 수 없다는 것은 opvertor() 함수에 'const'가 붙었기 때문에 수정을 할 수 없다는 것을 이해할 수 있습니다.
그렇다는 것은 mutable은 이 const 키워드를 뚫고 해당 변수를 값이 아닌 참조로 캡처하게 되어, 외부에서 람다 함수 객체의 'operator()' 함수를 호출할 때 참조로 캡처된 변수를 수정할 수 있게 되는 것입니다.
참조로 캡처된 변수? 그렇다면 참조(&) 와 같은 참조인데 람다식의 스코프 범위를 넘어가면 해당 값이 바뀌지 않는 걸까요? 그 이유는 람다 함수가 캡처한 변수를 외부로부터 복사하여 유지하기 때문입니다.
즉, 캡처한 변수와 똑같은 변수를 하나 만들고 그것을 참조를 하여 값을 수정하는 것입니다. 그렇기에 스코프 안에서 만들어졌으니 스코프 밖을 빠져나가면 캡처된 변수가 사라지는 것 입니다.
여기서 'mutable'와 참조(&) 의 차이를 알 수 있습니다.
mutable : 캡처된 변수를 복사하여 참조를 해 변수를 수정 ( 캡처된 변수 원본 수정 X )
참조(&) : 캡처된 변수 원본을 참조를 해 변수를 수정 ( 캡처된 변수 원본 수정 O )
람다 호출 연산자 ( 즉시 호출 )
람다 호출 연산자는 람다 함수를 정의하자마자 즉시 호출하는 것을 의미합니다. 이것은 보통 한 번만 사용되는 간단한 기능을 구현하거나 특정 문맥에서만 사용되는 코드 블록을 만들 때 유용합니다.
람다 함수를 즉시 호출하려면 함수 호출 연산자 '()'를 람다 정의의 끝에 추가합니다.
다음은 람다 호출 연산자의 기본 구조입니다.
[capture](parameters_list) {
람다 함수 본문
}(arguments);
예를 들어, 두 정수를 더하는 간단한 람다를 정의하고 즉시 호출하는 코드는 다음과 같습니다.
[](int a, int b) {
std::cout << a + b << std::endl;
}(3, 4);
이 코드에서 람다 표현식은 변수에 저장되지 않고, 바로 '(3, 4)' 함께 즉시 실행 됩니다. 이렇게 사용되는 람다는 결과가 필요한 부분에서 직접 호출되어 일회성으로 활용됩니다.
'프로그래밍 > C,C++' 카테고리의 다른 글
C++ 동적 라이브러리 만들기 (1) | 2023.01.03 |
---|---|
C++ 정적 라이브러리 만들기 (0) | 2023.01.03 |
C 평가 순서 [unspecified, undefined, Sequence Point] (1) | 2022.12.30 |
C++ 입력 버퍼 초기화 [ cin.ignore() ] (0) | 2022.12.28 |