Sao chép hàm tạo cho một lớp với unique_ptr


105

Làm cách nào để triển khai một hàm tạo sao chép cho một lớp có unique_ptrbiến thành viên? Tôi chỉ đang xem xét C ++ 11.


9
Chà, bạn muốn hàm tạo bản sao làm gì?
Nicol Bolas

Tôi đọc rằng unique_ptr là không thể sao chép. Điều này khiến tôi tự hỏi làm cách nào để sử dụng một lớp có biến thành viên unique_ptr trong a std::vector.
codefx

2
@AbhijitKadam Bạn có thể sao chép sâu nội dung của unique_ptr. Trên thực tế, đó thường là điều hợp lý để làm.
Khối

2
Xin lưu ý rằng bạn có thể đang đặt câu hỏi sai. Bạn có thể không muốn một hàm tạo sao chép cho lớp của mình chứa a unique_ptr, bạn có thể muốn một hàm tạo di chuyển, nếu mục tiêu của bạn là đặt dữ liệu vào a std::vector. Mặt khác, các tiêu chuẩn C ++ 11 đã tự động tạo nhà xây dựng di chuyển, vì vậy có thể bạn muốn có một nhà xây dựng bản sao ...
Yakk - Adam Nevraumont

3
Các phần tử vectơ @codefx không nhất thiết phải sao chép được; nó chỉ có nghĩa là vector sẽ không thể sao chép được.
MM

Câu trả lời:


81

unique_ptrkhông thể chia sẻ, bạn cần sao chép sâu nội dung của nó hoặc chuyển đổi unique_ptrthành a 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;
}

Bạn có thể, như NPE đã đề cập, sử dụng move-ctor thay vì copy-ctor nhưng điều đó sẽ dẫn đến ngữ nghĩa khác nhau của lớp của bạn. Một người di chuyển sẽ cần làm cho thành viên có thể di chuyển một cách rõ ràng thông qua std::move:

A( A&& a ) : up_( std::move( a.up_ ) ) {}

Có một tập hợp đầy đủ các toán tử cần thiết cũng dẫn đến

A& operator=( const A& a )
{
   up_.reset( new int( *a.up_ ) );
   return *this,
}

A& operator=( A&& a )
{
   up_ = std::move( a.up_ );
   return *this,
}

Nếu bạn muốn sử dụng lớp của mình trong a std::vector, về cơ bản bạn phải quyết định xem vectơ có phải là chủ sở hữu duy nhất của một đối tượng hay không, trong trường hợp đó, nó sẽ đủ để làm cho lớp có thể di chuyển được nhưng không thể sao chép. Nếu bạn bỏ qua bản sao-ctor và sao chép-gán, trình biên dịch sẽ hướng dẫn bạn cách sử dụng vectơ std :: với các kiểu chỉ di chuyển.


4
Có thể đáng nói đến các nhà xây dựng chuyển động?
NPE

4
+1, nhưng hàm tạo di chuyển nên được nhấn mạnh hơn nữa. Trong một nhận xét, OP cho biết mục tiêu là sử dụng đối tượng trong một vector. Vì vậy, chuyển công trình và chuyển nhượng là những việc cần thiết.
jogojapan

36
Như một cảnh báo, chiến lược trên hoạt động đối với các loại đơn giản như int. Nếu bạn có một unique_ptr<Base>cửa hàng a Derived, ở trên sẽ cắt.
Yakk - Adam Nevraumont

5
Không có kiểm tra cho null, vì vậy điều này cho phép một hội nghị nullptr. Thế cònA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining

1
@Aaron trong các tình huống đa hình, trình phân tách sẽ bị xóa bằng cách nào đó, hoặc vô nghĩa (nếu bạn biết loại cần xóa, tại sao chỉ thay đổi trình phân tách?). Trong mọi trường hợp, có, đây là thiết kế của một value_ptr- unique_ptrcộng với thông tin về máy cắt / máy photocopy.
Yakk - Adam Nevraumont,

46

Trường hợp thông thường để có một unique_ptrtrong một lớp là có thể sử dụng kế thừa (nếu không thì một đối tượng thuần túy cũng thường làm như vậy, hãy xem RAII). Đối với trường hợp này, không có câu trả lời thích hợp trong chủ đề này cho đến nay .

Vì vậy, đây là điểm bắt đầu:

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
};

... và mục tiêu là, như đã nói, để có thể đối Foophó được.

