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


256

간단한 예 :

#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가 표준이어야합니다)


101
좋은 질문; 비록 const기본적 으로 무언가가 기쁘지만 기쁘지만 !
xtofl

3
대답은 아니지만 이것이 합리적인 것이라고 생각합니다. 값으로 무언가를 가져 가면 로컬 변수에 사본 1 개를 저장하기 위해 값을 변경해서는 안됩니다. 적어도 =를 &로 바꿔서 n을 바꾸는 실수를 저 지르지 않을 것입니다.
stefaanv

8
@ xtofl : 다른 모든 것이 const기본적으로 아닐 때 좋은지 확실하지 않습니다 .
kizzx2

8
@ Tamás Szelei : 논쟁을 시작하는 것이 아니라 IMHO 개념은 "쉽게 배우기 쉽다"는 개념이 C ++ 언어, 특히 현대에 존재하지 않습니다. 어쨌든 : P
kizzx2

3
"값별 캡처의 요점은 사용자가 임시를 변경할 수 있도록하는 것입니다"-아니요, 요점은 람다는 캡처 된 변수의 수명을 넘어서도 유효하게 유지 될 수 있다는 것입니다. C ++ 람다는 참조로 캡처 만하는 경우 너무 많은 시나리오에서 사용할 수 없습니다.
Sebastian Redl

답변:


230

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


7
이것은 좋은 지적입니다. 전적으로 동의합니다. 그러나 C ++ 0x에서는 기본값이 위의 내용을 적용하는 데 어떻게 도움이되는지 알지 못합니다. 내가 람다의 끝을 받고 있다고 생각하십시오 void f(const std::function<int(int)> g). g실제로 참조 적으로 투명 하다는 것을 어떻게 보증 합니까? g의 공급 업체는 mutable어쨌든 사용했을 수 있습니다 . 그래서 모르겠습니다. 기본이 아닌 경우, 다른 한편으로 const, 사람들은 추가해야합니다 const대신에 mutable함수 객체로, 컴파일러는 실제로 적용 할 수 있습니다 const std::function<int(int)>지금 부분과 f그 가정 할 수 g있다 const아니오?
kizzx2

8
@ kizzx2 : C ++에서는 아무 것도 시행 되지 않으며 제안 만합니다 . 평소와 같이 어리석은 일 (참조 투명성에 대한 문서화 된 요구 사항을 참조한 다음 비 참조 투명 기능을 전달)을 수행하면 무엇이든지 얻을 수 있습니다.
강아지

6
이 대답은 내 눈을 뜨었다. 이전에는이 ​​경우 람다는 현재 "실행"에 대한 사본 만 변경한다고 생각했습니다.
Zsolt Szatmari

4
@ZsoltSzatmari 귀하의 의견은 내 눈을 뜨게했습니다! : -DI는 귀하의 의견을 읽을 때 까지이 답변의 진정한 의미를 얻지 못했습니다.
Jendas

5
이 답변의 기본 전제에 동의하지 않습니다. C ++에는 언어의 다른 곳에서 "함수는 항상 같은 값을 반환해야합니다"라는 개념이 없습니다. 디자인 원칙으로서 함수를 작성하는 것이 좋은 방법이라는 데 동의하지만 표준 동작 이유로 물을 보유한다고 생각하지 않습니다 .
Ionoclast Brigham

103

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

#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 () 클래스를 생성하는 것으로 생각할 수 있습니다.

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


5
동등한 사용자 정의 유형으로 구현하면 a 또는 lambda가 어떻게 보이는지에 대한 좋은 설명이 있지만 제목과 같이 OP가 주석으로 설명 된 이유 는 기본값이므로 질문에 대답하지 않습니다. constmutable const
underscore_d

36

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

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

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

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

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


2
좋은 예입니다. 이는 가치 별 포착을위한 매우 강력한 사용 사례입니다. 그러나 왜 기본 설정 const입니까? 어떤 목적을 달성합니까? "거의"(: P)의 언어가 아닌 다른 모든 언어의 기본값 이 아닐mutable 때 여기에 없는 것 같습니다 . const
kizzx2

8
@ kizzx2 : 나는 소원은 const사람들이 CONST-정확성을 고려하도록 강요 될 것이다 적어도 기본이었다 : /
마티유 M.

