Передача shared_ptr в функцию из другой функции

Вопрос или проблема

Передача shared_ptr в функцию из другой функции

Я возвращаюсь к C++ после долгого перерыва, унаследовав кодовую базу, часть которой имеет структуру, представленную в следующем коде. У меня есть два объекта, Wrapped и Wrapper, и, как можно ожидать, Wrapper оборачивает Wrapped. Существуют два фабричных метода make_Wrapper и make_Wrapped, которые возвращают указатели на соответствующие объекты.

В следующем коде я заметил, что Case1, в которой я использую shared_ptr, не работает, в то время как Case2, в которой я использую обычный указатель C++, работает. Под “не работает” я подразумеваю, что значение nz_=10 в wrapped заменяется на мусор в Case1, в то время как в Case2 оно равно 10. Я могу проверить это, распечатав значение wrapper.nz_ в main с помощью outside_wrapper->print().

Я думаю, что это происходит потому, что объект, на который указывает shared_ptr, удаляется после выхода из этой функции make_Wrapper. Я прав?

Каков правильный способ передачи объекта через shared_ptr от make_Wrapper к конструктору Wrapper?

#include <iostream>
#include <memory>
#include <functional>

// это структура, которую нужно оборачивать
struct Wrapped {
  int nz_;
  Wrapped(int nz):nz_(nz) {}
  inline virtual void destroy() { delete this; };
};

// это обертка
struct Wrapper {
  Wrapper(Wrapped& wrapped):wrapped_(wrapped){}
  Wrapped& wrapped_;
  
  inline virtual void destroy() { delete this; };
  
  void print() {
    std::cout << "из обертки: wrapped_.nz_=" << wrapped_.nz_<<std::endl;
  }
};

// фабричная функция, которая возвращает указатель на оборачиваемый объект
Wrapped* make_Wrapped(int nz) {
  return new Wrapped(nz);
}

// фабричная функция, которая возвращает указатель на объект-обертку
Wrapper* make_Wrapper(int nz) {

  // Case 1
  // следующая строка не работает - я думаю, что объект, на который указывает shared_ptr, удаляется после выхода из этой функции
  std::shared_ptr<Wrapped> wrapped(make_Wrapped(nz), std::mem_fun(&Wrapped::destroy));

  // Case 2
  // следующая строка работает
  // Wrapped *wrapped = new Wrapped(nz);
  
  std::cout<<" из make_Wrapper "<<(*wrapped).nz_<<std::endl;
  return new Wrapper(*wrapped);
}

int main() {
    int nz=10;
    char tempbuffer[80];
    
    std::shared_ptr<Wrapper> outside_wrapper(make_Wrapper(nz), std::mem_fun(&Wrapper::destroy));
    outside_wrapper->print();

    std::cout << "Введите что-то, чтобы продолжить ..." << std::endl;
    std::cin >> tempbuffer;
    return 0;
}

Вы можете использовать умный указатель для хранения объекта Wrapped внутри Wrapper. Когда вы используете умные указатели, нет необходимости в обычных указателях и ручных new/delete. Также нет необходимости (по умолчанию) в явном методе destroy().

В приведенном ниже примере я использовал std::unique_ptr, который не поддерживает копирование, и, следовательно, используется move, когда это необходимо.
std::unique_ptr должен быть умным указателем по умолчанию, когда этого достаточно.

Если вы хотите иметь совместное владение, вы можете использовать std::shared_ptr и std::make_shared вместо этого (и затем вы можете просто удалить std::moves).

#include <iostream>
#include <memory>

// это структура, которую нужно оборачивать
struct Wrapped {
    int nz_;
    Wrapped(int nz) :nz_(nz) {}
};

// это обертка
struct Wrapper {
    Wrapper(std::unique_ptr<Wrapped> wrapped) :wrapped_(std::move(wrapped)) {}
    std::unique_ptr<Wrapped> wrapped_;

    void print() {
        std::cout << "из обертки: wrapped_.nz_=" << wrapped_->nz_ << std::endl;
    }
};

// фабричная функция, которая возвращает указатель на оборачиваемый объект
std::unique_ptr<Wrapped> make_Wrapped(int nz) {
    return std::make_unique<Wrapped>(nz);
}

// фабричная функция, которая возвращает указатель на объект-обертку
std::unique_ptr<Wrapper> make_Wrapper(int nz) {
    auto wrapped = make_Wrapped(nz);
    std::cout << " из make_Wrapper " << wrapped->nz_ << std::endl;
    auto wrapper = std::make_unique<Wrapper>(std::move(wrapped));
    return wrapper;
}

int main() {
    int nz = 10;
    auto outside_wrapper = make_Wrapper(nz);
    outside_wrapper->print();
}

