과제(숙제)

2022.08.25 과제

디지털 블리자드 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://blog.koriel.kr/modern-cpp-lambdayi-teugjinggwa-sayongbeob/

 

출처 : 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;
}


출처
https://boycoding.tistory.com/233
https://reakwon.tistory.com/17


thiscall
호출 규약은 호출원과 함수간의 약속이므로 양쪽이 다른 형태로 약속을 할 수도 있는 것입니다.

호출 규약 : thiscall
인수 전달 : 오른쪽 먼저, this 포인터는 ecx 레지스터로 전달됩니다.
스택 정리 : 함수
이름 규칙 : C++ 이름 규칙을 따릅니다.

thiscall은 클래스의 멤버 함수에 대해서만 적용되는데 ecx로 객체의 포인터(this)가 전달된다는 것이 특징이며 나머지 규칙은 __stdcall과 동일합니다.
예외적으로 가변 인수를 사용하는 멤버 함수는 __cdecl로 작성되며 이때 this는 스택의 제일 마지막에(그러므로 첫 번째 인수로) 전달됩니다.
이 호출 규약은 컴파일러가 멤버 함수에 대해서만 특별히 적용하는 것이므로 일반 함수에는 이 호출 규약을 적용할 수 없습니다.
thiscall은 이 호출 규약의 이름일 뿐 키워드가 아니기 때문에 함수 원형에 thiscall이라고 쓸 수도 없습니다.
멤버 함수이기만 하면 컴파일러가 알아서 thiscall 호출 규약을 적용합니다.

 

출처
https://skmagic.tistory.com/117