developer tip

C ++ 11 람다에서 참조로 참조 캡처

copycodes 2020. 12. 9. 08:23
반응형

C ++ 11 람다에서 참조로 참조 캡처


이걸 고려하세요:

#include <functional>
#include <iostream>

std::function<void()> make_function(int& x) {
    return [&]{ std::cout << x << std::endl; };
}

int main() {
    int i = 3;
    auto f = make_function(i);
    i = 5;
    f();
}

이 프로그램이 5정의되지 않은 동작을 호출하지 않고 출력되도록 보장 됩니까?

x값 ( [=])으로 캡처하면 어떻게 작동하는지 이해 하지만 참조로 캡처하여 정의되지 않은 동작을 호출하는지 확실하지 않습니다. make_function반환 후 매달린 참조로 끝날 수 있습니까, 아니면 원래 참조 된 객체가 여전히 존재하는 한 캡처 된 참조가 작동하도록 보장됩니까?

여기에서 확실한 표준 기반 답변을 찾고 있습니다. :) 지금까지 실제로 충분히 작동합니다 .)


코드는 작동하도록 보장됩니다.

표준 문구를 살펴보기 전에이 코드가 작동하는 것이 C ++위원회의 의도입니다. 그러나 현재의 표현은 이것에 대해 불충분하다고 믿어졌고 (실제로 C ++ 14 이후 표준에 대한 버그 수정이 작동하도록 만든 섬세한 배열을 깨뜨림) 문제 를 명확히하기 위해 CWG 문제 2011 이 제기되었습니다. 현재위원회를 통해 진행되고 있습니다. 내가 아는 한, 어떤 구현도 이것이 잘못되지 않습니다.


