C++ - Lambda

mohadang·2022년 10월 14일
0

C++

목록 보기
30/48
post-thumbnail

Lambda

  • 다른 언어에 많이 포함되어 있음
  • 잘못 사용할 경우 가독성을 해침
  • 이름이 없는 함수 개체(언어의 시점에서 메모리를 잡아 먹는것, 클래스의 개체 아님...)
  • 내포(nested) 되는 함수
  • Ex) 이전의 벡터 정렬하기
//1)
struct Comparer
{
  bool operator()(float a, float b)
  {
    return (a > b);
  }
}
std::sort(scores.begin(), scores.end(), comparer);
//2)
bool Sort(float a, float b)// 남에게 공유하지 않을 코드라면 안 보이게 하는 것이 좋다.
{
  return (a > b);
}
std::sort(scores.begin(), scores.end(), Sort);

// 위와 같은 코드는 유지보수 하기 힘듬, Sort 내용이 바뀌면....
  • Ex) 새로운 벡터 정렬하기
// 람다식 이용
std::sort(scores.begin(), scores.end(), [](float a, float b){return (a > b)})

람다식

[<captures>](<parameters>) <specifiers> -> <return_type>
{
  <body>
}

<captures> : 캡처 블록
<parameters> : 매개변수 목록(선택 사항)
<specifiers> : 지정자 (선택 사항)
<return_type> : 반환 형(선택 사항)

캡처 블록

  • 람다식을 품는 범위(scope)안에 있는 변수를 람다 식에 넘겨줄때 사용

    • [] :
      • 비어 있음. 캡처하지 않음
    • = :
      • 값에 의한 캡처, 모든 외부 변수를 캡처함
      • 람다 식 안에서 수정할 수 없음
    • & :
      • 참조에 의한 캡처, 모든 외부 변수를 캡처함
    • <변수 이름>
      • 특정 변수를 값으로 캡처
      • 람다 식 안에서 수정할 수 없음
    • &<변수 이름>
      • 특정 변수를 참조로 캡처
  • 쓸데 없이 중복해서 캡처하면 컴파일 에러

int w = 1;
int x = 10;
int y = 11;
int z = 12;

auto magic = [=, z]() // Error, z는 = 때문에 필요 없다.
{
  return w + x + y + z;
};

Ex)

1)
auto noCapture = [](){std::cout << "No capturing" << std::endl;}
noCapture();
// noCapture를 다른 곳으로 넘길 수 있어서 의미 있음.

2) 바로 실행
[] {std::cout << "No capturing" << std::endl;}();
// 그다지 의미 없음, 이건 람다 안 쓰더라도 수행 할 수 있음

Ex) 외부 변수 사용하기

float score1 = 80.f;
float score2 = 20.f;

auto max = [](){return score1 > score2 ? score1 : score2;};
// 컴파일 에러 발생, 캡처를 아무것도 안 했으니까...

std::cout << "Max Value is " << max() << std::endl;

// 위 컴파일 에러 해결하기 위해 값에 의한 캡처 수행 하면 됨

auto max = [=](){return score1 > score2 ? score1 : score2;};
// 위에 있는 모든 변수를 값에 의한 캡처를 하여라

Ex) 값에 의한 캡처의 한계

float score1 = 80.f;
float score2 = 20.f;

auto changeValue = [=]()
{
  score1 = 100.f;
  // 컴파일 에러 발생, score1을 수정 할 수 없음, 
  // 값 복사를 해서 캡처한 것이니까...
};

changeValue();

Ex) 참조에 의한 캡처

float score1 = 80.f;
float score2 = 20.f;

// "왠지 여기를 보면 이 코드가 실행 되는것 같아서 혼란... 람다를 남발하면 가독성에 안좋다"
auto changeValue = [&]() 
{
  score1 = 100.f;// 참조에 의한 캡처라 값을 수정할 수 있고 컴파일 할 수 있음.
  score2 = 100.f;
};

// "이런 캡처 방식은 코드의 가독성에 별로 좋지 않다, changeValue에서 무었을 하는지 확인 하기 위해 코드를 역행해서 봐야 한다."
changeValue();

Ex) 특정 변수만 캡처

float score1 = 80.f;
float score2 = 20.f;

