Скопируйте конструктор для класса с unique_ptr
Как мне реализовать конструктор копирования для класса, который имеет unique_ptr
переменная-член? Я рассматриваю только C++11.
7 ответов
Так как unique_ptr
не может быть передан, вам нужно либо глубоко скопировать его содержимое или преобразовать unique_ptr
к shared_ptr
,
class A
{
std::unique_ptr< int > up_;
public:
A( int i ) : up_( new int( i ) ) {}
A( const A& a ) : up_( new int( *a.up_ ) ) {}
};
int main()
{
A a( 42 );
A b = a;
}
Как упоминалось в NPE, вы можете использовать move-ctor вместо copy-ctor, но это приведет к различной семантике вашего класса. Move-ctor должен был бы сделать член как перемещаемый явно через std::move
:
A( A&& a ) : up_( std::move( a.up_ ) ) {}
Наличие полного набора необходимых операторов также приводит к
A& operator=( const A& a )
{
up_.reset( new int( *a.up_ ) );
return *this,
}
A& operator=( A&& a )
{
up_ = std::move( a.up_ );
return *this,
}
Если вы хотите использовать свой класс в std::vector
вы в основном должны решить, должен ли вектор быть единственным владельцем объекта, и в этом случае будет достаточно сделать класс подвижным, но не копируемым. Если вы не укажете copy-ctor и copy-assignment, компилятор покажет вам, как использовать std::vector с типами только для перемещения.
Обычный случай для одного, чтобы иметь unique_ptr
в классе должна быть возможность использовать наследование (в противном случае обычный объект также подойдет, см. RAII). Для этого случая до сих пор нет подходящего ответа в этой теме.
Итак, вот отправная точка:
struct Base
{
//some stuff
};
struct Derived : public Base
{
//some stuff
};
struct Foo
{
std::unique_ptr<Base> ptr; //points to Derived or some other derived class
};
... и цель, как сказано, сделать Foo
копируемый.
Для этого нужно сделать глубокую копию содержимого указателя, чтобы убедиться, что производный класс скопирован правильно.
Это может быть достигнуто путем добавления следующего кода:
struct Base
{
//some stuff
auto clone() const { return std::unique_ptr<Base>(clone_impl()); }
protected:
virtual Base* clone_impl() const = 0;
};
struct Derived : public Base
{
//some stuff
protected:
virtual Derived* clone_impl() const override { return new Derived(*this); };
};
struct Foo
{
std::unique_ptr<Base> ptr; //points to Derived or some other derived class
//rule of five, but a user-defined dtor is not necessary due to unique_ptr
Foo(Foo const& other) : ptr(other.ptr->clone()) {}
Foo(Foo && other) = default;
Foo& operator=(Foo const& other) { ptr = other.ptr->clone(); return *this; }
Foo& operator=(Foo && other) = default;
};
Здесь в основном происходит две вещи:
Первый - это добавление конструкторов копирования и перемещения, которые неявно удаляются в
Foo
как конструктор копированияunique_ptr
удален Конструктор перемещения может быть добавлен просто= default
... который просто сообщает компилятору, что обычный конструктор перемещения не должен быть удален (это работает, какunique_ptr
уже есть конструктор перемещения, который можно использовать в этом случае).Для копирования конструктора
Foo
, нет аналогичного механизма, так как нет конструктора копированияunique_ptr
, Итак, нужно построить новыйunique_ptr
заполните его копией оригинального pointee и используйте в качестве члена скопированного класса.В случае наследования, копия оригинального pointee должна быть сделана тщательно. Причина в том, что делать простое копирование с помощью
std::unique_ptr<Base>(*ptr)
в приведенном выше коде это приведет к нарезке, т. е. копируется только базовый компонент объекта, а производная часть отсутствует.Чтобы избежать этого, копия должна быть сделана через шаблон клона. Идея состоит в том, чтобы сделать копию через виртуальную функцию
clone_impl()
который возвращаетBase*
в базовом классе. Однако в производном классе он расширяется через ковариацию для возвратаDerived*
и этот указатель указывает на вновь созданную копию производного класса. Базовый класс может затем получить доступ к этому новому объекту через указатель базового класса.Base*
заверните вunique_ptr
и вернуть его через фактическийclone()
функция, которая вызывается извне.
Попробуйте этот помощник для создания глубоких копий, и справьтесь, когда source unique_ptr имеет значение null.
template< class T >
std::unique_ptr<T> copy_unique(const std::unique_ptr<T>& source)
{
return source ? std::make_unique<T>(*source) : nullptr;
}
Например:
class My
{
My( const My& rhs )
: member( copy_unique(rhs.member) )
{
}
// ... other methods
private:
std::unique_ptr<SomeType> member;
};
Даниэль Фрей упомянул о решении копирования, я бы поговорил о том, как переместить unique_ptr
#include <memory>
class A
{
public:
A() : a_(new int(33)) {}
A(A &&data) : a_(std::move(data.a_))
{
}
A& operator=(A &&data)
{
a_ = std::move(data.a_);
return *this;
}
private:
std::unique_ptr<int> a_;
};
Они называются конструктором перемещения и назначением перемещения.
вы можете использовать их как это
int main()
{
A a;
A b(std::move(a)); //this will call move constructor, transfer the resource of a to b
A c;
a = std::move(c); //this will call move assignment, transfer the resource of c to a
}
Вам нужно обернуть a и c с помощью std:: move, потому что они имеют имя std:: move, указывающее компилятору преобразовать значение в ссылку на rvalue, какими бы ни были параметры. В техническом смысле std:: move является аналогом чего-то вроде " станд:: Rvalue"
После перемещения ресурс unique_ptr переносится в другой unique_ptr
Есть много тем, которые документируют ссылки; это довольно легко для начала.
Редактировать:
Перемещенный объект должен оставаться в действительном, но неопределенном состоянии.
C++ primer 5, ch13 также дают очень хорошее объяснение о том, как "перемещать" объект
Я предлагаю использовать make_unique
class A
{
std::unique_ptr< int > up_;
public:
A( int i ) : up_(std::make_unique<int>(i)) {}
A( const A& a ) : up_(std::make_unique<int>(*a.up_)) {};
int main()
{
A a( 42 );
A b = a;
}
Как упомянул Давдихай в своем ответе . Один использует в основном из-за наследования. Таким образом, в большинстве случаев вместо (гдеclass ChildType : public BaseType {}
). Однако для того, чтобы создатьstd::unique_ptr<BaseType>
, всегда в какой-то момент принимает непосредственное участие. Вы либо передаете указатель на , либоunique_ptr<ChildType>
в конструктор.
Бывают ситуации, когда проще переключить используемый класс, чем добавлять интерфейс клона ко всем базовым и дочерним типам, которые будут использоваться в какой-то момент.
Это позволяет написать обертку вокругunique_ptr
с помощью метода клонирования (аналогично уже существующему пользовательскому средству удаления), например:
template<typename T>
requires (std::has_virtual_destructor_v<T>)
class clonable_unique_ptr {
public: // Associated types
using pointer = std::unique_ptr<T>::pointer;
using element_type = std::unique_ptr<T>::element_type;
using cloner_type = std::function<T*(const T*)>;
private:
std::unique_ptr<T> ptr;
cloner_type cloneFn = [](const T*) { return nullptr; };
clonable_unique_ptr(T* rawPtr, cloner_type cloneFn)
: ptr(rawPtr), cloneFn(cloneFn) {}
public:
constexpr clonable_unique_ptr() noexcept {}
constexpr clonable_unique_ptr(std::nullptr_t) noexcept {}
/**
* ctor creating a clonable_unique_ptr from a given rawPtr.
* @attention The rawPtr must be of the type that it actually points to!
* Meaning: The ChildType
*/
template<typename U>
explicit clonable_unique_ptr(U* rawPtr) : ptr(rawPtr) {
static_assert(std::derived_from<U, T>, "Types not compatible");
static_assert(!std::is_abstract_v<U>, "Entrypoint ctors need to be given the actual type, not the base type!");
static_assert(std::is_copy_constructible_v<U>, "Type needs to be copy constructible");
cloneFn = [](const T* ptr) { return new U(*static_cast<const U*>(ptr)); };
}
/**
* ctor creating a clonable_unique_ptr from a given unique_ptr<ChildType>.
* @attention The ptr must be of the type that it actually points to!
* Meaning: The ChildType
*/
template<typename U>
clonable_unique_ptr(std::unique_ptr<U>&& o) {
static_assert(std::derived_from<U, T>, "Types not compatible");
static_assert(!std::is_abstract_v<U>, "Entrypoint ctors need to be given the actual type, not the base type!");
static_assert(std::is_copy_constructible_v<U>, "Type needs to be copy constructible");
cloneFn = [](const T* ptr) { return new U(*static_cast<const U*>(ptr)); };
ptr = std::move(o);
}
/**
* move ctor, moving from another instance of clonable_unique_ptr
*/
clonable_unique_ptr(clonable_unique_ptr&& o)
: ptr(std::move(o.ptr)), cloneFn(std::move(o.cloneFn)) {}
/** move assignable */
clonable_unique_ptr& operator=(clonable_unique_ptr&&) = default;
// default unique_ptr interface with accessors
T* get() const noexcept { return ptr.get(); }
T* operator->() { return ptr.get(); }
auto& operator*() const noexcept(noexcept(*std::declval<pointer>())) {
return *ptr;
}
void reset() noexcept {
ptr.reset();
cloneFn = [](const T*) { return nullptr; };
}
void swap(clonable_unique_ptr& o) noexcept {
ptr.swap(o.ptr);
std::swap(cloneFn, o.cloneFn);
}
/**
* Clone the instance of T contained in this pointer by using the copy ctor
* of the ChildType that is contained in this ptr.
*/
clonable_unique_ptr<T> clone() const {
return clonable_unique_ptr(cloneFn(get()), cloneFn);
}
};
template<typename T0, typename T1>
bool inline operator==(const clonable_unique_ptr<T0>& ptr0, const clonable_unique_ptr<T1>& ptr1) {
return ptr0.get() == ptr1.get();
}
template<typename T0>
bool inline operator==(const clonable_unique_ptr<T0>& ptr0, std::nullptr_t) {
return ptr0.get() == nullptr;
}
Таким образом, ответственность за реализациюclone
был помещен в указатель и выведен из BaseClass/ChildClass. Некоторые примеры:
struct BaseClass {
virtual void doStuff() = 0;
virtual ~BaseClass() {}
};
struct ChildClass : public BaseClass {
std::string value;
ChildClass(std::string value) : value(value) {}
void doStuff() override {
std::cout << "doStuff: " << value << " " << this << std::endl;
}
};
The clonable_unique_ptr
затем может быть создан с использованием указателя наChildType
:
clonable_unique_ptr<BaseClass> fromPtr(new ChildClass("cake"));
clonable_unique_ptr<BaseClass> fromPtrClone = fromPtr.clone();
fromPtr->doStuff(); // two different instances
fromPtrClone->doStuff();
или изstd::unique_ptr<ChildType>
для облегчения совместимости:
clonable_unique_ptr<BaseClass> fromUPtr = std::make_unique<ChildClass>("blub");
clonable_unique_ptr<BaseClass> fromUPtrClone = fromUPtr.clone();
fromUPtr->doStuff(); // two different instances
fromUPtrClone->doStuff();
Вот ссылка на Godbolt , чтобы поиграть с ним. Я еще не добавил в эту оболочку поддержку специального удаления, потому что лично мне это не нужно, но это должно быть тривиально.
unique_ptr
не копируется, это только подвижно.
Это напрямую повлияет на Test, который во втором примере также может быть перемещаемым и не копируемым.
На самом деле, это хорошо, что вы используете unique_ptr
который защищает вас от большой ошибки.
Например, основная проблема с вашим первым кодом заключается в том, что указатель никогда не удаляется, что очень, очень плохо. Скажем, вы бы это исправили:
class Test
{
int* ptr; // writing this in one line is meh, not sure if even standard C++
Test() : ptr(new int(10)) {}
~Test() {delete ptr;}
};
int main()
{
Test o;
Test t = o;
}
Это тоже плохо. Что произойдет, если вы копируете Test
? Будет два класса, у которых есть указатель, указывающий на один и тот же адрес.
Когда один Test
уничтожен, он также уничтожит указатель. Когда твой второй Test
уничтожен, он также попытается удалить память за указателем. Но он уже был удален, и мы получим некоторую ошибку времени доступа к памяти (или неопределенное поведение, если нам не повезет).
Таким образом, правильный путь - это реализовать конструктор копирования и оператор присваивания копии, чтобы поведение было ясным, и мы могли создать копию.
unique_ptr
это далеко впереди нас здесь. Это имеет смысловой смысл: " Я есть unique
так что вы не можете просто скопировать меня. "Таким образом, это предотвращает нас от ошибки при реализации операторов под рукой.
Вы можете определить конструктор копирования и оператор назначения копирования для особого поведения, и ваш код будет работать. Но вы по праву (!) Вынуждены это сделать.
Мораль этой истории: всегда используйте unique_ptr
в таких ситуациях.