Ben Voigt의 답변에는 약간의 혼란을 일으키는 몇 가지 사실적인 오류가 포함되어 있기 때문에 몇 가지를 명확히하고 싶습니다.

  1. "범위"는 정규화되지 않은 이름 조회가 특정 이름을 선언과 연결하는 프로그램 소스 코드의 영역을 설명하는 C ++의 정적 어휘 개념입니다. 그것은 일생과는 아무런 관련이 없습니다. [basic.scope.declarative] / 1을 참조하십시오 .
  2. 람다에 대한 "범위 도달"규칙은 마찬가지로 캡처가 허용되는시기를 결정하는 구문 속성입니다. 예를 들면 :

    void f(int n) {
      struct A {
        void g() { // reaching scope of lambda starts here
          [&] { int k = n; };
          // ...
    

    n여기에서 범위에 있지만 람다의 도달 범위에는 포함되지 않으므로 캡처 할 수 없습니다. 달리 말하면, 람다의 도달 범위는 얼마나 멀리 도달하고 변수를 캡처 할 수 있는지입니다. 둘러싸는 (람다가 아닌) 함수와 해당 매개 변수까지 도달 할 수 있지만 그 밖에 도달 할 수는 없습니다. 외부에 나타나는 선언을 캡처합니다.

따라서 "범위 도달"이라는 개념은이 질문과 무관합니다. 캡처되는 엔티티 는 람다의 도달 범위 내에있는 make_function의 매개 변수 x입니다.


좋습니다. 이제이 문제에 대한 표준 문구를 살펴 보겠습니다. [expr.prim.lambda] / 17에 따라 copy에 의해 캡처 된 엔티티를 참조하는 id-expression람다 클로저 유형에 대한 멤버 액세스로 변환됩니다. 참조에 의해 캡처 된 엔티티를 참조하는 id-expression 은 그대로 남아 있으며 여전히 둘러싸는 범위에서 표시 한 것과 동일한 엔티티를 나타냅니다.

이것은 즉시 좋지 않은 것 같습니다. 참조 x의 수명이 끝났는데 어떻게 참조 할 수 있습니까? 글쎄, 그것은 거의 (아래 참조) 수명 이외의 참조를 참조 할 수있는 방법이 없다는 것이 밝혀졌습니다 (당신은 선언을 볼 수 있고,이 경우 범위에 있고 따라서 사용하기에 괜찮을 것입니다, 또는 클래스입니다 멤버 (이 경우 멤버 액세스식이 유효하려면 클래스 자체가 수명 내에 있어야 함). 그 결과, 표준은 아주 최근까지 수명이 다한 참조를 사용하는 것을 금지하지 않았습니다.

람다 표현은 참조를 사용하는 것에 대한 불이익이 없다는 사실을 이용하여 참조로 캡처 한 엔티티에 대한 액세스가 무엇을 의미하는지에 대한 명시적인 규칙을 제공 할 필요가 없었습니다. 실재; 참조 인 경우 이름은 이니셜 라이저를 나타냅니다. 그리고 이것이 바로 최근까지 (C ++ 11 및 C ++ 14 포함) 작동하도록 보장 된 방법입니다.

그러나 수명을 벗어난 참조를 언급 할 수 없다는 것은 사실이 아닙니다. 특히, 자신의 이니셜 라이저 내에서, 참조 이전의 클래스 멤버의 이니셜 라이저에서 참조 할 수 있습니다. 또는 네임 스페이스 범위 변수이고 이전에 초기화 된 다른 전역에서 액세스 할 수 있습니다. CWG 문제 2012 는 이러한 감독 문제 를 해결하기 위해 도입되었지만 실수로 참조 참조로 람다 캡처 사양을 위반했습니다. C ++ 17이 출시되기 전에이 회귀를 수정해야합니다. 적절한 우선 순위가 지정되었는지 확인하기 위해 National Body의 의견을 제출했습니다.


요약 : 문제의 코드는 표준에 의해 보장되지 않으며 람다를 깨뜨리는 합리적인 구현이 있습니다. 휴대용이 아니라고 가정하고 대신 사용하십시오.

std::function<void()> make_function(int& x)
{
    const auto px = &x;
    return [/* = */ px]{ std::cout << *px << std::endl; };
}

C ++ 14부터는 초기화 된 캡처를 사용하여 포인터를 명시 적으로 사용하지 않아도됩니다. 이렇게하면 둘러싸는 범위에있는 변수를 다시 사용하는 대신 람다에 대한 새 참조 변수가 만들어집니다.

std::function<void()> make_function(int& x)
{
    return [&x = x]{ std::cout << x << std::endl; };
}

언뜻보기에는 안전 해야하는 것 같지만 표준의 문구는 약간의 문제를 일으 킵니다.

A lambda-expression whose smallest enclosing scope is a block scope (3.3.3) is a local lambda expression; any other lambda-expression shall not have a capture-default or simple-capture in its lambda-introducer. The reaching scope of a local lambda expression is the set of enclosing scopes up to and including the innermost enclosing function and its parameters.

...

All such implicitly captured entities shall be declared within the reaching scope of the lambda expression.

...

[ Note: If an entity is implicitly or explicitly captured by reference, invoking the function call operator of the corresponding lambda-expression after the lifetime of the entity has ended is likely to result in undefined behavior. — end note ]

What we expect to happen is that x, as used inside make_function, refers to i in main() (since that is what references do), and the entity i is captured by reference. Since that entity still lives at the time of the lambda call, everything is good.

But! "implicitly captured entities" must be "within the reaching scope of the lambda expression", and i in main() is not in the reaching scope. :( Unless the parameter x counts as "declared within the reaching scope" even though the entity i itself is outside the reaching scope.

What this sounds like is that, unlike any other place in C++, a reference-to-reference is created, and the lifetime of a reference has meaning.

Definitely something I would like to see the Standard clarify.

In the meantime, the variant shown in the TL;DR section is definitely safe because the pointer is captured by value (stored inside the lambda object itself), and it is a valid pointer to an object which lasts through the call of the lambda. I would also expect that capturing by reference actually ends up storing a pointer anyway, so there should be no runtime penalty for doing this.


On closer inspection, we also imagine that it could break. Remember that on x86, in the final machine code, both local variables and function parameters are accessed using EBP-relative addressing. Parameters have a positive offset, while locals are negative. (Other architectures have different register names but many work in the same way.) Anyway, this means that capture-by-reference can be implemented by capturing only the value of EBP. Then locals and parameters alike can again be found via relative addressing. And in fact I believe I've heard of lambda implementations (in languages which had lambdas long before C++) doing exactly this: capturing the "stack frame" where the lambda was defined.

What this implies is that when make_function returns and its stack frame goes away, so does all ability to access locals AND parameters, even those which are references.

And the Standard contains the following rule, likely specifically to enable this approach:

It is unspecified whether additional unnamed non-static data members are declared in the closure type for entities captured by reference.

Conclusion: The code in the question is not guaranteed by the Standard, and there are reasonable implementations of lambdas which cause it to break. Assume it is non-portable.

참고URL : https://stackoverflow.com/questions/21443023/capturing-a-reference-by-reference-in-a-c11-lambda

반응형