Đối với điều này, người ta cần thực hiện một bản sao sâu của con trỏ chứa để đảm bảo lớp dẫn xuất được sao chép chính xác.

Điều này có thể được thực hiện bằng cách thêm mã sau:

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
    ~Foo() = default;
    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;
};

Về cơ bản có hai điều đang xảy ra ở đây:

  • Đầu tiên là việc bổ sung các hàm tạo sao chép và di chuyển, các hàm này bị xóa ngầm Fookhi hàm tạo sao chép của unique_ptrbị xóa. Phương thức khởi tạo di chuyển có thể được thêm đơn giản bằng cách = default... chỉ để cho trình biên dịch biết rằng phương thức khởi tạo di chuyển thông thường sẽ không bị xóa (điều này hoạt động, vì unique_ptrđã có một phương thức khởi tạo di chuyển có thể được sử dụng trong trường hợp này).

    Đối với hàm tạo bản sao của Foo, không có cơ chế tương tự vì không có phương thức tạo bản sao của unique_ptr. Vì vậy, người ta phải tạo một mới unique_ptr, điền vào nó một bản sao của con trỏ ban đầu và sử dụng nó như một thành viên của lớp đã sao chép.

  • Trong trường hợp có liên quan đến thừa kế, bản sao của điểm chỉ gốc phải được thực hiện cẩn thận. Lý do là việc sao chép đơn giản qua std::unique_ptr<Base>(*ptr)đoạn mã trên sẽ dẫn đến việc cắt, tức là chỉ có thành phần cơ sở của đối tượng được sao chép, trong khi phần dẫn xuất bị thiếu.

    Để tránh điều này, việc sao chép phải được thực hiện thông qua mẫu sao chép. Ý tưởng là thực hiện sao chép thông qua một hàm ảo clone_impl()trả về một Base*trong lớp cơ sở. Tuy nhiên, trong lớp dẫn xuất, nó được mở rộng thông qua hiệp phương sai để trả về a Derived*và con trỏ này trỏ đến một bản sao mới được tạo của lớp dẫn xuất. Sau đó, lớp cơ sở có thể truy cập đối tượng mới này thông qua con trỏ lớp cơ sở Base*, bọc nó thành một unique_ptrvà trả về nó thông qua clone()hàm thực được gọi từ bên ngoài.


3
Đây lẽ ra phải là câu trả lời được chấp nhận. Mọi người khác sẽ đi vào các vòng kết nối trong chủ đề này, mà không gợi ý về lý do tại sao người ta muốn sao chép một đối tượng được trỏ đến unique_ptrkhi ngăn chặn trực tiếp sẽ làm theo cách khác. Câu trả lời??? Sự kế thừa .
Tanveer Badar

4
Một người có thể đang sử dụng unique_ptr ngay cả khi họ biết loại bê tông được trỏ đến vì nhiều lý do: 1. Nó cần phải là nullable. 2. Con trỏ rất lớn và chúng ta có thể có không gian ngăn xếp hạn chế. Thường thì (1) và (2) sẽ đi cùng nhau, do đó, thỉnh thoảng người ta có thể thích unique_ptrhơn optionalđối với các loại có thể trống.
Ponkadoodle

3
Thành ngữ mụn nhọt là một lý do khác.
emsr

Điều gì sẽ xảy ra nếu một lớp cơ sở không được trừu tượng? Để nó mà không có bộ chỉ định thuần túy có thể dẫn đến lỗi thời gian chạy nếu bạn quên thực hiện lại nó trong nguồn gốc.
Oleksij Plotnyc'kyj

1
@ OleksijPlotnyc'kyj: vâng, nếu bạn triển khai clone_implcơ sở trong, trình biên dịch sẽ không cho bạn biết nếu bạn quên nó trong lớp dẫn xuất. Tuy nhiên, bạn có thể sử dụng một lớp cơ sở khác Cloneablevà thực hiện một ảo thuần túy clone_implở đó. Sau đó, trình biên dịch sẽ phàn nàn nếu bạn quên nó trong lớp dẫn xuất.
davidhigh

11

Hãy thử trình trợ giúp này để tạo các bản sao sâu và đối phó khi nguồn unique_ptr rỗng.

    template< class T >
    std::unique_ptr<T> copy_unique(const std::unique_ptr<T>& source)
    {
        return source ? std::make_unique<T>(*source) : nullptr;
    }

Ví dụ:

class My
{
    My( const My& rhs )
        : member( copy_unique(rhs.member) )
    {
    }

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};

