Sự khác biệt về make_ Shared và normal_ptr bình thường trong C ++


275
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

Nhiều bài viết trên google và stackoverflow có trên đó, nhưng tôi không thể hiểu tại sao make_sharedhiệu quả hơn là sử dụng trực tiếp shared_ptr.

Ai đó có thể giải thích cho tôi từng bước trình tự các đối tượng được tạo và các hoạt động được thực hiện bởi cả hai để tôi có thể hiểu làm thế nào make_sharedlà hiệu quả. Tôi đã đưa ra một ví dụ ở trên để tham khảo.


4
Nó không hiệu quả hơn. Lý do để sử dụng nó là vì an toàn ngoại lệ.
Yuushi

Một số bài báo nói, nó tránh một số chi phí xây dựng, bạn có thể vui lòng giải thích thêm về điều này?
Anup Buchke

16
@Yuushi: An toàn ngoại lệ là một lý do tốt để sử dụng nó, nhưng nó cũng hiệu quả hơn.
Mike Seymour

3
32:15 là nơi anh ấy bắt đầu trong video tôi liên kết ở trên, nếu điều đó có ích.
chris

4
Lợi thế kiểu mã nhỏ: sử dụng make_sharedbạn có thể viết auto p1(std::make_shared<A>())và p1 sẽ có loại chính xác.
Ivan Vergiliev

Câu trả lời:


332

Sự khác biệt là std::make_sharedthực hiện một phân bổ heap, trong khi gọi hàm std::shared_ptrtạo thực hiện hai.

Trường hợp phân bổ heap xảy ra ở đâu?

std::shared_ptr quản lý hai thực thể:

  • khối điều khiển (lưu trữ dữ liệu meta, chẳng hạn như số lần đếm, số lần xóa loại, v.v.)
  • đối tượng được quản lý

std::make_sharedthực hiện một kế toán phân bổ heap duy nhất cho không gian cần thiết cho cả khối điều khiển và dữ liệu. Trong trường hợp khác, new Obj("foo")gọi một phân bổ heap cho dữ liệu được quản lý và hàm std::shared_ptrtạo thực hiện một phân bổ khác cho khối điều khiển.

Để biết thêm thông tin, hãy kiểm tra các ghi chú thực hiện tại cppreference .

Cập nhật I: Ngoại lệ-An toàn

LƯU Ý (2019/08/30) : Đây không phải là vấn đề kể từ C ++ 17, do những thay đổi trong thứ tự đánh giá của các đối số hàm. Cụ thể, mỗi đối số cho một hàm được yêu cầu thực hiện đầy đủ trước khi đánh giá các đối số khác.

Vì OP dường như đang tự hỏi về khía cạnh an toàn ngoại lệ của mọi thứ, tôi đã cập nhật câu trả lời của mình.

Hãy xem xét ví dụ này,

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

Bởi vì C ++ cho phép thứ tự đánh giá các biểu thức con tùy ý, nên một thứ tự có thể là:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

Bây giờ, giả sử chúng ta nhận được một ngoại lệ được ném ở bước 2 (ví dụ: ngoài ngoại lệ bộ nhớ, hàm Rhstạo đã ném một số ngoại lệ). Sau đó chúng tôi mất bộ nhớ được phân bổ ở bước 1, vì sẽ không có cơ hội để dọn sạch nó. Cốt lõi của vấn đề ở đây là con trỏ thô không được chuyển đến hàm std::shared_ptrtạo ngay lập tức.

Một cách để khắc phục điều này là thực hiện chúng trên các dòng riêng biệt để việc đặt hàng độc lập này không thể xảy ra.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

Cách ưa thích để giải quyết điều này tất nhiên là sử dụng std::make_sharedthay thế.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

Cập nhật II: Nhược điểm của std::make_shared

Trích dẫn ý kiến ​​của Casey :