Вывод:

 из make_Wrapper 10
из обертки: wrapped_.nz_=10

Живой пример

Поскольку вы явно спросили о std::shared_ptr,
вот также демонстрация с его использованием: Живой пример с std::shared_ptr.

Как только объект обернут в shared_ptr, он будет уничтожен, когда последний shared_ptr, указывающий на него, будет уничтожен, что происходит в конце make_Wrapper.

Решение:

Пусть Wrapper хранит shared_ptr вместо просто ссылки, теперь Wrapped останется живым, пока жив Wrapper.

struct Wrapper {
    Wrapper(std::shared_ptr<Wrapped> wrapped) :wrapped_(std::move(wrapped)) {}
    std::shared_ptr<Wrapped> wrapped_;

    inline virtual void destroy() { delete this; };

    void print() {
        std::cout << "из обертки: wrapped_.nz_=" << wrapped_->nz_ << std::endl;
    }
};

// фабричная функция, которая возвращает указатель на объект-обертку
Wrapper* make_Wrapper(int nz) {

    std::shared_ptr<Wrapped> wrapped(make_Wrapped(nz), std::mem_fun(&Wrapped::destroy));

    std::cout << " из make_Wrapper " << (*wrapped).nz_ << std::endl;
    return new Wrapper(std::move(wrapped));
}

Ответ или решение

Ваша проблема заключается в том, что вы пытаетесь использовать std::shared_ptr для управления жизненным циклом объекта Wrapped, но передаёте его в конструктор Wrapper как ссылку. Таким образом, когда функция make_Wrapper завершает работу, объект Wrapped, на который указывает shared_ptr, уничтожается, что приводит к тому, что Wrapper указывает на недействительное место в памяти (т.н. "висячий указатель"). Давайте рассмотрим, как правильно организовать код с учётом использования std::shared_ptr.

Исправленный код с использованием std::shared_ptr

Для решения данной проблемы вам нужно изменить реализацию Wrapper так, чтобы он хранил std::shared_ptr<Wrapped> вместо ссылки на объект Wrapped. Таким образом, объект Wrapped останется в памяти до тех пор, пока на него есть хотя бы один указатель shared_ptr.

Вот как это можно сделать:

#include <iostream>
#include <memory>

// структура, которую нужно завернуть
struct Wrapped {
    int nz_;
    Wrapped(int nz): nz_(nz) {}
};

// структура-обёртка
struct Wrapper {
    Wrapper(std::shared_ptr<Wrapped> wrapped): wrapped_(std::move(wrapped)) {}

    std::shared_ptr<Wrapped> wrapped_;  // хранит shared_ptr на Wrapped

    void print() {
        std::cout << "from wrapper: wrapped_.nz_=" << wrapped_->nz_ << std::endl;
    }
};

// фабричная функция, возвращающая shared_ptr на объект Wrapped
std::shared_ptr<Wrapped> make_Wrapped(int nz) {
    return std::make_shared<Wrapped>(nz);
}

// фабричная функция, возвращающая shared_ptr на объект Wrapper
std::shared_ptr<Wrapper> make_Wrapper(int nz) {
    auto wrapped = make_Wrapped(nz);
    std::cout << " from make_Wrapper " << wrapped->nz_ << std::endl;
    return std::make_shared<Wrapper>(wrapped);  // передаёт shared_ptr
}

int main() {
    int nz = 10;

    // используя shared_ptr для управления жизненным циклом Wrapper
    std::shared_ptr<Wrapper> outside_wrapper = make_Wrapper(nz);
    outside_wrapper->print();

    std::cout << "Enter something to continue ..." << std::endl;
    char tempbuffer[80];
    std::cin >> tempbuffer;
    return 0;
}

Объяснение изменений

  1. Замена ссылки на общий указатель: Вместо того, чтобы передавать объект Wrapped по ссылке в конструкторе Wrapper, мы передаём std::shared_ptr<Wrapped>. Это гарантирует, что объект Wrapped останется в памяти до тех пор, пока существует хотя бы один указатель на него.

  2. Использование std::make_shared: Во избежание возможных утечек памяти и для повышения производительности мы используем std::make_shared, чтобы создать объекты.

  3. Главная функция: Она теперь использует std::shared_ptr для управления объектом Wrapper, а последующая печать значений происходит без проблем.

Вывод

При использовании std::shared_ptr в классе Wrapper и передаче его в конструктор из функции make_Wrapper, вы избегаете проблем с уничтожением объектов, и структура становится более безопасной и удобной для работы. Подобный подход значительно упрощает управление памятью и уменьшает вероятность ошибок, связанных с ручным управлением ресурсами.

Оцените материал
Добавить комментарий

Капча загружается...