Tính hữu dụng của `enable_ Shared_from_this` là gì?


349

Tôi đã chạy qua enable_shared_from_thistrong khi đọc các ví dụ Boost.Asio và sau khi đọc tài liệu tôi vẫn không biết làm thế nào để sử dụng chính xác điều này. Ai đó có thể xin vui lòng cho tôi một ví dụ và giải thích khi sử dụng lớp này có ý nghĩa.

Câu trả lời:


362

Nó cho phép bạn có được một shared_ptrví dụ hợp lệ this, khi tất cả những gì bạn có là this. Nếu không có nó, bạn sẽ không có cách nào nhận được một shared_ptrđến this, trừ khi bạn đã có một là một thành viên. Ví dụ này từ tài liệu tăng cường cho enable_ Shared_from_this :

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_from_this();
    }
}

int main()
{
    shared_ptr<Y> p(new Y);
    shared_ptr<Y> q = p->f();
    assert(p == q);
    assert(!(p < q || q < p)); // p and q must share ownership
}

Phương thức f()trả về một giá trị hợp lệ shared_ptr, mặc dù nó không có cá thể thành viên. Lưu ý rằng bạn không thể đơn giản làm điều này:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

Con trỏ được chia sẻ mà con số này trả về sẽ có số tham chiếu khác với con trỏ "thích hợp" và một trong số chúng sẽ bị mất và giữ tham chiếu lơ lửng khi đối tượng bị xóa.

enable_shared_from_thisđã trở thành một phần của tiêu chuẩn C ++ 11. Bạn cũng có thể lấy nó từ đó cũng như từ boost.


202
+1. Điểm mấu chốt là kỹ thuật "hiển nhiên" của việc trả về shared_ptr <Y> (cái này) đã bị hỏng, vì điều này tạo ra nhiều đối tượng shared_ptr riêng biệt với số lượng tham chiếu riêng biệt. Vì lý do này, bạn không bao giờ phải tạo nhiều hơn một shared_ptr từ cùng một con trỏ thô .
j_random_hacker

3
Cần lưu ý rằng trong C ++ 11 trở lên , việc sử dụng hàm tạo trên con trỏ thôhoàn toàn hợp lệ nếu nó kế thừa từ . Tôi không biết liệu ngữ nghĩa của Boost có được cập nhật để hỗ trợ việc này không. std::shared_ptr std::enable_shared_from_this
Matthew

6
@MatthewHolder Bạn có một trích dẫn cho điều này? Trên cppreference.com tôi đọc "Xây dựng một std::shared_ptrđối tượng đã được quản lý bởi người khác std::shared_ptrsẽ không tham khảo tài liệu tham khảo yếu được lưu trữ nội bộ và do đó sẽ dẫn đến hành vi không xác định." ( en.cppreference.com/w/cpp/memory/enable_ Shared_from_this )
Thorbjørn Lindeijer

5
Tại sao bạn không thể làm gì shared_ptr<Y> q = p?
Dan M.

2
@ ThorbjørnLindeijer, bạn nói đúng, đó là C ++ 17 trở lên. Một số triển khai đã tuân theo ngữ nghĩa C ++ 16 trước khi nó được phát hành. Cách xử lý thích hợp cho C ++ 11 đến C ++ 14 nên được sử dụng std::make_shared<T>.
Matthew

198