Vì chỉ có một phân bổ, bộ nhớ của con trỏ không thể bị phân bổ cho đến khi khối điều khiển không còn được sử dụng. A weak_ptrcó thể giữ khối điều khiển sống vô thời hạn.

Tại sao các trường hợp của weak_ptrs giữ khối điều khiển sống?

Phải có một cách để weak_ptrs xác định xem đối tượng được quản lý có còn hợp lệ không (ví dụ: for lock). Họ làm điều này bằng cách kiểm tra số shared_ptrs sở hữu đối tượng được quản lý, được lưu trữ trong khối điều khiển. Kết quả là các khối điều khiển còn sống cho đến khi cả shared_ptrsố đếm và weak_ptrsố đếm đều chạm 0.

Quay lại std::make_shared

std::make_sharedthực hiện phân bổ heap duy nhất cho cả khối điều khiển và đối tượng được quản lý, không có cách nào giải phóng bộ nhớ cho khối điều khiển và đối tượng được quản lý một cách độc lập. Chúng ta phải đợi cho đến khi chúng tôi có thể giải phóng cả hai khối điều khiển và đối tượng quản lý, mà sẽ xảy ra là cho đến khi không có shared_ptrs hay weak_ptrlà còn sống.

Giả sử thay vào đó chúng tôi đã thực hiện hai phân bổ heap cho khối điều khiển và đối tượng được quản lý thông qua newvà hàm shared_ptrtạo. Sau đó, chúng tôi giải phóng bộ nhớ cho đối tượng được quản lý (có thể sớm hơn) khi không shared_ptrcòn sống và giải phóng bộ nhớ cho khối điều khiển (có thể sau này) khi không weak_ptrcòn sống.


53
Bạn cũng nên đề cập đến nhược điểm nhỏ của trường hợp góc nhỏ make_shared: vì chỉ có một phân bổ, bộ nhớ của người chỉ điểm không thể được giải phóng cho đến khi khối điều khiển không còn được sử dụng. A weak_ptrcó thể giữ khối điều khiển sống vô thời hạn.
Casey

13
Một điểm khác, phong cách hơn, là: Nếu bạn sử dụng make_sharedmake_uniquenhất quán, bạn sẽ không sở hữu các con trỏ thô và có thể coi mọi sự xuất hiện của newmột mùi mã.
Philipp

5
Nếu chỉ có một có shared_ptr, và không có weak_ptrs, gọi reset()trên shared_ptrdụ sẽ xóa các khối điều khiển. Nhưng điều này là bất kể hoặc make_sharedđã được sử dụng. Việc sử dụng make_sharedtạo ra sự khác biệt bởi vì nó có thể kéo dài thời gian sống của bộ nhớ được phân bổ cho đối tượng được quản lý . Khi shared_ptrđếm lượt truy cập 0, destructor cho đối tượng quản lý được gọi là không phân biệt make_shared, nhưng cách giải phóng bộ nhớ của nó chỉ có thể được thực hiện nếu make_sharedđược không sử dụng. Hy vọng điều này làm cho nó rõ ràng hơn.
viên

4
Một điều đáng nói nữa là make_ Shared có thể tận dụng tối ưu hóa "Chúng tôi biết bạn sống ở đâu" cho phép khối điều khiển trở thành một con trỏ nhỏ hơn. (Để biết chi tiết, xem bản trình bày GN2012 của Stephan T. Lavavej vào khoảng phút 12.) make_ Shared do đó không chỉ tránh phân bổ mà còn phân bổ tổng bộ nhớ ít hơn.
KnowIt ALLWannabe

1
@HannaKhalil: Đây có lẽ là lãnh địa của những gì bạn đang tìm kiếm ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark

26

Con trỏ chia sẻ quản lý cả chính đối tượng và một đối tượng nhỏ chứa số tham chiếu và dữ liệu vệ sinh khác. make_sharedcó thể phân bổ một khối bộ nhớ duy nhất để chứa cả hai thứ này; xây dựng một con trỏ được chia sẻ từ một con trỏ đến một đối tượng đã được phân bổ sẽ cần phân bổ một khối thứ hai để lưu trữ số tham chiếu.

