Лямбда-функция фиксирует неверный указатель «this» - PullRequest
3 голосов
/ 21 мая 2019

У меня проблема с созданием одного тривиального проекта C ++ с использованием лямбда-выражений и общих указателей для работы.Проект находится в Visual Studio, Debug, x64.Вот код, который у меня есть.

Class1.h:

#pragma once
#include <functional>

class Class1
{
    int m_data;
    const int* m_data_ptr;
public:
    std::function<int(int)> m_func;
    Class1(int, int);
    void Assign(const int&);
};

Class2.h:

#pragma once
#include "Class1.h"

class Class2
{
    Class1 m_class1obj;
public:
    Class2(int, int);
    void Assign(const int&);
    int Compute(int);
};

main.cpp:

#include <iostream>
#include "Class1.h"
#include "Class2.h"

Class1::Class1(int i, int j) : m_data(j), m_data_ptr(nullptr)
{
    m_func = [i, this](int x)
    {
        int val = *m_data_ptr;
        return (val + m_data + i)*x;
    };
    std::cout << "Creating class1 object!\n";
}
void Class1::Assign(const int& v)
{
    m_data_ptr = &v;
}
Class2::Class2(int i, int j) : m_class1obj(i, j)
{
    std::cout << "Creating class2 object!\n";
}
void Class2::Assign(const int& v)
{
    m_class1obj.Assign(v);
}
int Class2::Compute(int v)
{
    return m_class1obj.m_func(v);
}
int main()
{
    int val = 4;
    /*
    Class2 class2obj(3, 5);
    class2obj.Assign(val);
    std::cout << class2obj.Compute(23.0) << std::endl;
    */
    std::shared_ptr<Class2> class2_ptr;
    class2_ptr = std::make_shared<Class2>(Class2(3, 5));
    class2_ptr->Assign(val);
    std::cout << class2_ptr->Compute(23) << std::endl;
}

Код компилируется нормально, но вылетает при выполнении последней строки main().Во время отладки я обнаружил, что проблема обнаруживается после завершения строки class2_ptr = std::make_shared<Class2>(Class2(3, 5));. По какой-то причине, когда лямбда в строке m_func = ... захватывает указатель this во время создания объекта Class1, он записывает адрес, который становится другимс адреса объекта сразу после создания умного указателя для Class2!Кажется, первый записанный адрес устарел.Когда я вызываю class2_ptr->Compute(23), я в конечном итоге разыменовываю нулевой указатель на int val = *m_data_ptr;, что приводит к сбою, хотя я назначил ненулевой адрес в class2_ptr->Assign(val); до вызова m_func!Но почему изменился адрес this?Это из-за внутреннего перераспределения объекта в памяти, скрытого от пользователя?Если так, почему компилятор не переназначил правильное значение this в хранилище m_func?Кроме того, несмотря на неправильный адрес объекта Class1, записанный в m_func, доступ к другому элементу данных m_data осуществляется с правильным значением в m_func.

Важная вещь заключается в том, что если я закомментирую последние четыре строки в main () и удалю комментарии для остальных трех строк, программа не завершится сбоем!Понятно, что использование общего указателя как-то связано с этим.Есть ли проблема в неправильном использовании лямбда-выражений или умных указателей в этом случае?Кажется, я не могу найти объяснение в стандарте C ++ для моей ситуации.

Спасибо за все комментарии, объясняющие, что происходит!

Ответы [ 2 ]

5 голосов
/ 21 мая 2019

std::make_shared<Class2>(Class2(3, 5)) создает временное Class2, которое перемещается в shared_ptr хранилище, а затем оригинал уничтожается.

Ваша лямбда-память захватывает адрес этого временного и использует его после временногоуничтожен.

Вам нужно избегать создания временного объекта и создать Class2 непосредственно в shared_ptr хранилище: std::make_shared<Class2>(3, 5);.

Или, что еще лучше, избавиться от лямбды.Я не уверен, почему вы хотите использовать его там.

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

Компилятор сделал то, что вы сказали.

Если так, почему компилятор не переназначил правильное значение этого в хранилище m_func?

потому что ты никогда этого не говорил?

[i, this](int x)
{
    int val = *m_data_ptr;
    return (val + m_data + i)*x;
};

значение this фиксируется в момент создания лямбды.

Никакой магии, кроме разыменования ее при использовании переменной-члена, не происходит.

Затем вы копируете m_func с одного объекта на другой; указатель в лямбде все еще указывает на старый объект. Тогда старый объект уничтожается. Тогда вы звоните m_func.

Ваш конструктор копирования поврежден. Удалить это:

  class Class1 {
    Class1(Class1 const&)=delete;

потому что вы храните указатель на себя в кишках m_func и никогда не исправляете его при копировании.

Это приводит к тому, что ваш код не компилируется; исправление приводит к компиляции и устранению ошибки:

 class2_ptr = std::make_shared<Class2>(3, 5);

оригинал создал временный Class2, а затем скопировал его в общий ресурс; эта версия создает его непосредственно на месте.

Другой подход - исправить сломанное m_func:

std::function<int(Class1*, int)> m_func;

и

m_func =  [i](Class1* self, int x)
{
    int val = *self->m_data_ptr;
    return (val + self->m_data + i)*x;
};

И где вы его используете:

return m_class1obj.m_func(&m_class1obj, v);

и теперь вам больше не нужно ни копировать ctor =delete Class1, ни изменять его конструкцию.

...