1)
auto changeValue = [score1]()// score1만 값 캡처
{
  // 컴파일 에러, score2에 접근 할 수 없음
  std::cout << score1 <<" / " << score2 << std::endl;
};
changeValue();

2)
// score1, score2 이렇게 2개를 캡처 할 수 있음
auto changeValue = [score1, score2]()
{
  std::cout << score1 <<" / " << score2 << std::endl;
};
changeValue();

Ex) 참조에 의한 특정 변수만 캡처

float score1 = 80.f;
float score2 = 20.f;

auto changeValue = [&score1]()// score1만 값 캡처
{
  score1 = 100.f;
  // score2는 접근 불가능...
};
changeValue();

Ex) 캡처 옵션 섞기

float score1 = 80.f;
float score2 = 20.f;

// 모든 값을 값 캡처하되 score1만 참조 캡처를 한다.
auto changeValue = [=, &score1]() 
{
  score1 = 100.f;
  std::cout << score2 << std::endl;
};
changeValue();

매개 변수 목록

  • 선택 사항
  • 빈 괄호를 생략 할 수 있음
  auto message = [] {std::cout << "Let's go" << std::endl;};
  • 그러나 별로 좋은 방법은 아닌 것 같다

Ex)

int score1 = 70;
int score2 = 85;

auto add = [](int a, int b) // 매개 변수
{
    return a + b;
}

std::cout << add(score1, score2) << std::endl;

Ex) 정렬 하기

std::vector<float> scores;

scores.push_back(50.f);
scores.push_back(88.f);
scores.push_back(70.f);

std::sort(scores.begin(), scores.end(), [](float a, float b){return (a > b;)});
std::sort(scores.begin(), scores.end(), [](float a, float b){return (a <= b;)});
  • 정렬 같은 알고리즘 함수를 쓸데는 정말 유용하다.
  • 따로 일회용 구조체나 함수를 만들 필요가 없기 때문이다.
  • 깔끔하게 사용할 부분에만 사용하고 어떤 함수자를 쓰는지 확인하기도 쉽다.

지정자

  • 선택 사항
  • mutable
    • 값에 의해 캡처된 개체를 수정할 수 있게 함
    • 괜찮은 언어 디자인, 허나 C++에 너무 늦게 들어옴
      - 기본적으로는 람다식에서 외부값을 못 바꾸도록 하고 바꾸고 싶다면 지정 하는 것이 실수를 줄일 수 있는 좋은 디자인이다.
      - Rust : 기본적으로 모든 변수가 상수여서 바꿀 수 없다, 바꾸고 싶다면 mutable을 변수 앞에 붙여야 한다.
      Ex)
// 1)
int value = 100;
auto foo = [value]()
{
  std::cout << ++value << std::endl;// 컴파일 에러
}

foo();

// 2)
int value = 100;
auto foo = [value]() mutable
{
  std::cout << ++value << std::endl;// 에러는 발생 안 하지만 원본이 바뀌지는 않는다.
}

foo();

this

struct S { void f(int i); };
void S::f(int i) {
    [&, i]{};      // OK
    [&, &i]{};     // ERROR: i preceded by & when & is the default
    [=, this]{};   // ERROR: this when = is the default
    [=, *this]{ }; // OK: captures this by value. See below.
    [i, i]{};      // ERROR: i repeated
}
  • this 를 캡처하면 클래스 안에 있는 모든 변수에 접근 가능
  • Ex)
class Temp
{
public:
    Temp(int ii) : i{ ii } { }

    int GetNumber() const
    {
        auto getNumber = [this] { return i; };
        return getNumber();
    }
private:
    int i;
};

가변 인자 ...

template <typename... Arg>
void Algorithm(int i, Arg... v)
{
  auto helper = [&i, &v...] { return i * (h1(v...) + h2(v...)); };
  // do something
}
-

람다 가리키기

auto lambda = [](int x, int y) { return x + y; };
int (*p)(int, int) = lambda;
std::function<int(int, int)> fp = lambda;

