IT story

C ++ 11의 람다가 기본적으로 값별 캡처를 위해 "mutable"키워드를 요구하는 이유는 무엇입니까?

hot-time 2020. 4. 7. 08:31
반응형

C ++ 11의 람다가 기본적으로 값별 캡처를 위해 "mutable"키워드를 요구하는 이유는 무엇입니까?


간단한 예 :

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

질문 : 왜 mutable키워드 가 필요 합니까? 명명 된 함수에 전달되는 기존 매개 변수와는 상당히 다릅니다. 배후의 근거는 무엇입니까?

가치 별 포착의 요점은 사용자가 임시를 변경할 수 있다는 인상을 받았습니다.

깨달음?

(저는 MSVC2010을 사용하고 있습니다. AFAIK는 표준이어야합니다)


mutable기본적으로 함수 객체는 호출 될 때마다 동일한 결과를 생성 해야 하기 때문에 필요합니다 . 이것은 객체 지향 함수와 전역 변수를 효과적으로 사용하는 함수의 차이입니다.


귀하의 코드는 다음과 거의 같습니다.

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

따라서 람다는 가변적이라고 말하지 않는 한 기본적으로 const로 설정된 operator () 클래스를 생성하는 것으로 생각할 수 있습니다.

또한 [] 내에서 (명시 적으로 또는 암시 적으로) 캡처 된 모든 변수를 해당 클래스의 구성원으로 생각할 수도 있습니다. [=]에 대한 객체의 사본 또는 [&]에 대한 객체에 대한 참조. 숨겨진 생성자가있는 것처럼 람다를 선언하면 초기화됩니다.


가치 별 포착의 요점은 사용자가 임시를 변경할 수 있다는 인상을 받았습니다.

문제는 "거의"인가? 빈번한 사용 사례는 람다를 반환하거나 전달하는 것으로 보입니다.

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

나는 그것이 mutable"거의"경우가 아니라고 생각합니다 . "값으로 캡처"를 "캡쳐 된 개체가 사망 한 후 그 값을 사용하도록 허용하는 것"보다는 "값으로 캡처"를 고려합니다. 그러나 아마도 이것은 논쟁의 여지가 있습니다.


F ++, C ++ 표준화위원회의 유명한 멤버 인 Herb Sutter는 Lambda 정확성 및 사용성 문제 에서 해당 질문에 대한 다른 답변을 제공합니다 .

프로그래머가 값으로 로컬 변수를 캡처하고 캡처 된 값 (람다 객체의 멤버 변수)을 수정하려고하는이 straw man 예제를 고려하십시오.

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

이 기능은 사용자가 자신에게 사본이 있음을 인식하지 못하고 특히 람다를 복사 할 수 있기 때문에 다른 람다의 사본을 변경할 수 있다는 우려로 추가 된 것으로 보입니다.

그의 논문은 이것이 C ++ 14에서 왜 바뀌어야 하는가에 관한 것입니다. 이 특정 기능과 관련하여 "[위원회 회원] 마음에 무엇이 있는지"를 알고 싶다면 짧고 잘 쓰여지고 읽을 가치가 있습니다.


5.1.2 [expr.prim.lambda], 하위 조항 5 의이 초안을 참조하십시오 .

람다 식의 클로저 형식에는 매개 변수와 반환 형식이 각각 람다 식의 매개 변수 선언 절과 후행 반환 형식으로 설명되는 공용 인라인 함수 호출 연산자 (13.5.4)가 있습니다. 이 함수 호출 연산자는 lambdaexpression의 parameter-declaration-clause 뒤에 변경할 수없는 경우에만 const (9.3.1)로 선언됩니다.

litb의 의견 편집 : 변수에 대한 외부 변경 사항이 람다 내부에 반영되지 않도록 값으로 캡처를 생각했을 수 있습니까? 참조는 두 가지 방식으로 작동하므로 내 설명입니다. 그래도 좋은지 모르겠습니다.

kizzx2의 의견 편집 : 람다가 사용되는 대부분의 시간은 알고리즘의 functor입니다. 기본 기능을 사용하면 const정규화 된 const함수를 사용할 수 있지만 정규화 되지 않은 함수는 사용할 수없는 것처럼 일정한 환경에서 사용할 수 있습니다 const. 어쩌면 그들은 자신의 마음 속에 무슨 일이 일어나고 있는지 알고있는 경우에 대해 더 직관적으로 만들려고 생각했을 수도 있습니다. :)