2
Nó sẽ sao chép chính xác nếu nguồn trỏ đến một cái gì đó bắt nguồn từ T?
Roman Shapovalov

3
@RomanShapovalov Không, có thể là không, bạn sẽ bị cắt. Trong trường hợp đó, giải pháp có thể là thêm một phương thức virtual unique_ptr <T> clone () vào kiểu T của bạn và cung cấp ghi đè phương thức clone () trong các kiểu bắt nguồn từ T. Phương thức clone sẽ tạo ra một phiên bản mới của kiểu dẫn xuất và trả về kiểu đó.
Scott Langham

Không có con trỏ duy nhất / phạm vi trong c ++ hoặc thư viện tăng cường có chức năng sao chép sâu được tích hợp sẵn? Sẽ rất tuyệt nếu không phải tạo các hàm tạo bản sao tùy chỉnh của chúng tôi, v.v. cho các lớp sử dụng các con trỏ thông minh này, khi chúng ta muốn hành vi sao chép sâu, điều này thường xảy ra. Chỉ băn khoăn.
shadow_map

5

Daniel Frey đề cập đến giải pháp sao chép, tôi sẽ nói về cách di chuyển 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_;
};

Chúng được gọi là hàm tạo di chuyển và phép gán di chuyển

bạn có thể sử dụng chúng như thế này

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

}

Bạn cần bao bọc a và c bởi std :: move vì chúng có tên std :: move đang yêu cầu trình biên dịch chuyển đổi giá trị thành tham chiếu rvalue bất kể tham số nào Theo nghĩa kỹ thuật, std :: move tương tự như " std :: rvalue "

Sau khi di chuyển, tài nguyên của unique_ptr được chuyển sang unique_ptr khác

Có rất nhiều chủ đề tài liệu tham khảo rvalue; đây là một trong những khá dễ dàng để bắt đầu .

Biên tập :

Đối tượng được di chuyển sẽ vẫn còn hiệu lực nhưng trạng thái không xác định .

C ++ primer 5, ch13 cũng giải thích rất tốt về cách "di chuyển" đối tượng


1
vậy điều gì sẽ xảy ra với đối tượng asau khi gọi std :: move (a) trong hàm tạo bmove? Nó chỉ là hoàn toàn không hợp lệ?
David Doria,

3

Tôi đề nghị sử dụng 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;
}

-1

unique_ptr không thể sao chép, nó chỉ có thể di chuyển.

Điều này sẽ ảnh hưởng trực tiếp đến Kiểm tra, trong ví dụ thứ hai của bạn, cũng chỉ có thể di chuyển và không thể sao chép.

Trên thực tế, điều tốt là bạn sử dụng unique_ptrnó sẽ bảo vệ bạn khỏi một sai lầm lớn.

Ví dụ, vấn đề chính với mã đầu tiên của bạn là con trỏ không bao giờ bị xóa, điều này thực sự rất tệ. Giả sử, bạn sẽ sửa lỗi này bằng cách:

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;
}

Điều này cũng tệ. Điều gì xảy ra, nếu bạn sao chép Test? Sẽ có hai lớp có một con trỏ trỏ đến cùng một địa chỉ.

Khi một cái Testbị phá hủy, nó cũng sẽ phá hủy con trỏ. Khi thứ hai của bạn Testbị phá hủy, nó cũng sẽ cố gắng xóa bộ nhớ đằng sau con trỏ. Nhưng nó đã bị xóa và chúng tôi sẽ gặp một số lỗi thời gian chạy truy cập bộ nhớ kém (hoặc hành vi không xác định nếu chúng tôi không may mắn).

Vì vậy, cách đúng là triển khai hàm tạo bản sao và toán tử gán sao chép, để hành vi rõ ràng và chúng ta có thể tạo bản sao.

unique_ptrlà con đường phía trước của chúng tôi ở đây. Nó có ý nghĩa ngữ nghĩa: “ Tôi là uniquevậy, vì vậy bạn không thể chỉ sao chép tôi. ” Vì vậy, nó ngăn chúng ta khỏi sai lầm khi thực hiện các toán tử trong tầm tay.

Bạn có thể xác định hàm tạo bản sao và toán tử gán sao chép cho hành vi đặc biệt và mã của bạn sẽ hoạt động. Nhưng bạn, đúng như vậy (!), Buộc phải làm điều đó.

Đạo đức của câu chuyện: luôn sử dụng unique_ptrtrong những tình huống như thế này.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.