함수 - C++ 11에서 람다 식은 무엇입니까?




람다 함수 란 (6)

람다 함수 란 무엇입니까?

람다 함수의 C ++ 개념은 람다 계산법과 함수 프로그래밍에서 시작됩니다. lambda는 재사용이 불가능하고 명명 할 가치가없는 간단한 코드 스 니펫에 유용한 무명 함수입니다 (실제 프로그래밍에서는 이론이 아님).

C ++에서 람다 함수는 다음과 같이 정의됩니다.

[]() { } // barebone lambda

또는 모든 영광으로

[]() mutable -> T { } // T is the return type, still lacking throw()

[] 는 캡처 목록, () 인수 목록 및 {} 함수 본문입니다.

캡처 목록

캡쳐리스트는 람다 외부에서 함수 몸체 내에서 사용할 수 있어야하는 방법과 방법을 정의합니다. 다음 중 하나 일 수 있습니다.

  1. 값 : [x]
  2. 참조 [& x]
  3. 현재 참조 범위 [&]로 범위에있는 모든 변수
  4. 3과 같으나 값으로 [=]

쉼표로 구분 된 목록 [x, &y] 위의 항목 중 하나를 혼합 할 수 있습니다.

인수 목록

인수 목록은 다른 C ++ 함수와 동일합니다.

함수 본문

람다가 실제로 호출 될 때 실행될 코드.

반환 유형 공제

람다에 return 문이 하나만있는 경우 반환 유형을 생략하고 암시 적 유형의 decltype(return_statement) 집니다.

변하기 쉬운

람다가 변경 가능하다고 표시되면 (예 : []() mutable { } ) 값으로 캡처 된 값을 변경하는 것이 허용됩니다.

사용 사례

ISO 표준에 의해 정의 된 라이브러리는 람다 (lambdas)의 도움을 많이받으며 사용자가 접근 할 수있는 범위에서 작은 펑터로 코드를 복잡하게 만들 필요가 없으므로 사용 편의성을 높입니다.

C ++ 14

C ++에서는 14 개의 람다가 다양한 제안에 의해 확장되었습니다.

초기화 된 람다 캡처

이제 캡처 목록의 요소를 = 로 초기화 할 수 있습니다. 이렇게하면 변수의 이름을 바꿀 수 있고 이동하여 캡처 할 수 있습니다. 표준에서 취한 예 :

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

하나는 Wikipedia에서 가져온 std::move 로 캡처하는 방법을 보여줍니다.

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

일반 람다

Lambda는 이제 일반화 될 수 있습니다 ( T 가 주변 범위의 유형 템플릿 인수 인 경우 autoT 와 같습니다).

auto lambda = [](auto x, auto y) {return x + y;};

개선 된 반환 유형 공제

C ++ 14는 모든 함수에 대해 추론 된 반환 유형을 허용하고 return expression; 함수로 제한하지 않습니다 return expression; . 이것은 또한 람다에게까지 확장되었습니다.

C ++ 11에서 람다 식은 무엇입니까? 내가 언제 사용하나요? 소개하기 전에는 불가능했던 어떤 종류의 문제가 해결 되었습니까?

몇 가지 예와 유스 케이스가 유용 할 것입니다.


문제

C ++에는 std::for_eachstd::transform 과 같은 유용한 제네릭 함수가 포함되어있어 매우 편리 할 수 ​​있습니다. 유감스럽게도, 특히 사용하고자하는 functor 가 특정 기능에 고유 한 경우에는 사용하기가 어렵습니다.

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

f 한 번만 사용하면 특정 장소에서 사소한 일을하고 수업 전체를 쓰는 것이 과잉으로 보인다.

C ++ 03에서는 functor를 로컬로 유지하기 위해 다음과 같은 것을 작성하려고합니다.

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

그러나 이것은 허용되지 않습니다. f 는 C ++ 03의 template 함수에 전달 될 수 없습니다.

새로운 솔루션

C ++ 11에서는 lambda를 사용하여 struct f 를 대체 할 인라인 익명 함수 작성기를 작성할 수 있습니다. 작은 간단한 예제의 경우이 코드는 읽을 때 (한 곳에서 모든 것을 유지함) 더 간단하고 유지 관리가 더 간단 할 수 있습니다 (예 : 간단한 형식).

