(참고1 : https://learn.microsoft.com/ko-kr/cpp/cpp/lambda-expressions-in-cpp?view=msvc-170)
C++ 람다 식
자세한 정보: C++의 람다 식
learn.microsoft.com
(참고2 : https://gdngy.tistory.com/184)
[C/C++ 프로그래밍 : 중급] 12. 람다 표현식
Chapter 12. 람다 표현식 람다 표현식은 C++11에서 도입된 강력한 기능입니다. 이름이 없는 함수를 직접 정의하고 이를 변수에 저장하거나 함수 인자로 전달할 수 있습니다. 이와 같은 람다 표현식의
gdngy.tistory.com
[ ✔️"람다 식"이란? ]
= 람다, 람다 표현식
"익명 함수 개체(클로저)"를 정의하고 이를 변수에 저장하거나 함수의 인자로 전달할 수 있다.
람다는 코드를 간결고, 가독성 좋게 만드는 데 큰 역할을 한다.
C++11 부터 도입된 기능으로 코드를 간결하게 만들어주고, 일반 함수와 비교했을 때 익명성과 직관성의 장점을 가진다.
이는 함수를 선언하고 정의하는 전통적인 방법을 벗어나, 필요한 곳에서 직접 함수를 정의하고 사용할 수 있게 해준다.
람다 식은 "일급 객체(first-class object)"이기에 변수에 할당하거나, 함수의 인수로 전달하거나, 함수의 결과로 반환할 수 있있다.
람다 표현식은 캡쳐 매커니즘을 제공한다. 이를 통해 람다 표현식이 정의된 스코프의 변수를 사용할 수 있다. 이는 람다 표현식이 비동기 프로그래밍, 콜백 함수 등에서 유용하게 사용되는 이유다.
[ ✔️람다 식의 구성 ]
📍기본 구성 :
[캡처 리스트] (파라미터리스트) -> 반환타입 { 함수 본문 }
ex) 두 수를 더하는 람다 식 :
auto add = [](int a, int b)->int { return a + b; };
std::cout << add(1, 2) << std::endl; // 3
"add" 는 람다 표현식을 가리키는 변수다. 즉, add는 람다 식이라고 할 수 있다.
람다 식을 변수에 할당할 수 있다는 것은 매우 유용한 특징이다.
ex) 외부 변수 a, b 를 캡처하는 람다 식
int a = 1, b = 2;
auto add = [a, b]() { return a + b; };
std::cout << add() << std::endl; // 3
람다 식이 외부 범위의 변수인 a, b 를 "캡처"하고 있다. 이를 통해 람다 식 내부에서도 a,b에 접근할 수 있다.
람다 표현식은 값에 의한 캡처(value capture)와 참조에 의한 캡처(reference capture)를 지원한다.
값에 의한 캡처는 변수의 값만을 캡쳐하며, 참조에 의한 캡처는 변수의 참조를 캡처한다.
때문에 참조 캡처를 사용할 때는 람다 표현식이 변수를 사용하는 동안 변수가 범위를 벗어나지 않도록 주의해야한다.
📍Capture 리스트 :
"캡처 리스트([ ])"는 람다 표현식이 자신을 둘러싸는 코드의 범위(scope)에서 변수를 캡처할 수 있게 해주는 도구
캡처 리스트에 아무것도 없다면 람다 표현식은 외부 변수를 캡처하지 않는다.
📍Parameter 리스트 :
일반 함수의 파라미터 리스트와 동일하게 동작한다.
📍반환타입 :
반환 타입은 "->" 기호를 이용해 지정한다.
반환 타입을 명시적으로 지정하지 않으면, 컴파일러가 자동으로 반환 타입을 추론한다.
📍람다 본문 :
동작하는 기능을 구현하는 영역이다.
[ ✔️람다 표현식의 특징 ]
람다 식의 특징은 여러 가지가 있다.
📍익명함수 :
람다 식은 이름이 없는 익명 함수다. 이는 람다 식을 변수에 할당하거나 다른 함수에 인자로 전달할 수 있게 한다. 이러한 특성은 함수를 재사용할 필요가 없는 경우나, 코드를 간결하게 유지하고 싶을 떄 특히 유용하다.
📍값(Value) 캡쳐와 참조(Reference) 캡쳐 :
람다 표현식은 캡쳐 절을 통해 외부 변수를 캡쳐할 수 있다. 캡쳐할 변수를 "대 괄호([ ])" 안에 명시하면 람다 표현식 내부에서 그 변수를 사용할 수 잇게 된다. 값 캡쳐는 변수의 값을 람다에 복사하는 반면, 참조 캡쳐는 변수에 대한 참조를 람다에 저장한다.
📍유연한 반환 타입 :
람다 식의 반환 타입은 일반적으로 컴파일러에 의해 자동으로 추론된다. 하지만 필요한 경우에는 개발자가 반환 타입을 명시적으로 선언할 수 있다.
📍함수와 메서드에서 사용가능 :
람다 표현식은 일반 함수나 클래스 메서드 내부에서 모두 사용할 수 있습니다. 이는 람다 표현식이 코드를 더욱 간결하고 읽기 쉽게 만드는데 도움을 준다.
📍예제 :
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> numbers = { 1,2,3,4,5 };
// 람다 표현식을 사용하여 벡터 내 모든 원소에 +1 연산을 해준다.
std::for_each(numbers.begin(), numbers.end(), [](int &num) {
num += 1;
});
// 람다 표현식을 사용하여 벡터 내 모든 원소를 출력한다.
std::for_each(numbers.begin(), numbers.end(), [](int number) {
std::cout << number << ' ';
});
// 2 3 4 5 6
return 0;
}
[ ✔️람다 표현식의 기본 문법 ]
기본 형태는 "[ ]( ) { }" 이다.
- [ ] : 캡처 목록
- ( ) : 매개변수 목록
- { } : 함수 본문
📍람다 표현식의 구조 :
기본적으로 한 개 이상의 문장을 포함하는 익명 함수다.
[Capture](Parameter)->ReturnType{Body}
Capture의 경우 람다 표현식의 상위 범위 변수를 어떻게 접근하는지 결정하며, 여러 옵션이 존재한다.
- [ ] : 캡처하지 않음. 즉, 람다 함수 내에서 상위 범위의 변수를 사용할 수 없음
- [=] : 모든 변수를 값으로 캡처한다. 즉, 상위 범위의 변수의 복사본을 만들어 사용한다. (값에 영향 안줌)
- [&] : 모든 변수를 참조로 캡처한다. 즉, 상위 범위의 변수에 직접 접근이 가능한다. (값에 영향을 줌)
- [x, &y] : x 는 값으로, y는 참조로 캡처한다.
Parameter의 경우 람다 함수의 매개변수 목록이다. 이 부분은 일반 함수와 비슷하며, 필요에 따라 매개변수를 받아 처리할 수 있다.
ReturnType은 람다 함수의 반환 형태를 정의한다. 이 부분은 생략 가능하며, 생략시 컴파일러가 Body의 반환 값을 통해 추론하여 사용한다.
Body의 경우 람다 함수가 실행할 코드 블럭에 해당한다. 이 부분에는 람다 함수가 해야 할 작업이 정의된다.
📍람다 표현식 적성법 :
람다 표현식은 일반적으로 함수 객체를 생성하기에, 함수를 필요로 하는 곳에 람다 표현식을 사용할 수 있다.
auto add = [](int a, int b) -> int { return a + b; };
std::cout << add(1, 2) << std::endl; // 3
#include <iostream>
#include <vector>
#include <algorithm> // sort, for_each
int main()
{
std::vector<int> vec = { 3,1,4,1,6,8,4 };
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; }); // 정렬
std::for_each(vec.begin(), vec.end(), [](int num) { std::cout << num << " "; }); // 출력
// 1 1 3 4 4 6 8
return 0;
}
위의 예제들에서 람다 식은 반환형을 명시하지 않았지만, 컴파일러는 a+b의 결과가 int 형임을 추론하였다.
람다 식이 return 문을 포함하고 있는 경우, 모든 return 문은 같이 타입의 값을 반환해야한다. 그렇지 않으면 컴파일러는 반환 형식을 추론할 수 없다.
이러한 유연성은 개발자가 더 복잡한 논리를 구현하면서도 코드를 간결하게 유지할 수 있도록 해준다.
그러나 명시적인 반환 형식이 코드의 가독성을 높일 수 있게에, 복잡한 람다 식에는 반환 형식을 명시하는 것이 좋다.
📍일반화된 람다 :
auto add = [](int a, int b)->int { return a + b; };
auto generic_add = [](auto a, auto b) { return a + b; };
std::cout << add(1.1f, 2.f) << std::endl; // 3
std::cout << generic_add(1.1f, 2.f) << std::endl; // 3.1
위의 예제에서 generic_add 함수는 어떤 형식의 매개변수도 받을 수 있다.
이는 템플릿과 유사하게 작동하며, 다양한 형식에 대해 동일한 동작을 수행하는 람다 표현식을 작성할 수 있게 해준다.
[ ✔️람다 표현식과 스코프 ]
📍람다 표현식에서의 변수 접근 :
람다 표현식에서 중요하고 독특한 특성 중 하나는 람다가 선언된 영역의 변수에 접근할 수 있다는 것이다. 이러한 특성은 "캡처(Capture)" 라고 불린다.
람다 식은 "복사(Capture by Copy)" 또는 "참조(Capture by Reference)" 방식으로 외부 변수를 캡처할 수 있다.
변수의 캡처 방식은 람다 식의 시작 부분, 즉 [ ] 안에 지정한다. 외부 변수를 참조로 캡처한다면 [&] 를 사용하고, 복사로 캡처하면 [=]를 사용한다. 또한, 특정 변수만을 캡처하려는 경우 변수를 명시적으로 지정할 수도 있다.
📍예제 :
int a = 1;
int b = 2;
auto lambda = [a, &b]() {
std::cout << a + b << std::endl;
//a += 1;
b += 1;
};
lambda(); // 3
std::cout << "b :" << b << std::endl; // b :3
참조로 캡처된 b변수의 경우 람다 함수 내에서 값이 변경되면 외부에서도 그 변경이 반영된다.
람다 식은 코드의 유연성을 높이며, 함수를 정의하고 호출하는 위치에서 필요한 데이터에 직접적으로 접근할 수 있게한다.
하지만 이런 특성은 변수의 생명주기와 스코프에 대한 이해를 필요로한다. 왜냐하면 캡처된 변수가 람다 표현식의 생명 주기를 벗어나면, 예기치 않은 동작이 발생할 수 있기 떄문이다.
즉, 람다 표현식이 사용될 때 해당 변수가 여전히 존재하고 유효한지 확인해야한다.
예를 들어, 함수가 끝나면 지역 변수는 소멸되기에, 이런 변수를 람다 식이 참조로 캡처하고 함수 외부에서 람다를 호출하면 문제가 발생할 수 있다.
#include <iostream>
#include <functional>
std::function<void()> GetReferLambda()
{
int a = 1;
auto lambda = [&a]() {
std::cout << "a : " << a << std::endl;
};
return lambda;
}
std::function<void()> GetValueLambda()
{
int a = 1;
auto lambda = [a]() {
std::cout << "a : " << a << std::endl;
};
return lambda;
}
int main()
{
auto referLambda = GetReferLambda();
auto valueLambda = GetValueLambda();
referLambda(); // a : 32759
valueLambda(); // a : 1
return 0;
}
해당 예제에선 람다 GetLambda() 함수에서 반환하는 람다 식이 지역변수 'a'를 참조로 캡처하고 있다. GetLambda() 함수가 람다 식을 반환하면서 a 는 소멸되기에, main 함수에서 이 람다를 호출하면 예상했던 '1'이라는 값이 아닌 엉뚱한 값을 반환하게된다.
하지만 람다 식이 값에 의한 캡처를 사용한다면 람다식이 선언되는 타이밍에 캡처된 값을 복사하여 람다 내부에 저장하여 사용하기에 캡처한 값의 소멸여부와 상관없이 캡처한 값의 사용이 가능해진다.
📍캡처 절(리스트)의 종류 :
C++에서 네 가지 유형의 캡처 절을 지원한다.
- 값에 의한 캡처 Value capture : 변수를 복사하여 람다 표현식 내부에 저장
- 참조에 의한 캡처 Reference Capture : 변수를 참조로 캡처하여 람다 표현식 내부에 사용
- 모든 변수를 값에 의해 캡처 Capture all by Value : 모든 외부 변수를 복사하여 람다 표현식 내부에 저장
- 모든 변수를 참조에 의해 캡처 Capture all by Reference : 모든 외부 변수를 참조로 캡처하여 람다 표현식 내부에서 사용
int a = 1;
int b = 2;
// value capture
auto valueLambda = [a]() { std::cout << "Value : " << a << std::endl; };
valueLambda();
// refer capture
auto referLambda = [&a]() { std::cout << "Refer : " << a << std::endl; };
referLambda();
// capture all by value
auto allValueLambda = [=]() { std::cout << "All Value : " << a << ", " << b << std::endl; };
allValueLambda();
// capture all by refer
auto allReferLambda = [&]() { std::cout << "All Value : " << a << ", " << b << std::endl; };
allReferLambda();
아래의 예제를 통해 값에 의한 캡처의 경우 외부의 값을 복사하여 람다식 내부에 저장한 값을 사용하기에 복사 이후에 외부에서 값을 변경하더라도 영향을 받지 않는것을 알 수 있다.
int a = 0;
auto valueLambda = [=]() { std::cout << a << std::endl; };
auto referLambda = [&]() { std::cout << a << std::endl; };
valueLambda(); // 0
referLambda(); // 0
std::cout << a << std::endl; // 0
a += 1;
valueLambda(); // 0
referLambda(); // 1
std::cout << a << std::endl; // 1
[ ✔️람다 식의 활용 ]
람다는 클로저를 제공하여 상위 스코프의 변수를 캡처하고, 이를 통해 더 복잡한 연산을 수행하는 데 도움이 된다. 또한, 익명성과 짧은 수명은 로컬 연산에 이상적이며, 이를 통해 코드의 복잡성을 줄이고 의도를 명확하게 표현할 수 있다.
📍함수 인수(Argument)로서의 람다 & STL 알고리즘 :
std::sort의 세번째 인수로 넣어줌으로써 두 값을 비교하여 람다 식에 정의한 순서로 배열의 인자들을 정렬할 수 있다.
#include <iostream>
#include <vector>
#include <algorithm>
std::vector<int> vec = { 3,5,34,7,42,1 };
std::sort(vec.begin(), vec.end(), [](auto a, auto b) { return a < b; });
std::for_each(vec.begin(), vec.end(), [](auto val) {std::cout << val << " "; });
// 1 3 5 7 34 42
📍반환 값으로서의 람다 :
함수의 반환 값으로 표현식을 사용하며, 함수가 다른 함수를 생성하고 반환하는 팩토리 함수를 작성할 수 있다.
이런 방식은 동적인 동작을 필요로 하는 경우 유용하게 사용될 수 있다.
auto makeMultiplier(int x)
{
return [x](int y) {
return x + y;
};
}
int main()
{
auto multiplier = makeMultiplier(1);
std::cout << multiplier(2) << std::endl; // 3
return 0;
}
📍람다를 변수에 할당하기 :
람다 표현식의 강력한 특징 중 하나는 람다 함수를 변수에 할당할 수 있다. 이를 통해 동적으로 생성된 함수를 저장하고, 코드의 다른 부분에서 재사용할 수 있다.
int main()
{
auto printHello = []() {
std::cout << "Hello World" << std::endl;
};
printHello(); // Hello World
return 0;
}
[ ✔️람다 식의 고급 사용 ]
C++14 이후로 람다 식에 auto 키워드를 사용할 수 잇게 되었다. 이를 통해 Generic Lambda 를 작성할 수 있다.
또한, 람다 캡처에 대해 더 깊게 배울 수 있다. 예를 들어 [=]는 모든 변수를 값으로 캡처하고, [&] 는 모든 변수를 참조로 캡처한다. 특정 변수만 캡처하려면, [x, &y]와 같이 작성할 수 있다.
📍뮤테이블 람다 :
뮤테이블 람다는 캡처한 값을 람다 함수 내에서 변경할 수 있도록 한다.
기본적으로 람다 표현식에서 캡처한 값을 변경하는 것은 허용되지 않는다.
위 코드는 mutable 키워드를 사용하면 값으로 캡처한 값을 람다 식 내에서 변경할 수 있게된다.
int val = 0;
auto lambda = [val]() mutable {
val += 1;
std::cout << val << std::endl;
};
lambda(); // 1
std::cout << val << std::endl; // 0
mutable 키워드는 람다 함수 내부에서만 캡처한 값을 변경할 수 있게 해준다.
즉, 람다 함수 내에서 캡처한 값을 변경하더라도 외부에는 영향이 가질 않는다는 의미다.
뮤테이블 람다를 사용할 때는 람다 함수의 순수성(purity)을 포기하게 되기에, 사이드 이펙트에 주으이해야한다.
📍람다 내부에서의 예외 처리 :
C++에서 예외 처리는 try-catch, throw 키워드를 사용해서 처리한다.
람다 함수 내에서도 동일하게 예외 처리를 할 수 있다.
int main()
{
auto divide = [](double a, double b) {
if (b == 0.f)
{
throw std::invalid_argument("Divisor can't be zero");
}
return a / b;
};
try
{
double result = divide(5.f, 0.f);
}
catch (const std::invalid_argument& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
이처럼 람다 함수 내에서도 예외를 던지고 처리할 수 있지만, 가독성이 떨어질 수 있다. 때문에 가능한 람다 식은 간단하게 유지하는게 좋다.
📍람다와 템플릿 :
C++의 템플릿 기능은 재사용 가능한 코드를 작성하는데 있어 매우 유용한다. 람다 표현식을 템플릿과 결합하면, 타입에 대해 중립적인 범용 코드를 작성할 수 있다.
#include <iostream>
#include <vector>
#include <algorithm>
template<class T>
void printVector(const std::vector<T>& vec)
{
std::for_each(vec.begin(), vec.end(), [](const T& val) {
std::cout << val << " ";
});
std::cout << std::endl;
}
int main()
{
std::vector<int> vecInt = { 1,2,3,4 };
printVector(vecInt); // 1 2 3 4
std::vector<std::string> vecStr = { "Hello", "World" };
printVector(vecStr); // Hello World
return 0;
}
📍재귀 람다 :
"재귀 람다"는 람다 식이 자기 자신을 호출하는 것을 의미한다. 이는 특히, "분할 정보 알고리즘"에서 유용하게 사용된다.
람다 식은 선언되기 전에는 이름이 없으므로, 자신을 직접 참조할 수 없다. 이 문제를 해결하기 위해, 람다를 자신이 포함된 변수에 할당하고 그 변수를 람다의 캡처 목록에 포함시킨다.
그럼 이 변수를 통해 람다가 자신을 참조할 수 있게 된다.
#include <iostream>
#include <functional>
int main()
{
std::function<int(int)> factorial = [&factorial](int i) {
return i > 1 ? i * factorial(i - 1) : 1;
};
std::cout << "5! = " << factorial(5) << std::endl; // 5! = 120
return 0;
}
================================
개인 공부 기록용 포스팅입니다.
댓글, 질문 환영해요~!
Unreal Engine Ver : 5.1.1
Refer : Inflearn_이득우의 언리얼 프로그램
================================