Вопрос или проблема
Передача 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::move
s).
#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;
}
Объяснение изменений
-
Замена ссылки на общий указатель: Вместо того, чтобы передавать объект
Wrapped
по ссылке в конструктореWrapper
, мы передаёмstd::shared_ptr<Wrapped>
. Это гарантирует, что объектWrapped
останется в памяти до тех пор, пока существует хотя бы один указатель на него. -
Использование
std::make_shared
: Во избежание возможных утечек памяти и для повышения производительности мы используемstd::make_shared
, чтобы создать объекты. - Главная функция: Она теперь использует
std::shared_ptr
для управления объектомWrapper
, а последующая печать значений происходит без проблем.
Вывод
При использовании std::shared_ptr
в классе Wrapper
и передаче его в конструктор из функции make_Wrapper
, вы избегаете проблем с уничтожением объектов, и структура становится более безопасной и удобной для работы. Подобный подход значительно упрощает управление памятью и уменьшает вероятность ошибок, связанных с ручным управлением ресурсами.