전역 변수에 대한 참조에서 람다 함수 변경 가능 캡처의 동작 차이


22

람다를 사용하여 가변 키워드로 전역 변수에 대한 참조를 캡처 한 다음 람다 함수의 값을 수정하면 컴파일러마다 결과가 다릅니다.

#include <stdio.h>
#include <functional>

int n = 100;

std::function<int()> f()
{
    int &m = n;
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

VS 2015 및 GCC의 결과 (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609) :

100 223 100

clang ++의 결과 (clang 버전 3.8.0-2ubuntu4 (태그 / RELEASE_380 / 최종)) :

100 223 223

왜 이런 일이 발생합니까? 이것이 C ++ 표준에서 허용됩니까?


Clang의 동작은 여전히 ​​트렁크에 있습니다.
월넛

이것들은 모두 다소 오래된 컴파일러 버전입니다
MM

최신 버전의 Clang : godbolt.org/z/P9na9c
Willy

1
캡처를 완전히 제거해도 GCC는 여전히이 코드를 수락하고 clang의 기능을 수행합니다. 그것은 GCC 버그가 있다는 강력한 힌트입니다-간단한 캡처는 람다 본문의 의미를 변경하지 않아야합니다.
TC

답변:


16

람다는 값 으로 참조 자체 를 캡처 할 수 없습니다 (사용std::reference_wrapper 해당 목적으로 사용).

람다에서는 ( [m]캡쳐 m에 없기 때문에) 값으로 &캡처하므로 m(참조 인 n)이 먼저 참조 해제되고 참조 하는 것 ( ) 의 사본n 이 캡처됩니다. 이것은 이것을하는 것과 다르지 않습니다.

int &m = n;
int x = m; // <-- copy made!

그런 다음 람다는 원본이 아닌 사본을 수정합니다. 이것이 예상대로 VS 및 GCC 출력에서 ​​발생하는 것입니다.

Clang 출력이 잘못되었으므로 아직 버그가 아닌 경우 버그로보고해야합니다.

람다를 수정하려면 참조로 n캡처 m하십시오 [&m]. 이는 하나의 참조를 다른 참조에 할당하는 것과 다르지 않습니다. 예 :

int &m = n;
int &x = m; // <-- no copy made!

또는 m전체를 제거 하고 n대신 참조로 캡처 할 수 있습니다 [&n].

이후, 비록 n전역 범위에, 정말 전혀 포착 할 필요가 없습니다, 람다는 캡처하지 않고 전 세계적으로 액세스 할 수 있습니다 :

return [] () -> int {
    n += 123;
    return n;
};

5

Clang이 실제로 정확하다고 생각합니다.

따르면 [lambda.capture] / 11 , ID 표현 그것이 구성에만 람다에서 사용 람다의 복사에 의해 캡처 된 부재를 의미 ODR을 이용 . 그렇지 않은 경우 원래 엔티티를 참조합니다 . 이것은 C ++ 11 이후의 모든 C ++ 버전에 적용됩니다.

C ++ 17의 [basic.dev.odr] / 3 에 따르면 , lvalue-to-rvalue 변환을 적용하면 상수 변수가 생성되면 참조 변수가 odr-used되지 않습니다.

그러나 C ++ 20 초안에서는 lvalue-to-rvalue 변환에 대한 요구 사항이 삭제되고 관련 통과가 여러 번 변경되어 변환을 포함하거나 포함하지 않습니다. 참조 CWG 문제 1472CWG 문제 1741을 뿐만 아니라, 개방 CWG 문제 2083 .

이후 m사용 (정적 저장 기간 오브젝트 참조) 상수 식 초기화는 예외 당 일정한 산출 식 [expr.const]을 /2.11.1 .

그러나 lvalue-to-rvalue 변환이 적용되는 경우 n에는 그렇지 않습니다. 상수 값에는 값을 사용할 수 없기 때문 입니다.

따라서 lvalue-to-rvalue 변환이 odr-use를 결정하는 데 적용되는지 여부 m에 따라 람다에서 사용할 때 람다 멤버를 참조하거나 참조하지 않을 수 있습니다.

변환을 적용해야하는 경우 GCC 및 MSVC가 정확하고 그렇지 않으면 Clang이 올바른 것입니다.

m더 이상 상수 표현식이 아닌 초기화를 변경하면 Clang이 동작을 변경 함 을 알 수 있습니다.

#include <stdio.h>
#include <functional>

int n = 100;

void g() {}

std::function<int()> f()
{
    int &m = (g(), n);
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

이 경우 모든 컴파일러는 출력이

100 223 100

m람다는 int에서의 참조 변수 m에서 copy-initialized 유형의 클로저 멤버를 참조하기 때문 입니다 f.


VS / GCC 및 Clang 결과가 모두 정확합니까? 아니면 그들 중 하나만?
윌리

[basic.dev.odr] / 3는 변수 m에 lvalue-to-rvalue 변환을 적용하는 것이 상수 표현식이 아닌 한 변수를 명명하는 표현식에 의해 변수 가 사용 된다고 말합니다 . [expr.const] / (2.7)에 따르면이 변환은 핵심 상수 표현식이 아닙니다.
aschepler

Clang의 결과가 정확하다면 어쨌든 반 직관적이라고 생각합니다. 프로그래머의 관점에서 볼 때 캡처 목록에 작성하는 변수가 실제로 변경 가능한 경우를 위해 복사되고 있는지 확인해야하며 나중에 어떤 이유로 프로그래머가 m의 초기화를 변경할 수 있습니다.
윌리

1
m += 123;여기는 modr-used입니다.
Oliv

1
Clang은 현재의 말로 옳다고 생각하며, 이것에 대해 파헤 치지 않았지만 여기의 관련 변경 사항은 거의 모든 DR입니다.
TC

4

이것은 C ++ 17 표준에서는 허용되지 않지만 다른 표준 초안에서는 허용됩니다. 이 답변에 설명되지 않은 이유로 인해 복잡합니다.

[expr.prim.lambda.capture] / 10 :

사본으로 캡처 된 각 엔티티에 대해 이름이 지정되지 않은 비 정적 데이터 멤버가 클로저 유형으로 선언됩니다. 이 멤버의 선언 순서는 지정되어 있지 않습니다. 엔티티가 오브젝트에 대한 참조 인 경우 이러한 데이터 멤버의 유형은 참조 된 유형이고, 엔티티가 함수에 대한 참조 인 경우 참조 된 함수 유형에 대한 lvalue 참조이거나 그렇지 않은 경우 해당 캡처 된 엔티티의 유형입니다.

[m]가변의 것을 의미 m에서이 f복사에 의해 포착된다. 엔터티 m는 개체 에 대한 참조이므로 클로저 형식에는 형식이 참조 형식 인 멤버가 있습니다. 즉, 멤버의 유형은입니다 int.int& .

이름 사람 m람다 바디 명 안에 밀폐 개체의 부재와하지에서 변수 f(이것이 문제가되는 부분)에 문장 m += 123;수정하는 다르다고 부재 int로부터 개체 ::n.

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