void func3(std::vector<int>& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

람다 함수는 익명의 펑터를위한 문법적 설탕 일뿐입니다.

반환 유형

간단한 경우 람다의 반환 유형이 추론됩니다 (예 :

void func4(std::vector<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

그러나 좀 더 복잡한 람다를 작성하기 시작하면 반환 유형을 컴파일러에서 추론 할 수없는 경우가 발생합니다. 예 :

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

이를 해결하기 위해 -> T 사용하여 람다 함수의 반환 유형을 명시 적으로 지정할 수 있습니다.

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

"캡처"변수

지금까지는 람다에 전달 된 것 이외의 것을 사용하지 않았지만 람다 내에서 다른 변수를 사용할 수도 있습니다. 다른 변수에 접근하고 싶다면 capture 절 (식의 [] 을 사용할 수 있습니다. 예를 들면 다음과 같습니다 :

void func5(std::vector<double>& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

& and = 각각을 사용하여 지정할 수있는 참조 및 값으로 캡처 할 수 있습니다.

  • [&epsilon] 참조로 캡처
  • [&] 는 참조로 람다에 사용 된 모든 변수를 캡처합니다.
  • [=] 람다에서 사용 된 모든 변수를 값으로 캡처합니다.
  • [&, epsilon] 은 [&]와 같은 변수를 캡처하지만 값에 의해 ε
  • [=, &epsilon] 는 [=]와 같은 변수를 캡처하지만 참조로는 ε

생성 된 operator() 는 기본적으로 const 이며, 기본적으로 액세스 할 때 캡처는 const 됩니다. 이것은 동일한 입력을 가진 각각의 호출이 동일한 결과를 산출한다는 효과가 있습니다. 그러나 생성 mutable operator()const 가 아닌 것을 요청하기 위해 람다를 mutable 으로 표시 할 수 있습니다.


람다 식은 일반적으로 알고리즘을 캡슐화하여 다른 함수로 전달할 수 있도록 사용됩니다. 그러나 정의 후에 즉시 람다를 실행할 수 있습니다 .

[&](){ ...your code... }(); // immediately executed lambda expression

기능적으로는

{ ...your code... } // simple code block

이렇게하면 람다식이 복잡한 함수를 리팩터링하는 강력한 도구가됩니다 . 위에서 보인 것처럼 람다 함수에서 코드 섹션을 래핑하는 것으로 시작합니다. 명시 적 매개 변수화 프로세스는 각 단계 후 중간 테스트를 통해 점진적으로 수행 될 수 있습니다. 코드 블록을 완전히 매개 변수화하면 ( & 가 제거 된 것처럼) 코드를 외부 위치로 이동하여 정상적인 기능으로 만들 수 있습니다.

마찬가지로 람다 식을 사용 하여 알고리즘 결과에 따라 변수초기화 할 수 있습니다.

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

프로그램 논리를 분할하는 방법으로 람다 식을 다른 람다 식의 인수로 전달하는 것이 유용 할 수도 있습니다.

[&]( std::function<void()> algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

람다 식 (Lambda expressions)은 또한 네임드 ( nested) 함수 를 생성 할 수있게 해주 며, 중복 된 로직을 피할 수있는 편리한 방법이 될 수 있습니다. 명명 된 람다 사용은 다른 함수에 매개 변수로 중요하지 않은 함수를 전달할 때 (익명 인라인 람다와 비교할 때) 눈이 약간 쉬워지는 경향이 있습니다. 참고 : 닫는 중괄호 뒤에 세미콜론을 잊지 마십시오.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

후속 프로파일 링이 함수 오브젝트에 대한 상당한 초기화 오버 헤드를 나타내면이를 일반 함수로 다시 쓰도록 선택할 수 있습니다.


람다 함수는 사용자가 인라인으로 생성하는 익명 함수입니다. 일부 설명 된 것처럼 변수를 캡처 할 수 있지만 (예 : http://www.stroustrup.com/C++11FAQ.html#lambda ) 몇 가지 제한 사항이 있습니다. 예를 들어, 이와 같은 콜백 인터페이스가있는 경우,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

당신은 아래에 적용하기 위해 전달 된 것과 같이 그것을 사용하기 위해 그 자리에서 함수를 작성할 수 있습니다 :

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

그러나 당신은 이것을 할 수 없습니다 :

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

C ++ 11 표준의 한계 때문입니다. 캡처를 사용하려면 라이브러리에 의존해야하며

#include <functional> 

(또는 간접적으로 그것을 얻기 위해 알고리즘과 같은 다른 STL 라이브러리) 그리고 다음과 같이 매개 변수로 일반 함수를 전달하는 대신 std :: function을 사용하십시오.

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

lambda expression 에 대한 가장 좋은 설명 중 하나는 C ++ Bjarne Stroustrup 의 저서 " ***The C++ Programming Language*** chapter 11 ( ISBN-13 : 978-0321563842 )"에서 작성되었습니다.

What is a lambda expression?

람다 표현식 (lambda expression )은 람다 ( lambda) 함수라고 불리며, 엄격히 말하면 람다 (lobda)라고도하며, 익명의 함수 객체 를 정의하고 사용하기위한 단순화 된 표기법입니다. operator ()로 명명 된 클래스를 정의하고, 나중에 해당 클래스의 객체를 만들고, 마지막으로 호출하는 대신, 약식을 사용할 수 있습니다.

When would I use one?

이것은 연산을 인수로 알고리즘에 전달하려는 경우에 특히 유용합니다. 그래픽 사용자 인터페이스 (및 다른 곳)의 맥락에서 이러한 작업은 종종 콜백 이라고합니다.

What class of problem do they solve that wasn't possible prior to their introduction?

여기에 람다 식으로 수행 된 모든 작업은 그것 없이는 해결 될 수 있지만 훨씬 더 많은 코드와 훨씬 더 복잡한 작업으로 해결 될 수 있습니다. 람다 식 (Lambda expression) 이것은 코드 최적화를위한 방법이며 코드를 더욱 매력적으로 만드는 방법입니다. Stroustup에 의해 슬픈 것처럼 :

효과적인 최적화 방법

Some examples

λ 식을 통해

void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
    for_each(begin(v),end(v),
        [&os,m](int x) { 
           if (x%m==0) os << x << '\n';
         });
}

또는 기능을 통해

class Modulo_print {
         ostream& os; // members to hold the capture list int m;
     public:
         Modulo_print(ostream& s, int mm) :os(s), m(mm) {} 
         void operator()(int x) const
           { 
             if (x%m==0) os << x << '\n'; 
           }
};

또는

void print_modulo(const vector<int>& v, ostream& os, int m) 
     // output v[i] to os if v[i]%m==0
{
    class Modulo_print {
        ostream& os; // members to hold the capture list
        int m; 
        public:
           Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
           void operator()(int x) const
           { 
               if (x%m==0) os << x << '\n';
           }
     };
     for_each(begin(v),end(v),Modulo_print{os,m}); 
}

u가 필요하다면 아래와 같이 lambda expression 명명 할 수 있습니다 :

void print_modulo(const vector<int>& v, ostream& os, int m)
    // output v[i] to os if v[i]%m==0
{
      auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
      for_each(begin(v),end(v),Modulo_print);
 }

또는 다른 간단한 샘플을 가정 해보십시오.

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});

    sort(v.begin(),v.end(),
         [sensitive](int x, int y) {
             printf("\n%i\n",  x < y);
             return sensitive ? x < y : abs(x) < abs(y);
         });


    printf("sorted");
    for_each(v.begin(), v.end(),
             [](int x) {
                 printf("x - %i;", x);
             }
             );
}

다음에 생성됩니다.

0

1

0

1

0

1

0

1

0

1

0 sortedx - 1 x - 3 x - 4 x - 5 x - 6 x - 7 x - 33;

[] - 이것은 캡처 목록 또는 lambda introducer : lambdas 가 로컬 환경에 액세스 할 필요가 없다면 사용할 수 있습니다.

책에서 인용 :

람다 식의 첫 번째 문자는 항상 [ . 람다 소개자는 다양한 형태를 취할 수 있습니다 :

[] : 빈 캡처 목록입니다. 이는 람다 본문에서 주변 컨텍스트의 로컬 이름을 사용할 수 없음을 의미합니다. 이러한 람다 표현식의 경우 데이터는 인수 또는 비 지역 변수에서 가져옵니다.

[&] : 참조로 암시 적으로 캡처합니다. 모든 로컬 이름을 사용할 수 있습니다. 모든 지역 변수는 참조로 액세스됩니다.

[=] : 값으로 암시 적으로 캡처합니다. 모든 로컬 이름을 사용할 수 있습니다. 모든 이름은 람다 식을 호출 할 때 로컬 변수의 복사본을 참조합니다.

[capture-list] : 명시 적 캡처; capture-list는 참조 또는 값에 의해 캡처 (즉, 객체에 저장) 될 로컬 변수의 이름 목록입니다. &가 앞에 오는 변수는 참조로 캡처됩니다. 다른 변수는 값으로 캡처됩니다. 캡처 목록에는이 요소와 이름 뒤에 ...을 요소로 포함 할 수도 있습니다.

[&, capture-list] : 목록 에 이름이없는 모든 로컬 변수를 참조로 암시 적으로 캡처합니다. 캡처 목록에는이를 포함 할 수 있습니다. 나열된 이름 앞에는 &를 사용할 수 없습니다. 캡처 목록에 명명 된 변수는 값으로 캡처됩니다.

[=, capture-list] : 목록에 언급되지 않은 이름을 가진 모든 로컬 변수를 값으로 암시 적으로 포착합니다. 캡처 목록에는이를 포함 할 수 없습니다. 나열된 이름 앞에는 &가 와야합니다. 캡처 목록에 명명 된 변수는 참조로 캡처됩니다.

&로 시작하는 로컬 이름은 항상 참조로 캡처되며 &로 끝나지 않은 로컬 이름은 항상 값으로 캡처됩니다. 참조로 캡처 만하면 호출 환경에서 변수를 수정할 수 있습니다.

Additional

Lambda expression 형식

추가 참조 :


답변

Q : C ++ 11의 람다 식은 무엇입니까?

A : 내부적으로 연산자 () const 가 오버로드 된 자동 생성 클래스의 객체입니다. 이러한 객체는 클로저 라고하며 컴파일러에 의해 만들어집니다. 이 '클로저'개념은 C ++ 11의 바인드 개념에 가깝습니다. 그러나 람다는 일반적으로 더 나은 코드를 생성합니다. 클로저를 통한 호출은 전체 인라이닝을 허용합니다.

Q : 언제 사용하나요?

A : "간단하고 작은 논리"를 정의하고 컴파일러에게 이전 질문에서 생성을 수행하도록 요청하십시오. 컴파일러에게 operator () 안에 넣고 싶은 식을 제공합니다. 다른 모든 stuff 컴파일러가 생성합니다.

Q : 그들이 소개하기 전에 불가능했던 어떤 종류의 문제를 해결합니까?

A : 사용자 정의 add, subrtact 작업을위한 함수 대신 연산자 오버로딩과 같은 구문 설탕입니다 ... 그러나 불필요한 코드를 줄이면 실제 논리의 1-3 줄을 일부 클래스로 래핑 할 수 있습니다! 어떤 엔지니어들은 라인의 수가 작 으면 에러가 발생할 확률이 적다 고 생각합니다. (나는 또한 그렇게 생각합니다)

사용 예

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

질문에 의해 보호되지 않는 람다에 관한 추가 정보. 관심 없다면이 섹션을 무시하십시오.

1. 캡처 된 값. 캡처 할 수있는 항목

1.1. 람다 (lambda)의 정적 저장 기간을 가진 변수를 참조 할 수 있습니다. 그들은 모두 잡혔다.

1.2. 캡처 값 "값"에 대해 람다를 사용할 수 있습니다. 이 경우 캡쳐 된 변수는 함수 객체 (클로저)에 복사됩니다.

[captureVar1,captureVar2](int arg1){}

1.3. 당신은 참조가 될 수 있습니다. & -이 문맥에서는 포인터가 아니라 참조를 의미합니다.

   [&captureVar1,&captureVar2](int arg1){}

1.4. 모든 비 정적 병을 값으로 또는 참조로 캡쳐하는 표기법이 있습니다.

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. 모든 비 정적 변수를 값으로 또는 참조로 지정하고 smth를 지정하는 표기법이 있습니다. 더. 예 : 모든 비 정적 병을 값으로 캡처하지만 참조 캡처로 Param2 캡처

[=,&Param2](int arg1){} 

참조로 모든 비 정적 덩어리를 캡처하지만 값 캡처로 Param2 캡처

[&,Param2](int arg1){} 

2. 반환 유형 공제

2.1. lambda가 하나의 식인 경우 람다 반환 유형을 추론 할 수 있습니다. 또는 명시 적으로 지정할 수 있습니다.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

lambda에 하나 이상의 표현식이있는 경우 반환 유형은 후행 반환 유형을 통해 지정해야합니다. 또한, 유사한 문법을 ​​자동 함수와 멤버 함수에 적용 할 수있다.

3. 캡처 된 값. 캡처 할 수없는 항목

3.1. 객체의 멤버 변수가 아닌 로컬 변수 만 캡처 할 수 있습니다.

4. С onversions

4.1 !! Lambda는 함수 포인터가 아니며 익명 함수는 아니지만 캡쳐가없는 lambdas는 암시 적으로 함수 포인터로 변환 될 수 있습니다.

추신

  1. 람다 문법에 대한 더 많은 정보는 Programming Language C ++ # 337, 2012-01-16, 5.1.2에서 Working Draft를 참조하십시오. 람다 표현, p.88

  2. C ++ 14에서는 "init capture"라는 추가 기능이 추가되었습니다. 종결 데이터 멤버의 임의 선언을 수행 할 수 있습니다.

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    




c++-faq