디지털 블리자드
2022. 8. 26. 02:13
출처 : https://blog.koriel.kr/modern-cpp-lambdayi-teugjinggwa-sayongbeob/
위의 블로그의 정보를 좀더 간략히 정리하였습니다.
lambda는 람다 표현식 또는 람다 함수, 익명 함수(anonymous function)로 불립니다. 그 성질은 함수 객체(functor)와 동일합니다. 람다 함수는 이름이 없는 함수입니다. lambda로 생성된 함수 객체는 타입을 가지고 있긴 하지만 decltype이나 sizeof를 사용할 수 없습니다.
|
1. 함수 객체와 lambda C++ 프로그래밍을 하다 보면 함수 포인터나 함수 객체가 필요한 시점이 있습니다. 함수 포인터는 상태를 가질 수 없기 때문에 상태를 가져야하는 함수가 필요할 땐 함수 객체를 사용합니다. 둘 다 장단점이 있습니다. 함수 객체의 가장 큰 단점은 번거롭다는 것입니다. 함수 객체를 정의할 땐 클래스나 구조체를 정의해야 합니다. 코드도 길어지고 보기도 좋지 않습니다.
lambda는 함수 포인터와 함수 객체의 장점만 골라 가졌다고 볼 수 있습니다. 정의하기 위해서 클래스나 구조체를 정의할 필요가 없고가 상태도 가질 수 있습니다.
[&sum](int& number) { sum += number; });
합계를 저장할 변수를 참조자로 전달받아 거기에 값을 누적시키는 람다함수입니다. 함수 객체로 구현할 경우 번거롭게 구조체를 먼저 정의한 후 사용해야 합니다. 하지만 lambda는 단 몇줄만으로 정의가 끝났습니다.
|
2. lambda의 사용법
2-1. lambda의 문법 lambda의 문법은 크게 캡처(capture), 인자(parameter), 반환형(return type), 몸통(body)로 이루어져 있습니다.
[captures](parameters) -> return type { body }
* captures: comma(,)로 구분된 캡처들이 들어갑니다. * parameters: 함수의 인자들이 들어갑니다. * return type: 함수의 반환형입니다. * body: 함수의 몸통입니다.
캡처 부분을 재외하면 람다 함수는 그냥 함수와 동일합니다. 캡처는 lambda에서 사용할 변수나 상수들을 미리 지정하여 찍어오는 것입니다. 참조하여 가져올 수도 있고 복사하여 가져올 수도 있습니다.
[]() { std::cout << "Hello World!" << std::endl; }();
2-2. lambda의 parameter [](std::string name) { std::cout << "My name is " << name << std::endl; }("Hello World!");
2-3. lambda를 std::function에 대입 lambda는 std::function에 대입이 가능합니다. auto 키워드로 타입 추정도 물론 가능합니다.
auto introduce = [](std::string name) { std::cout << "My name is " << name << std::endl; }; introduce("Hello World!");
2-4. lambda를 함수의 파라미터로 사용 lambda를 std::function에 대입할 수 있다는 것은 그것을 함수의 파라미터로 전달할 수도 있습니다. 하지만 전달되는 함수의 반환형과 그 파라미터의 타입이 항상 같지 않을 수 있으므로 주로 템플릿 함수의 파라미터로 전달됩니다.
template<typename T> void templateFunc(T func) { func();}
auto foo = []() { std::cout << "Hello World!" << std::endl; }; templateFunc(foo);
2-5. lambda를 반환하는 함수 함수가 lambda를 반환하는 것도 가능합니다.
std::function<void (void)> getLambda() { return []() { std::cout << "Hello World!" << std::endl; };} vodi main() { auto foo = getLambda(); foo(); }
2-6. lambda를 STL container에 저장
std::vector<std::function<void(void)>> funcs; funcs.push_back([]() { std::cout << "Hello" << std::endl; }); funcs.push_back([]() { std::cout << "World!" << std::endl; }); for (auto& func : funcs) {func();}
2-7. lambda의 반환형 명시 lambda는 값을 반환할 수 있고 그 반환형을 명시할 수 있습니다. 함수 포인터나 함수 객체와 다르게 반환형의 추론도 가능합니다.
// 은연중의 반환 형식 (double) auto foo = [] { return 3.14; }; // 명백한 반환 형식 (float) auto bar = []() -> float { return 3.14f; }; // warning. double에서 float으로 암시적 형변환. float x = foo(); // 같은 자료형 float y = bar();
|
3. lambda의 캡처 lambda 외부에 정의되어 있는 변수나 상수를 lambda 내부에서 사용하기 위해서 캡처를 사용합니다. 참조(reference)와 복사(copy) 두 방식을 사용할 수 있고, 참조는 &var로 복사는 var로 합니다. 그리고 캡처는 처음에 보았듯이 문법 요소 중 가장 처음에 나오는 []에 기술합니다.
3-1. 참조로 캡처 int sum = 0; // sum을 참조로 캡처 [&sum](int& number) {sum += number;};
3-2. 복사로 캡처 std::string name = "Hello World!"; [name]() { std::cout << "My name is " << name << std::endl; }();
주의해야할 점은 복사로 캡처된 변수는 body에서 값을 변경을 할 수 없습니다.
int sum = 0; //sum을 복사로 캡처 //sum 값을 직접적으로 변경 불가능합니다. (C++에 빨간줄 뜨면서 오류 납니다.) => 기본값 constexpr 상수로 복사됨 [sum](int& number) { sum += number;};
lambda의 문법 요소 중 지정자(specifier)라는 것이 있습니다. 이 지정자를 이용해서 캡처된 변수를 몸통안에서 어떻게 쓸 것인지 지정할 수 있습니다. 대표적으로 mutable과 constexpr이 있습니다. mutable은 복사로 캡처된 변수를 몸통안에서 수정될 수 있도록 허용하는 지정자이고 constexpr은 함수 호출 연산자(function call operator)가 상수 식인 것을 명시하는 지정자입니다. 지정자에 아무 것도 기술하지 않으면 constexpr이 기본값으로 들어갑니다.
int sum = 0; // sum을 복사로 캡처. mutable 지정자 [sum](int& number) mutable {sum += number;};
3-3. 여러개의 변수나 상수 캡처 여러개의 변수나 상수를 한번에 캡처할 수 있습니다. []안에 여러 캡처들을 comma(,)로 구분하여 기술하면 됩니다.
int x = 0; char y = 'J'; double z = 3.14; std::string w = "Hello World!"; // x, y는 참조로, z, w는 복사로 캡처. auto foo = [&x, &y, z, w]() {};
[a,&b] : a를 복사로 캡처, b를 참조로 캡처합니다. [this] : 현재 객체를 참조로 캡처합니다. [&] : [&]의 의미는 람다 함수 앞단에 모든(All) 외부 변수를 참조 타입(Reference Type)으로 잡아(Capture) 사용하겠다는 의미입니다. [=] : [=]는 모든 외부 변수를 상수값(const)으로 값 자체를 복사(Copy)하여 사용하겠다는 의미입니다. [] : 아무것도 캡처하지 않습니다.
[&]나 [=]는 lambda가 정의되어있는 함수를 넘어서서 전역(global) 범위까지가 그 캡처 범위입니다. [&, a] 이렇게 하면 모든 변수나 상수를 캡처하고 a만 예외적으로 복사로 캡처하는 것입니다. [=, &b]도 마찬가지로 모든 변수나 상수를 캡처하고 b만 예외적으로 참조로 캡처하는 것입니다.
int a, b, c; [&, a, b]() {}(); [=, &c]() {}();
3-4. 전역 변수 캡처 전역 변수를 아래와 같이 캡처하면 오류가 발생합니다.
int sum = 0; int main() { [&sum](int& number) { sum += number; }); return 0;}
전역 변수를 캡처하기 위해서는 기본 캡처 모드(capture-default)를 사용해야 합니다.
int sum = 0; int main() { // 참조 방식의 기본 캡처 모드 [&](int& number) { sum += number; }); return 0;}
3-5. 클래스 멤버 함수 속 lambda 클래스 멤버 함수안에서 lambda를 정의하면 [this]로 현재 객체를 참조로 캡처할 수 있습니다. 이때 lambda는 friend 함수이므로 현재 객체의 private 멤버에도 접근할 수 있습니다.
#include <iostream> #include <string>
class Person { public: Person(std::string name) : name(name) {} void introduce() { [this]() { std::cout << "My name is " << name << std::endl; }(); } private: std::string name; };
int main() { Person* devkoriel = new Person("Hello World!"); devkoriel->introduce(); return 0; }
클래스 멤버 함수안에서 정의되는 것뿐만 아니라 lambda 자체가 멤버로 선언될 수도 있습니다. 이럴 경우 [this]로 현재 객체를 참조로 캡처할 수 있는 것도 마찬가지입니다.
3-6. lambda 재귀 lambda는 기본적으로 함수이기 때문에 재귀도 가능합니다. 대입된 std::function 함수를 참조로 캡처한 후 람다함수 정의부에서 호출하면 됩니다.
#include <iostream> #include <functional>
int main() { std::function<int (int)> factorial = [&factorial](int x) -> int { return x <= 1 ? 1 : x * factorial(x - 1); }; std::cout << "factorial(5): " << factorial(5) << std::endl; }
주의해야할 점은 lambda를 대입시킬 함수의 타입을 auto 키워드로 추론하면 안됩니다. auto 키워드를 쓰면 컴파일러가 타입을 추론하게 되는데, 타입이 제대로 추론되기도 전에 lambda 몸통에서 그 함수를 재귀적으로 호출하고 있기 때문입니다. lambda 재귀를 사용할때는 반드시 대입시킬 함수의 타입을 명시하여야 합니다.
|
lambda는 익명 함수(anonymous function)이고 함수 객체를 생성합니다. lambda는 함수 포인터와 함수 객체의 장점을 모두 가지고 있습니다. 기본 캡처 모드(capture-default), 복사, 참조를 통해 변수나 상수를 캡처할 수 있습니다. 함수에서 반환할 수도 있고 함수의 파라미터로 전달할 수도 있있습니다. 클래스 멤버 함수안에서 정의되는 lambda는 [this]로 현재 객체를 참조로 캡처할 수 있습니다. 재귀 호출이 가능합니다.
|
출처 : https://boycoding.tistory.com/233 위의 블로그의 정보를 좀더 간략히 정리하였습니다.
함수 포인터 (function pointer) 함수 포인터(function pointer)는 함수를 가리키는 변수입니다. 즉, 함수의 주소를 저장하는 변수입니다.
①int ②(*ptrSum)③(int a,int b) 일반 포인터와 마찬가지로 주소를 가리킬때는 *을 사용해서 포인터라고 알려줍니다. ①은 함수의 반환형을 의미합니다. ②는 함수포인터의 이름을 의미합니다. (변수명과 같이 임의로 정해줍니다.) ③은 매개변수를 의미합니다. 매개변수가 없을 때는 빈 괄호나 void를 사용합니다.
int foo(){ return 5;} 식별자 foo는 함수의 이름입니다. 함수는 고유한 l-value 함수 타입입니다. 위 예제의 경우 정수(int)를 반환하고 매개 변수를 받지 않는 함수 타입입니다. 변수와 마찬가지로 함수는 메모리의 할당된 주소에 있습니다. () 연산자를 통해 함수를 호출하면, 호출되는 함수의 주소로 점프하여 실행합니다.
int foo(){ return 5; }
int main() { foo(); return 0; }
함수에 대한 포인터 (pointer to function) 비 상수 함수 포인터(non-const function pointer) 생성하는 문법은 C++에서 볼 수 있는 문법 중 하나입니다.
// fcnPtr 는 인수가 없고 정수를 반환하는 함수에 대한 포인터입니다. int (*fcnPtr)();
위 코드에서 fcnPtr은 인수가 없고 정수를 반환하는 함수에 대한 포인터입니다. 그러므로 같은 타입의 함수를 가리킬 수 있습니다. 괄호가 필요한 이유는int* fcnPtr()과 같은 코드는 정수에 대한 포인터를 반환하는 인수가 없는 함수 fcnPtr의 전방 선언으로 해석되기 때문에 우선순위를 위해서입니다.
상수 함수 포인터를 만들기 위해서는 * 뒤에 const 키워드를 사용하면 됩니다. int (*const fcnPtr)();
만약 int 앞에 const가 오면 함수가 const int를 반환한다는 것을 의미합니다.
함수 포인터에 함수 할당 함수 포인터는 함수로 초기화 할 수 있다.
int foo(){ return 5; } int goo(){ return 6; } int main() { int (*fcnPtr)() = foo; // fcnPtr points to function foo fcnPtr = goo; // fcnPtr now points to function goo return 0; }
C++는 기본 자료형과 달리 필요할 경우 함수를 함수 포인터로 암묵적으로 변환하므로 주소 연산자 &를 사용할 필요가 없습니다.
함수 포인터로 실제 함수를 호출할 수 있습니다. 호출하는 두 가지 방법이 있는데, 첫 번째는 명시적인 역참조 방법입니다.
int foo(int x){ return x; }
int main() { int (*fcnPtr)(int) = foo; // fcnPtr에 foo함수를 할당한다. (*fcnPtr)(5); // foo(5)를 fcnPtr로 호출한다. return 0; }
두 번째 방법은 암시적 역참조를 통한 방법입니다. fcnPtr(5); // foo(5)를 fcnPtr로 호출한다.
일반적인 함수 이름은 함수의 포인터가 되기 때문에 암시적 추론을 통한 역참조 방법은 일반 함수 호출과 똑같이 생겼습니다. 현대 컴파일러는 대부분 이 방법을 지원합니다.
C++ 11에서의 std::function C++ 11에서는 표준 라이브러리 <function> 헤더에 정의되어 있는 std::function을 이용해서 함수 포인터를 정의할 수 있습니다. #include <functional> bool validate(int x, int y, std::function<bool(int, int)> fcn); //std::function<반환타입(인수..)> 반환 타입과 매개 변수는 꺾쇠(<>)안에 들어 있습니다. 매개 변수를 꺾쇠 속 괄호 안에 둠으로써 좀 더 명시적으로 읽을 수 있습니다. 매개 변수가 없다면 괄호 안의 내용을 비워두면 됩니다.
#include <functional> #include <iostream> int foo(){ return 5;} int goo(){ return 6;} int main() { std::function<int()> fcnPtr; // int를 반환하고 매개 변수가 없는 함수 포인터 변수 fctPtr 선언 fcnPtr = goo; // fcnPtr은 함수 goo를 가리킨다. std::cout << fcnPtr(); // 일반적인 함수처럼 함수를 호출할 수 있다. return 0; }
|
thiscall 호출 규약은 호출원과 함수간의 약속이므로 양쪽이 다른 형태로 약속을 할 수도 있는 것입니다.
호출 규약 : thiscall 인수 전달 : 오른쪽 먼저, this 포인터는 ecx 레지스터로 전달됩니다. 스택 정리 : 함수 이름 규칙 : C++ 이름 규칙을 따릅니다.
thiscall은 클래스의 멤버 함수에 대해서만 적용되는데 ecx로 객체의 포인터(this)가 전달된다는 것이 특징이며 나머지 규칙은 __stdcall과 동일합니다. 예외적으로 가변 인수를 사용하는 멤버 함수는 __cdecl로 작성되며 이때 this는 스택의 제일 마지막에(그러므로 첫 번째 인수로) 전달됩니다. 이 호출 규약은 컴파일러가 멤버 함수에 대해서만 특별히 적용하는 것이므로 일반 함수에는 이 호출 규약을 적용할 수 없습니다. thiscall은 이 호출 규약의 이름일 뿐 키워드가 아니기 때문에 함수 원형에 thiscall이라고 쓸 수도 없습니다. 멤버 함수이기만 하면 컴파일러가 알아서 thiscall 호출 규약을 적용합니다.
|
출처
https://skmagic.tistory.com/117