[기술 질문] #17_Lambda
C++의 Lambda에 대해 알아보겠습니다.
Overview
- 개념
- 캡처 블록
- 람다의 활용
- LinQ
#0. 개념
1. 람다 표현식
[capture list] (parameter list) -> return type { function body }
Details
- [정의] : C++의 람다 표현식은 익명의 함수 객체를 인라인으로 정의할 수 있도록 해주는 기능입니다.
- [구성] : 람다 표현식은 대괄호로 시작하는 캡처 리스트, 중괄호로 시작하는 매개변수 리스트를 작성하고, 화살표로 구분되는 반환 타입 및 함수 바디를 작성합니다.
2. 기본 예제-1
#include <iostream>
using namespace std;
int main()
{
auto Sum = [](int a, int b) -> int { return a+b; };
}
Details
- 대괄호를 작성해 캡처 리스트를 작성합니다. 위 코드는 어떠한 변수도 캡처하고 있지 않습니다.
- 중괄호를 작성해 파라미터 리스트를 작성합니다.
- 화살표로 구분하여 함수 객체의 반환 타입과 함수 바디를 작성합니다.
- 결과적으로, Sum이라는 함수 객체를 람다 표현식을 통해 선언하고 있습니다.
3. 기본 예제-2
#include <iostream>
using namespace std;
int main()
{
// 리턴 타입 X
auto basic1 = [](int a, int b) {return a + b; };
cout << basic1(3, 5) << '\n';
// 리턴 타입 O
auto basic2 = [](int a, int b) -> int {return a + b; };
cout << basic2(3, 5) << endl;
}
Details
- basic1 함수 객체는 화살표와 반환 타입을 생략한 람다 표현식을 통해 생성됩니다.
- basic2 함수 객체는 화살표와 반환 타입을 모두 포함한 람다 표현식을 통해 생성됩니다.
- 결과적으로, 둘은 에러 없이 동일한 기능을 수행합니다.
#1. 캡처 블록
1. 캡처 블록
- [정의] : 람다 표현식의 캡처 블록은 람다 표현식 내부에서 동일한 유효범위 내 외부 변수들을 캡처하는 기능을 제공합니다.
- [활용 방법] : (1) [=], 값에 의한 캡처 : 값에 의한 캡쳐는 외부의 모든 변수를 캡처하는 방법입니다. 람다 함수가 참조된 시점에서 외부 변수의 값을 복사해 내부에서 사용하는 방법으로, 해당 값에 대한 변경은 원본 변수에 영향을 끼치지 않습니다. (2) [&], 참조에 의한 캡쳐 : 참조에 의한 캡쳐는 외부변수들을 참조로 캡쳐합니다. 람다 함수 생성 시 외부 변수를 참조하여 내부에서 사용하는 방법으로, 해당 값에 대한 변경은 원본 변수에 영향을 끼칩니다.
- [변수 초기화] : 캡처 블록 내에서 새로운 변수를 선언하거나, 캡처한 변수를 어떤 종류의 표현식으로도 초기화 할 수 있습니다.
2. 클로저
- [정의] : 클로저는 런 타임에 람다 표현식으로부터 생성된 임시 객체입니다. 클로저는 람다 컨텍스트에서 캡처된 변수들을 저장하고, 내부에서 사용할 수 있도록 합니다.
- [특징] : 헷갈릴 수 있지만, 람다라는 것은 람다 표현식의 준말이고, 그것은 단지 프로그램 소스 코드에서만 존재합니다. 런타임에 람다는 존재하지 않습니다. 런타임에 람다로부터 생성된 임시 인스턴스(객체)가 클로저입니다. 마치, 클래스는 런타임에 존재하지 않지만, 클래스 타입으로 생성된 객체들이 런타임에 존재하는 것과 동일합니다.
3. Mutable
[capture list](parameters) mutable -> return_type { function body }
- [정의] : 람다 표현식에서 캡처된 변수는 함수 객체의 멤버 변수가 되어, 해당 함수 객체의 상태를 유지하는데 사용됩니다. 따라서, 캡처된 변수들은 상태를 변경할 수 없는 상수로 취급됩니다. 하지만, 람다 표현 시 내부에서 해당 변수들을 변경할 필요가 있을 경우, mutable 키워드를 활용해 해당 변수들의 값을 변경할 수 있습니다!
- [캡처 방식에 따른 const 취급] : (1) [=] : const 취급, (2) [&] : non-const 취급
4. [=], 값에 의한 캡처
#include <iostream>
using namespace std;
int main()
{
int a = 5;
int b = 7;
auto lambda = [=](){
cout << a+b << endl;
}
lambda();
}
Details
- [정의] : 캡처 블록 내 =를 작성하면, 람다 함수 바디에서 활용하고자 하는 동일 유효범위 내 주변 변수들을 모두 가져옵니다. 이때, 일반 함수의 인자 전달 방식과 동일하게, =를 통해 캡처한 변수들의 복제본을 람다 함수 내부에서 활용합니다.
- [특징] : 값에 의한 캡처는 람다 함수 내부에서 사용하는 변수들을 constant로 취급합니다. 따라서, 해당 변수들은 람다 함수 내부에서 수정이 불가합니다.
5. [&], 참조에 의한 캡처
#include <iostream>
using namespace std;
int main()
{
int a = 5;
int b = 3;
auto lambda = [&](){
cout << a + b << endl;
a++;
b++;
};
lambda(); // a = 5, b = 3, 8 출력
cout << a+b << endl; // a = 6, b = 4, 10 출력
}
Details
- [정의] : 캡처 블록 내 & 사용은 람다 표현식 내부에서 활용하고자 하는 변수들을 참조를 통해 캡처합니다. 일반 함수와 동일한 방식으로, 참조 형식으로 캡처한 변수들을 람다 함수 내부에서 수정할 경우, 원본 값에 영향을 끼칩니다.
- [특징] : 참조에 의한 캡처 방식은 내부에서 사용할 변수들을 non-const 취급합니다.
6. [=], [&] 함께 사용하는 경우
int x = 42;
int y = 30;
auto lambda = [&x, =](){
cout << x+y << endl;
x++;
};
lambda();
Details
- [정의] : 람다 표현식 캡처 블록 내 값에 의한 캡처와 참조에 의한 캡처를 함께 사용하는 것 또한 가능합니다.
7. 캡처 블록 내 변수 초기화
#include <iostream>
using namespace std;
int main()
{
double pi = 3.1415;
auto lam1 = [myStr = "Pi is : ", pi]() { cout << myStr << pi << endl; };
lam1();
// unique_ptr : 복제 될 수 없는 스마트 포인터
auto myUniquePtr = make_unique<double>(3.1415);
auto lam2 = [p = move(myUniquePtr)](){cout << *p << endl; };
lam2();
}
Details
- [정의] : 람다 표현식의 캡처 블록 내에서 새로운 변수를 선언하거나, 캡처한 변수를 어떤 종류의 표현식으로도 초기화할 수 있습니다.
#2. 람다의 활용
1. std::function
- [정의] : C++ 표준 라이브러리에서 제공하는 std::function은 호출 가능한 어떠한 유형의 객체도 래핑 할 수 있는 템플릿 클래스입니다.
- [특징] : std::function 템플릿 클래스는 동일한 시그니처를 갖는 서로 다른 유형의 객체들을(함수 포인터, 함수 객체, 람다 표현식, C 스타일의 가변 인자 함수) 모두 담을 수 있으며, 기존의 호출 방식을 유지해 일관된 인터페이스와 코드의 유연성을 제공합니다.
2. std::function의 장점
#include <iostream>
#include <functional>
int add(int x, int y) {
return x + y;
}
int main() {
// #1. 함수 포인터
std::function<int(int, int)> func1 = add; // function pointer
// #2-1. 람다 표현식
std::function<int(int)> func2 = [](int x) { return x * x; }; // lambda function
// #2-2. 람다 표현식
std::function<double(double)> func3 = [](double x) { return x / 2.0; }; // lambda function
// #3. 가변 인자 함수, variadic function
std::function<int(int, ...)> func4 = printf;
std::cout << func1(2, 3) << std::endl; // Output: 5
std::cout << func2(4) << std::endl; // Output: 16
std::cout << func3(7.0) << std::endl; // Output: 3.5
f3("%d, %d %d\n", 1, 2, 3); // Output : "1 2 3"
return 0;
}
Details
- [유연성] : std::function은 일반화된 함수 객체 타입으로 다양한 호출 가능한 객체들을 모두 담을 수 있고 일관된 인터페이스를 유지하여 코드의 유연성과 확장성이 높아집니다.
- [타입 안정성] : std::function은 런타임에서 타입 안정성을 보장합니다. std::function에 저장한 호출 가능한 객체들 중 함수 포인터와 같이 그 타입이 컴파일 시점에 결정되지 않고 런 타임에 결정되므로 타입 불일지 오류를 방지할 수 있습니다.
- [다형성] : std::function은 다형성을 지원합니다. 서로 다른 호출 가능한 객체들이 std::function에 저장되어 동일한 인터페이스를 제공하기 때문입니다.
- [안정성] : std::function 템플릿 클래스는 예외 안정성을 보장합니다. std::function 객체는 호출 시 예외를 던질 경우에도 메모리 누수나 예기치 않은 동작이 발생하지 않습니다. 만약, std::function이 호출하는 함수나 함수 객체 내 예외가 발생하면 이 예외를 적절히 처리하여 호출자에게 전달하고, 메모리 리소스를 정리하여 메모리 누수를 방지합니다.
3. 콜백 함수
#include <iostream>
#include <vector>
#include <functional>
using namespace std;
// 람다 표현식을 파라미터로 활용하기위해 "std::function" 래퍼 클래스 활용
void testCallback(const vector<int>& vec, const function<bool(int)>& callbackLam)
{
for (const auto& val : vec)
{
if (callbackLam(val) != false)
break;
else
cout << val << " ";
}
cout << endl;
}
int main()
{
vector<int> v{ 4, 5, 6, 7, 8, 9 };
// testCallback 함수 호출
testCallback(v, [](int i) { return i > 6; });
}
Details
- 람다 표현식은 std::funciton과 함께 콜백 함수를 구현할 수 있습니다.
4. STL 알고리즘과 람다
- 람다 표현식은 STL이 제공하는 알고리즘과 함께 사용할 수 있습니다.
5. STL과 람다 예제-1
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int value = 4;
// count_if() : Predicate과 합치하는 항목의 개수를 반환합니다.
int cnt = count_if(cbegin(v), cend(v), [value](int i) { return i > value; });
cout << "Greater than " << value << " is " << cnt << endl;
}
Details
- STL의 count_if 알고리즘과 람다를 함께 사용한 예제 코드입니다.
- count_if 알고리즘의 세 번째 인자로 Predicate 함수 객체로 람다 표현식을 활용할 수 있습니다.
6. STL과 람다 예제-2
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> v(10);
int val = 1;
generate(begin(v), end(v), [&val] {val *= 2; return val; });
for (const auto &val : v)
cout << val << " ";
}
Details
- 마찬가지로, STL이 제공하는 generate 알고리즘의 Predicate으로 람다 표현식을 활용할 수 있습니다.
#3. LinQ
1. 개념
- .NET Framework에서 지원하는 C#에서 사용하는 언어 통합 쿼리(Launguage Integrated Query)입니다.
- LinQ를 사용하면 객체, DB, XML 문서 등의 데이터를 통합적으로 쿼리하고 조직할 수 있습니다.
- LinQ의 특징은 쿼리를 위한 문법과 데이터 소스를 다루기 위한 API를 제공한다는 점입니다. LinQ를 활용하여 SQL과 비슷한 형태의 쿼리를 작성하고, 다양한 데이터 소스에 대해 실행할 수 있습니다.
- 주요 기능은 그룹화, 필터링, 그리고 정렬 등이 있습니다.
'언어 > 기술 질문' 카테고리의 다른 글
[기술 질문]#18_알고리즘 복잡도 (0) | 2023.03.31 |
---|---|
[기술 질문]#16_RTTI, 런타임 타입 정보 (0) | 2023.03.22 |
[기술 질문]#15_가상 함수 (0) | 2023.03.20 |
[기술 질문]#14_템플릿, Template (0) | 2023.03.12 |
[기술 질문]#13_6가지 디폴트 멤버 메서드 (0) | 2023.03.05 |