Lambda 함수 클로저 유형무엇인지 생각해야 합니다. Lambda 표현식을 선언 할 때마다 컴파일러는 속성 (선언 된 Lambda 표현식이있는 환경 ) 및 함수 호출이 ::operator()구현 된 명명되지 않은 클래스 선언에 불과한 클로저 유형을 작성합니다 . 값으로 복사를 사용하여 변수를 캡처 하면 컴파일러가 const클로저 유형에 속성을 작성 하므로 "읽기 전용"속성이므로 Lambda 표현식 내에서 변경할 수 없습니다. 어떤 방식 으로든 변수를 상위 범위에서 Lambda 범위로 복사하여 Lambda 표현식을 닫고 있기 때문에 이를 " 닫기 "라고합니다.mutable캡처 된 엔티티는 non-const클로저 유형 속성이됩니다. 이것이 값으로 캡처 한 가변 변수에서 수행 된 변경 사항이 상위 범위로 전파되지 않고 상태 저장 Lambda 내부에 유지되도록하는 원인입니다. 항상 많은 도움이 된 Lambda 표현식의 결과 클로저 유형을 상상해보십시오. 도움이 될 수 있기를 바랍니다.


가치 별 포착의 요점은 사용자가 임시를 변경할 수 있다는 인상을 받았습니다.

n일시적 아닙니다 . n은 람다 식으로 만든 람다 함수 객체의 멤버입니다. 기본적으로 람다를 호출해도 상태가 수정되지 않으므로 실수로 수정하지 못하도록 구성되어 있습니다 n.


mutable람다 선언에 대한 필요성을 완화하기위한 제안이 이제 있습니다 : n3424


캡처의 의미를 이해해야합니다! 인수 전달이 아닌 캡처입니다! 몇 가지 코드 샘플을 보자.

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

알 수 있듯이 람다 x로 변경 되었지만 20여전히 10을 반환합니다 ( x아직 5람다 x내부에 있음) 람다 내부를 변경하면 각 호출에서 람다 자체가 변경됩니다 (람다는 각 호출에서 변경됩니다). 정확성을 높이기 위해 표준에 mutable키워드가 도입되었습니다 . 람다를 변경 가능으로 지정하면 람다를 호출 할 때마다 람다 자체가 변경 될 수 있습니다. 다른 예를 보자.

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

위의 예는 람다를 변경 가능하게하여 람다 x내부를 변경 하면 각 호출에서 람다를 "돌연변이" 시켜 주 함수 x의 실제 값과는 관련이없는 새로운 값을 보여줍니다.x


강아지의 대답을 확장하기 위해 람다 함수는 순수한 함수 입니다. 즉, 고유 한 입력 세트가 지정된 모든 호출은 항상 동일한 출력을 반환합니다. 람다가 호출 될 때 입력 을 모든 인수 세트와 캡처 된 모든 변수로 정의합시다 .

순수한 기능에서 출력은 일부 내부 상태가 아닌 입력에만 의존합니다. 따라서 순수한 경우 람다 함수는 상태를 변경할 필요가 없으므로 변경할 수 없습니다.

람다가 참조로 캡처 할 때 캡처 된 변수에 쓰는 것은 순수한 함수의 개념에 대한 부담입니다. 순수한 함수는 모두 출력을 반환해야하기 때문에 쓰기는 외부 변수에 발생하기 때문에 람다는 확실히 변경되지 않습니다. 이 경우에도 올바른 사용법은 동일한 입력으로 람다를 다시 호출하면 역 참조 변수에 대한 부작용에도 불구하고 매번 출력이 동일하다는 것을 의미합니다. 이러한 부작용은 일부 추가 입력을 반환하는 방법 (예 : 카운터 업데이트) 일 뿐이며 단일 값 대신 튜플을 반환하는 등 순수한 기능으로 재구성 될 수 있습니다.

참고 : https://stackoverflow.com/questions/5501959/why-does-c11s-lambda-require-mutable-keyword-for-capture-by-value-by-defau

반응형