từ bài viết của Tiến sĩ Dobbs về con trỏ yếu, tôi nghĩ ví dụ này dễ hiểu hơn (nguồn: http://drdobbs.com/cpp/184402026 ):

... mã như thế này sẽ không hoạt động chính xác:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

Cả hai shared_ptrđối tượng đều không biết về đối tượng kia, vì vậy cả hai sẽ cố gắng giải phóng tài nguyên khi chúng bị phá hủy. Điều đó thường dẫn đến các vấn đề.

Tương tự, nếu một hàm thành viên cần một shared_ptrđối tượng sở hữu đối tượng mà nó được gọi, nó không thể tạo một đối tượng một cách nhanh chóng:

struct S
{
  shared_ptr<S> dangerous()
  {
     return shared_ptr<S>(this);   // don't do this!
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->dangerous();
   return 0;
}

Mã này có cùng một vấn đề như ví dụ trước, mặc dù ở dạng tinh tế hơn. Khi nó được xây dựng, shared_ptđối tượng r sp1sở hữu tài nguyên mới được phân bổ. Mã bên trong hàm thành viên S::dangerouskhông biết về shared_ptrđối tượng đó , vì vậy shared_ptrđối tượng mà nó trả về khác với sp1. Sao chép shared_ptrđối tượng mới để sp2không giúp đỡ; khi sp2đi ra khỏi phạm vi, nó sẽ giải phóng tài nguyên và khi sp1đi ra khỏi phạm vi, nó sẽ giải phóng tài nguyên một lần nữa.

Cách để tránh vấn đề này là sử dụng mẫu lớp enable_shared_from_this. Mẫu lấy một đối số kiểu mẫu, là tên của lớp xác định tài nguyên được quản lý. Đến lượt, lớp đó phải được xuất phát công khai từ mẫu; như thế này:

struct S : enable_shared_from_this<S>
{
  shared_ptr<S> not_dangerous()
  {
    return shared_from_this();
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->not_dangerous();
   return 0;
}

Khi bạn làm điều này, hãy nhớ rằng đối tượng mà bạn gọi shared_from_thisphải được sở hữu bởi một shared_ptrđối tượng. Điều này sẽ không hoạt động:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}

15
Cảm ơn, điều này minh họa vấn đề đang được giải quyết tốt hơn câu trả lời hiện đang được chấp nhận.
goertzenator

2
+1: Câu trả lời hay. Bên cạnh đó, thay vì shared_ptr<S> sp1(new S);có thể được ưu tiên sử dụng shared_ptr<S> sp1 = make_shared<S>();, hãy xem ví dụ stackoverflow.com/questions/18301511/
mẹo

4
Tôi khá chắc chắn dòng cuối cùng nên đọc shared_ptr<S> sp2 = p->not_dangerous();bởi vì điều đáng tiếc ở đây là bạn phải tạo một shared_ptr theo cách thông thường trước khi bạn gọi shared_from_this()lần đầu tiên! Điều này thực sự dễ dàng để có được sai! Trước C ++ 17, UB phải gọi shared_from_this()trước khi chính xác một shared_ptr đã được tạo theo cách thông thường: auto sptr = std::make_shared<S>();hoặc shared_ptr<S> sptr(new S());. Rất may từ C ++ 17 trở đi làm như vậy sẽ ném.
AnorZaken


2
@AnorZaken Điểm tốt. Sẽ rất hữu ích nếu bạn đã gửi yêu cầu chỉnh sửa để thực hiện sửa lỗi đó. Tôi vừa mới làm như vậy. Một điều hữu ích khác là người đăng sẽ không chọn tên phương thức chủ quan, nhạy cảm theo ngữ cảnh!
gạch dưới

30

Đây là lời giải thích của tôi, từ góc độ các loại hạt và bu lông (câu trả lời hàng đầu không 'nhấp chuột' với tôi). * Lưu ý rằng đây là kết quả của việc điều tra nguồn cho shared_ptr và enable_ Shared_from_this đi kèm với Visual Studio 2012. Có lẽ các trình biên dịch khác triển khai enable_spl_from_this khác nhau ... *

enable_shared_from_this<T>thêm một weak_ptr<T>ví dụ riêng Tchứa ' một số tham chiếu thực ' cho ví dụ của T.

Vì vậy, khi bạn lần đầu tiên tạo một shared_ptr<T>T * mới, điểm yếu bên trong của T * sẽ được khởi tạo với số lần truy cập là 1. Cơ shared_ptrbản mới dựa vào điều nàyweak_ptr .

Tsau đó, trong các phương thức của nó, có thể gọi shared_from_thisđể lấy một thể hiện của các sao lưushared_ptr<T> đó vào cùng một số tham chiếu được lưu trữ bên trong . Theo cách này, bạn luôn có một nơi T*lưu trữ số lần giới thiệu thay vì có nhiều shared_ptrtrường hợp không biết về nhau và mỗi người nghĩ rằng họ là người shared_ptrchịu trách nhiệm đếm Tvà xóa nó khi họ tham gia -count đạt đến không.


1
Điều này là chính xác, và phần thực sự quan trọng là So, when you first create...bởi vì đó là một yêu cầu (như bạn nói, yếu_ptr không được khởi tạo cho đến khi bạn chuyển con trỏ đối tượng vào một ctor shared_ptr!) Và yêu cầu này là nơi mọi thứ có thể trở nên sai lầm khủng khiếp nếu bạn là không cẩn thận. Nếu bạn không tạo shared_ptr trước khi gọi, shared_from_thisbạn sẽ nhận được UB - tương tự như vậy nếu bạn tạo nhiều hơn một shared_ptr, bạn cũng nhận được UB. Bạn phải bằng cách nào đó đảm bảo rằng bạn tạo một shared_ptr chính xác một lần.
AnorZaken

2
Nói cách khác, toàn bộ ý tưởng enable_shared_from_thisbắt đầu dễ vỡ vì vấn đề là có thể lấy shared_ptr<T>từ a T*, nhưng trong thực tế khi bạn nhận được một con trỏ T* t, thường không an toàn khi thừa nhận bất cứ điều gì về nó đã được chia sẻ hay chưa, và đoán sai là UB.
AnorZaken

" Internal yếu_ptr được khởi tạo với tổng số 1 " ptr yếu cho T không sở hữu ptr thông minh cho T. Một ptr yếu là một sở hữu thông minh để có đủ thông tin để tạo ptr sở hữu là "bản sao" của ptr sở hữu khác. Một ptr yếu không có số lượng ref. Nó có quyền truy cập vào một số lượng ref, giống như tất cả các ref sở hữu.
tò mò

3

Lưu ý rằng việc sử dụng boost :: intrusive_ptr không gặp phải vấn đề này. Đây thường là một cách thuận tiện hơn để khắc phục vấn đề này.


Có, nhưng enable_shared_from_thischo phép bạn làm việc với một API cụ thể chấp nhận shared_ptr<>. Theo tôi, một API như vậy thường là Làm sai (vì tốt hơn là để thứ gì đó cao hơn trong ngăn xếp sở hữu bộ nhớ) nhưng nếu bạn buộc phải làm việc với API như vậy, thì đây là một lựa chọn tốt.
cdunn2001

2
Tốt hơn là ở trong tiêu chuẩn càng nhiều càng tốt.
Sergei

3

Nó giống hệt nhau trong c ++ 11 trở lên: Đó là cho phép khả năng quay trở lại thisnhư một con trỏ dùng chung vì thiscung cấp cho bạn một con trỏ thô.

nói cách khác, nó cho phép bạn biến mã như thế này

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

vào đây:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           

Điều này sẽ chỉ hoạt động nếu các đối tượng này luôn được quản lý bởi a shared_ptr. Bạn có thể muốn thay đổi giao diện để đảm bảo đó là trường hợp.
tò mò

1
Bạn hoàn toàn chính xác @cantlyguy. Điều này không cần phải nói. Tôi cũng thích typedef-ing tất cả shared_ptr của mình để cải thiện khả năng đọc khi xác định API công khai. Ví dụ, thay vì std::shared_ptr<Node> getParent const(), tôi thường sẽ phơi bày nó NodePtr getParent const()thay vào đó. Nếu bạn thực sự cần quyền truy cập vào con trỏ thô bên trong (ví dụ tốt nhất: giao dịch với thư viện C), thì std::shared_ptr<T>::gettôi sẽ không đề cập đến điều này vì tôi đã sử dụng trình truy cập con trỏ thô này vì sử dụng quá nhiều lần vì lý do sai.
mchiasson

-3

Một cách khác là thêm một weak_ptr<Y> m_stubthành viên vào class Y. Sau đó viết:

shared_ptr<Y> Y::f()
{
    return m_stub.lock();
}

Hữu ích khi bạn không thể thay đổi lớp bạn đang bắt nguồn (ví dụ: mở rộng thư viện của người khác). Đừng quên khởi tạo thành viên, ví dụ: m_stub = shared_ptr<Y>(this)nó có hiệu lực ngay cả trong khi xây dựng.

Sẽ ổn nếu có nhiều sơ khai như thế này trong hệ thống phân cấp thừa kế, nó sẽ không ngăn chặn sự phá hủy của đối tượng.

Chỉnh sửa: Như được chỉ định chính xác bởi nobar người dùng, mã sẽ phá hủy đối tượng Y khi quá trình gán kết thúc và các biến tạm thời bị hủy. Do đó, câu trả lời của tôi là không chính xác.


4
Nếu ý định của bạn ở đây là sản xuất một shared_ptr<>cái mà không xóa điểm của nó, thì điều này là quá mức cần thiết. Bạn có thể chỉ cần nói return shared_ptr<Y>(this, no_op_deleter);nơi no_op_deletermột đối tượng hàm unary lấy Y*và không làm gì.
John Zwinck

2
Có vẻ như đây không phải là một giải pháp làm việc. m_stub = shared_ptr<Y>(this)sẽ xây dựng và hủy ngay lập tức một shared_ptr tạm thời từ đây. Khi tuyên bố này kết thúc, thissẽ bị xóa và tất cả các tài liệu tham khảo tiếp theo sẽ được treo lủng lẳng.
tộc

2
Tác giả thừa nhận câu trả lời này là sai nên có lẽ ông chỉ có thể xóa nó. Nhưng lần cuối cùng anh đăng nhập 4,5 năm nên không có khả năng làm điều đó - ai đó có quyền hạn cao hơn có thể loại bỏ cá trích đỏ này không?
Tom Goodfellow

nếu bạn nhìn vào việc thực hiện enable_shared_from_this, nó sẽ giữ một weak_ptrchính nó (được điền bởi ctor), được trả về như một shared_ptrkhi bạn gọi shared_from_this. Nói cách khác, bạn đang nhân đôi những gì enable_shared_from_thisđã cung cấp.
mchiasson
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.