반환형

  • 선택 사항
  • 반환 형을 적지 않으면 반환문을 통해 유추해줌
    • 그러나 별로 좋지 않은 방식. 코드 프로토타입만 보고 유추를 하는데 방해...
  • 으엑, 자동 반환 형식 :(

람다 식의 장점

  • 간단한 함수를 빠르게 작성 할 수 있음
    • auto max = [](float a, float b){return a > b ? a : b;};
  • 유지 보수 안 해도 됨.
  • 다른 곳에 노출 안 시켜도 됨.
  • 람다 식의 단점
    • 디버깅하기 힘들어짐
      • 콜 스택에 이름이 안 뜸...
    • 함수 재사용성이 낮음
    • 사람들은 보통 함수를 새로 만들기 전에 클래스에 있는 기존 함수를 찾아 봄
    • 람다 함수는 눈에 잘 띄지 않아서 못 찾을 가능성이 높음
    • 그럼 코드 중복이 발생
- 재사용이 필요할 때 람다를 함수로 바꾸면 되는 것 아닌가???
  - 좋은 말이지만 현실적으로는 불가능. 다른 사람이 내가 만든 람다를 1000줄 라인에서 찾기 힘들다.
  - IDE의 도움으로도 어떤 람다가 있는지 확인하기 힘들다. 함수 이름이 없어서 문자열 검색 힘듬.
  - 함수가 아니기에 함수 내역으로도 안 떠짐.

베스트 프랙티스

  • 기본적으로, 이름 있는 함수를 쓰자(전통적 방식, 재활용 할 것 같은 함수)
  • 자잘한 함수는 람다로 써도 괜찮음(한 줄짜리 함수, 재활용 안 할것 같은 함수)
  • 허나 재사용 할 수 있는 함수를 찾기 좀 어려움
  • 정렬 함수 처럼 STL 컨테이너에 매개변수로 전달할 함수들도 좋은 후보
    • qsort()에 함수 포인터 넘겨줘야 했었는데 람다가 좀더 확실한 방법.
    • 예전에 함수 포인터 쓰여진 곳

가변인자(Variadic) 템플릿

  • make_unique, make_shared

  • 생각보다 별로 쓸 곳은 없음

  • 가변 인자...

    • printf("%d %d %d", data1, data2, data3); <== 대표적인 예
    • int printf(const char* format, ...);
    • 템플릿 프로그래밍에서도 똑같이 할 수 있음.
  • 다양한 매개변수 개수와 자료형을 지원하는 클래스 또는 함수 템플릿

  • 매개변수 목록에서 생략 부호(...)를 씀.

  • 가변 인자 템플릿 클래스

template<typename ... Arguments>
clas <클래스 >
{

};

<typename ... Arguments>
// 템플릿 매개변수 꾸러미
  • 가변 인자 템플릿 함수
template <typename... Argements>
<반환형> <함수명>(Arguments... args)
{

}

Ex) 가변 인자 템플릿 사용 예

// std::make_unique()
std::unique_ptr<Cat> myCat = std::make_unique<Cat>("Lulu", 2);// 다양한 인자
std::unique_ptr<Cat> myCat = std::make_unique<Vec2>(10.f, 15.f);// 다양한 인자
std::unique_ptr<Cat> myCat = std::make_unique<Cat>(20.f, 1.f, 0.f);// 다양한 인자

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// std::shared_ptr
template<typename T, typename... Args>
std::shared_ptr<T> make_shared(Args&&... args)
{
    RefCountObject<T> *ref = new RefCountObject<T>(std::forward<Args>(args)...);
    std::shared_ptr<T> returnVal = returnVal.set(ref);
    return returnVal;
}
  • 가변 인자 템플릿 활용

    • 활용법을 생각하기가 정말로 어려움
    • 주로 인자 전달용. 예) std::make_unique()
    • 몇몇 다른 아이디어들도 있음. 허나 대개 실용적이라는 생각이 안 듦
      • 구글 검색 : "practical uses for variadic templates"
  • Ex)

class Foo
{
public:
    Foo() = delete;
    Foo(float f1);
    Foo(float f1, float f2);
    ~Foo() = default;
};

template<typename T, typename... TArgs>
T* Create(TArgs... args)
{
    cout << "Creating instance" << endl;
    T* object = new T(args...);
    return object;
}

Foo* foo1 = Create<Foo>(1.0f);
Foo* foo2 = Create<Foo>(1.0f, 2.0f);

// Foo에 인자 3개받는 생성자 없어서 Error
Foo* foo2 = Create<Foo>(1.0f, 2.0f, 3.0f);
profile
mohadang

0개의 댓글