Cũng như hiệu quả này, sử dụng các make_sharedphương tiện mà bạn không cần phải xử lý newvà các con trỏ thô, mang lại sự an toàn ngoại lệ tốt hơn - không có khả năng ném ngoại lệ sau khi phân bổ đối tượng mà trước khi gán nó cho con trỏ thông minh.


2
Tôi hiểu điểm đầu tiên của bạn một cách chính xác. Bạn có thể vui lòng giải thích hoặc đưa ra một số liên kết về điểm thứ hai về an toàn ngoại lệ?
Anup Buchke

22

Có một trường hợp khác, hai khả năng khác nhau, trên đầu trang của những khả năng đã được đề cập: nếu bạn cần gọi một nhà xây dựng không công khai (được bảo vệ hoặc riêng tư), make_ Shared có thể không truy cập được, trong khi biến thể với phiên bản mới hoạt động tốt .

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};

Tôi gặp vấn đề chính xác này và quyết định sử dụng new, nếu không tôi sẽ sử dụng make_shared. Đây là một câu hỏi liên quan về điều đó: stackoverflow.com/questions/8147027/ cấp .
jigglypuff

6

Nếu bạn cần căn chỉnh bộ nhớ đặc biệt trên đối tượng được điều khiển bởi shared_ptr, bạn không thể dựa vào make_ Shared, nhưng tôi nghĩ đó là lý do duy nhất để không sử dụng nó.


2
Tình huống thứ hai trong đó make_ Shared không phù hợp là khi bạn muốn chỉ định một deleter tùy chỉnh.
KnowIt ALLWannabe

5

Tôi thấy một vấn đề với std :: make_ Shared, nó không hỗ trợ các hàm tạo riêng tư / được bảo vệ


3

Shared_ptr: Thực hiện phân bổ hai đống

  1. Khối điều khiển (số tham chiếu)
  2. Đối tượng đang được quản lý

Make_shared: Chỉ thực hiện một phân bổ heap

  1. Khối điều khiển và dữ liệu đối tượng.

0

Về hiệu quả và thời gian quan tâm dành cho việc phân bổ, tôi đã thực hiện bài kiểm tra đơn giản này dưới đây, tôi đã tạo ra nhiều trường hợp thông qua hai cách này (mỗi lần một cách):

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

Vấn đề là, sử dụng make_ Shared mất gấp đôi thời gian so với sử dụng mới. Vì vậy, sử dụng mới có hai phân bổ heap thay vì một sử dụng make_ Shared. Có thể đây là một thử nghiệm ngu ngốc nhưng không cho thấy việc sử dụng make_ Shared mất nhiều thời gian hơn so với sử dụng mới? Tất nhiên, tôi chỉ nói về thời gian được sử dụng.


4
Bài kiểm tra đó có phần vô nghĩa. Đã thử nghiệm trong cấu hình phát hành với tối ưu hóa? Ngoài ra tất cả các mục của bạn được giải phóng ngay lập tức vì vậy nó không thực tế.
Phil1970

0

Tôi nghĩ rằng phần an toàn ngoại lệ trong câu trả lời của mr mpark vẫn là một mối quan tâm hợp lệ. khi tạo một shared_ptr như thế này: shared_ptr <T> (T mới), T mới có thể thành công, trong khi việc phân bổ khối điều khiển của shared_ptr có thể thất bại. trong trường hợp này, T mới được phân bổ sẽ bị rò rỉ, vì shared_ptr không có cách nào để biết rằng nó được tạo tại chỗ và an toàn để xóa nó. hoặc tôi đang thiếu một cái gì đó? Tôi không nghĩ rằng các quy tắc chặt chẽ hơn về đánh giá tham số chức năng theo bất kỳ cách nào ở đâ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.