1
@ kizzx2 람다 논문을 살펴보면 const람다 객체가 const인지 여부에 관계없이 호출 할 수 있도록 기본값으로 설정합니다 . 예를 들어을 사용하는 함수에 전달할 수 std::function<void()> const&있습니다. 람다는 캡처 된 사본을 변경하기 위해 초기 논문에서 클로저의 데이터 멤버가 mutable내부적으로 자동 정의되었습니다 . 이제 mutable람다 식 을 수동으로 입력해야 합니다. 그래도 자세한 근거를 찾지 못했습니다.
Johannes Schaub-litb

2
자세한 내용은 open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2651.pdf 를 참조 하십시오.
Johannes Schaub-litb

5
이 시점에서, "실제"답변 / 이론은 "구현 세부 사항을 해결하지 못했습니다": /
kizzx2

32

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에서 왜 바뀌어야 하는가에 관한 것입니다. 이 특정 기능과 관련하여 "[위원회 회원] 마음에 무엇이 있는지"를 알고 싶다면 짧고 잘 쓰여지고 읽을 가치가 있습니다.


16

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


14

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

lambda-expression의 클로저 유형에는 매개 변수와 반환 유형이 각각 lambda-expression의 parameter-declaration-clause 및 trailingreturn-type에 의해 설명되는 공용 인라인 함수 호출 연산자 (13.5.4)가 있습니다. 이 함수 호출 연산자는 lambdaexpression의 parameter-declaration-clause 뒤에 변경할 수없는 경우에만 const (9.3.1)로 선언됩니다.

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

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


표준이지만 왜 이런 식으로 작성 했습니까?
kizzx2

@ kizzx2 : 내 설명은 바로 그 인용문 아래에 있습니다. :) 그것은 litb가 캡처 한 객체의 수명에 대해 말하는 것과 약간 관련이 있지만 조금 더 나아가고 있습니다.
Xeo

@ Xeo : 아 그렇습니다. : P 가치를 포착하는 데 대한 또 다른 좋은 설명 이기도 합니다 . 그러나 왜 const기본적으로 있어야 합니까? 나는 이미 새로운 사본을 받았는데, 내가 그것을 바꾸게하지 않는 것이 이상해 보인다 – 특히 그것은 주로 잘못된 것이 아니다 – 그들은 단지 내가 추가하기를 원한다 mutable.
kizzx2

명명 된 람다처럼 보이는 새로운 일반 함수 선언 구문을 작성하려는 시도가 있다고 생각합니다. 또한 기본적으로 모든 것을 const로 만들어 다른 문제를 해결해야했습니다. 결코 완성되지는 않았지만 아이디어는 람다 정의에서 문지릅니다.
Bo Persson

2
@ kizzx2-우리가 다시 시작할 수 있다면, 우리는 아마도 var다른 모든 것에 대한 변화와 상수를 허용하는 키워드로 사용 했을 것입니다 . 지금은 그렇지 않습니다. 그래서 우리는 그와 함께 살아야합니다. IMO, C ++ 2011은 모든 것을 고려하여 꽤 나왔습니다.
보 퍼슨

11

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

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


1
전체 람다 객체는 일시적이며 멤버도 일시적으로 수명이 있습니다.
벤 Voigt

2
@Ben : IIRC, 나는 누군가가 "임시"라고 말할 때 람다 자체가 명명되지 않은 임시 객체 를 의미한다는 것을 이해하고 있지만 구성원은 아닙니다. 또한 람다 "내부"에서 람다 자체가 일시적인지 여부는 중요하지 않습니다. 질문을 다시 읽으면 OP가 "임시"라고 말했을 때 "람다 내부 n"이라고 말한 것 같습니다.
Martin Ba

6

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

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냅니다.


4

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


그에 대한 정보가 있습니까? 새로운 "임의의 표현 캡처"가 대부분의 고통 지점을 부드럽게하기 때문에 개인적으로는 그것이 나쁜 생각이라고 생각합니다.
벤 Voigt

1
@BenVoigt 그래, 그것은 변화를위한 변화처럼 보인다.
Miles Rout

3
@BenVoigt 공평하지만 C ++ mutable의 키워드조차 모르는 C ++ 개발자가 많을 것으로 예상 됩니다.
Miles Rout

1

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

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

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.