Лямбда теряет захваченное значение - PullRequest
0 голосов
/ 26 мая 2019

Я не могу понять, почему захваченное значение потеряно.Я понимаю, что это связано с выходом за рамки или копированием объекта LambdaWrapper.Но что именно происходит?Если LambdaWrapper (100) покидает область действия в Add и ссылка на __value теряет, то почему нет такого же значения для LambdaWrapper (300).

#include <iostream>
#include <vector>
#include <functional>
using namespace std;

class LambdaWrapper {
public:
    LambdaWrapper(double new_value): __value (new_value) {
        cout << "constructed with " << __value << endl;
        __func = [this](){ cout << __value << endl;};
    }
    void call() const { __func(); }
private:
    double __value;
    function<void(void)> __func;
};

class LambdaContainer {
public:
    void Add(double value) {
        LambdaWrapper w(value); //out of scope
        parts.push_back(w);
    }

    void Add(LambdaWrapper w) // passed as value
    {
        parts.push_back(w);
    }

    void call() const {
        for (const auto& part : parts)
                part.call();
    }
private:
    vector<LambdaWrapper> parts;
};

int main() {
    LambdaContainer c;
    c.Add(100);

    LambdaWrapper w(200);
    c.Add(w);

    c.Add( LambdaWrapper(300) ); //LambdaWrapper(300) will out of scope too

    cout << "==============" << endl;
    c.call();

    return 0;
}

Вывод:

constructed with 100
constructed with 200
constructed with 300
==============
6.95168e-308 <<< WHY?
200
300

Ответы [ 3 ]

2 голосов
/ 26 мая 2019

Зачем это нужно делать, если лямбда уже такая обертка?Чтобы сохранить захват?Вы делаете противоположное этому.

Ваше замыкание, созданное в методе Add(double), фиксирует значение this, которое указывает на объект, для которого был вызван этот метод.И этот объект «умирает», когда метод выходит из области видимости.Значение этого указателя остается неверным, это висячий указатель на локальный или временный объект.То же самое произошло бы и с другими объектами из-за этой конструкции.

LambdaWrapper(const LambdaWrapper &obj) {
        __value = obj.__value;
        __func = [this](){cout << __value << endl;};
    }

Этот конструктор работает, потому что он создает новую лямбду с новым значением this, например, которое будет сохраняться.Лямбда - не более чем синтаксический сахар для экземпляра класса с полем указателя (предположительно __this), который хранит значение this и содержит void operator() () {cout << __this->__value << endl;};

Любой вызов Add(double) приведет к зависанию указателяи UB, вызов с временным объектом также приводит к UB, потому что этот объект не будет связан с константной ссылкой (которая в любом случае работает только в локальной), так что это также висячий указатель.Метод принимает оболочку по значению, что приводит к следующему шагу копирования.

Это неоптимальный дизайн, потому что каждый раз создается новый объект.Вам не понадобится конструктор копирования, если вы захватите только значение.

PS.В качестве иллюстрации к природе лямбда-замыканий, GCC даже получил ошибку \ недостаток, когда члены лямбды доступны снаружи, потому что они не являются частными.

1 голос
/ 26 мая 2019

Я полагаю, что этот вопрос спрашивает, что происходит, а не "Это нормально?", Так что в этом случае (обычно) gdb - ваш друг, модифицирующий программу для печати адреса этого во время построения, это внутри __func ифактический адрес объекта в контейнере, который мы видим: (адреса меняются, но расстояния и концепции должны оставаться неизменными)

# During constructors and captured value:
0x7fffffffda80 <- LambdaWrapper(100)
0x7fffffffdb00 <- LambdaWrapper(200)
0x7fffffffdb60 <- LambdaWrapper(300)
# Actual address of data stored in the container:
0x6170c0 <- LambdaWrapper(100)
0x6170e8 <- LambdaWrapper(200)
0x617110 <- LambdaWrapper(300)

Существует огромная разница в значениях, это потому, что создание происходит на стек , в то время как вектор выделяет данные с new в куче 1009 *.

Из gdb вызова info proc mappings мы получаем список адресов памяти:

Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x404000     0x4000        0x0 /[...]/LambdaOutOfScope/a.out
            0x603000           0x604000     0x1000     0x3000 /[...]/LambdaOutOfScope/a.out
            0x604000           0x605000     0x1000     0x4000 /[...]/LambdaOutOfScope/a.out
            0x605000           0x637000    0x32000        0x0 [heap]

[...]

      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

Но это не отвечает , почему изменяется только 100.Ответ на этот вопрос находится в кадрах стека : каждый вызов функции имеет локальное (обычно small ) пространство переменных для статических переменных (для которых нет new для простоты).

Если мы проверим информацию стека с помощью info frame, мы увидим, что:

(gdb) info frame
Stack level 0, frame at 0x7fffffffdbb0:
 rip = 0x400deb in main (main.cpp:75); saved rip = 0x7ffff7495830
 source language c++.
 Arglist at 0x7fffffffdba0, args: 
 Locals at **0x7fffffffdba0**, Previous frame's sp is 0x7fffffffdbb0
 Saved registers:
  rbx at 0x7fffffffdb98, rbp at 0x7fffffffdba0, rip at 0x7fffffffdba8

Внутри main, поэтому 100 остается вне этого кадра, потому что оно не создается в main , а в Add, чтобы проверить, когда внутри Add мы получаем:

(gdb) info frame 1
Stack frame at 0x1:
 rip = 0x0; saved rip = <not saved>
 Outermost frame: previous frame identical to this frame (corrupt stack?)
 Arglist at 0x7fffffffdac8, args: 
 Locals at **0x7fffffffdac8**, Previous frame's sp is 0x7fffffffdad8
 Saved registers:
  rip at 0x7fffffffdad0

Таким образом, повреждение происходит, когда мы вызываем другую функцию, но , так как элементы выделеныв основном локальные они сохраняются , если вы поставите c.Add(400); после 300, вы увидите, что он тоже поврежден (даже если построено после).

ПРИМЕЧАНИЕ : Iнадеюсь охватить все, но подробно об использовании gdb, в интернете есть множество руководств.

1 голос
/ 26 мая 2019

Обязательно обратите внимание на комментарий Питера.

Если вы действительно хотите найти решение, определите копию c'tor объекта LambdaWrapper, чтобы оно захватывало this исходного объекта.

class LambdaWrapper
{
public:
    LambdaWrapper(double new_value): __value (new_value)
    {
        cout << "constructed with " << __value << endl;
        __func = [this](){ cout << __value << endl;};
    }
    LambdaWrapper(const LambdaWrapper &obj) {
        __value = obj.__value;
        __func = [this](){cout << __value << endl;};
    }
    void call() const { __func(); }
private:
    double __value;
    function<void(void)> __func;
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...