Là một deleter của shared_ptr được lưu trữ trong bộ nhớ được cấp phát bởi bộ cấp phát tùy chỉnh?


22

Nói rằng tôi có một shared_ptrvới một cấp phát tùy chỉnh một deleter tùy chỉnh.

Tôi không thể tìm thấy bất cứ điều gì trong tiêu chuẩn nói về nơi lưu trữ của deleter: nó không nói rằng bộ cấp phát tùy chỉnh sẽ được sử dụng cho bộ nhớ của deleter và nó không nói rằng nó sẽ không tồn tại.

Đây có phải là không xác định hoặc tôi chỉ thiếu một cái gì đó?

Câu trả lời:


11

hồn.smartptr. Shared.const / 9 trong C ++ 11:

Hiệu ứng: Xây dựng một đối tượng shared_ptr sở hữu đối tượng p và deleter d. Các constructor thứ hai và thứ tư sẽ sử dụng một bản sao của một bộ nhớ để phân bổ bộ nhớ cho sử dụng nội bộ.

Các constructor thứ hai và thứ tư có các nguyên mẫu này:

template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template<class D, class A> shared_ptr(nullptr_t p, D d, A a);

Trong dự thảo mới nhất, produc.smartptr. Shared.const / 10 tương đương với mục đích của chúng tôi:

Hiệu ứng: Xây dựng một đối tượng shared_ptr sở hữu đối tượng p và deleter d. Khi T không phải là kiểu mảng, các hàm tạo thứ nhất và thứ hai cho phép shared_from_this với p. Các constructor thứ hai và thứ tư sẽ sử dụng một bản sao của một bộ nhớ để phân bổ bộ nhớ cho sử dụng nội bộ. Nếu một ngoại lệ được ném, d (p) được gọi.

Vì vậy, bộ cấp phát được sử dụng nếu có nhu cầu phân bổ nó trong bộ nhớ được phân bổ. Dựa trên tiêu chuẩn hiện tại và tại các báo cáo khiếm khuyết có liên quan, việc phân bổ không bắt buộc mà do ủy ban đảm nhận.

  • Mặc dù giao diện shared_ptrcho phép thực hiện trong đó không bao giờ có khối điều khiển và tất cả shared_ptrweak_ptrđược đưa vào danh sách được liên kết, nhưng thực tế không có triển khai như vậy. Ngoài ra, ví dụ, từ ngữ đã được sửa đổi, giả sử rằng từ use_countđược chia sẻ.

  • Deleter được yêu cầu chỉ di chuyển có thể xây dựng. Vì vậy, không thể có một vài bản sao trong shared_ptr.

Người ta có thể tưởng tượng một triển khai đưa deleter vào một thiết kế đặc biệt shared_ptrvà di chuyển nó khi nó đặc biệt shared_ptrbị xóa. Mặc dù việc triển khai có vẻ phù hợp, nhưng điều này cũng lạ, đặc biệt là vì khối điều khiển có thể cần thiết cho số lượng sử dụng (có lẽ có thể nhưng thậm chí còn lạ hơn khi làm điều tương tự với số lượng sử dụng).

Các DR có liên quan tôi đã tìm thấy: 545 , 575 , 2434 (thừa nhận rằng tất cả các triển khai đang sử dụng một khối điều khiển và dường như ngụ ý rằng các ràng buộc đa luồng phần nào bắt buộc nó), 2802 (yêu cầu rằng deleter chỉ di chuyển có thể xây dựng được và do đó ngăn cản việc thực hiện deleter được sao chép giữa một số shared_ptr).


2
"để phân bổ bộ nhớ cho sử dụng nội bộ" Điều gì xảy ra nếu việc triển khai sẽ không phân bổ bộ nhớ cho sử dụng nội bộ để bắt đầu? Nó có thể sử dụng một thành viên.
LF

1
@LF Không thể, giao diện không cho phép điều đó.
AProgrammer

Về mặt lý thuyết, nó vẫn có thể sử dụng một số loại "tối ưu hóa nhỏ", phải không?
LF

Điều kỳ lạ là tôi không thể tìm thấy bất cứ điều gì về việc sử dụng cùng một bộ cấp phát (bản sao a) để phân bổ bộ nhớ đó. Mà sẽ ngụ ý một số lưu trữ của bản sao đó a. Không có thông tin nào về nó trong [produc.smartptr. Shared.dest].
Daniel Langr

1
@DanielsaysreinstateMonica, tôi tự hỏi nếu trong produc.smartptr. Shared / 1: "Mẫu lớp shared_ptr lưu trữ một con trỏ, thường thu được thông qua mới. Shared_ptr thực hiện ngữ nghĩa của quyền sở hữu chung, chủ sở hữu cuối cùng của con trỏ có chịu trách nhiệm phá hủy đối tượng hoặc giải phóng các tài nguyên liên quan đến con trỏ được lưu trữ. " việc giải phóng các tài nguyên liên quan đến con trỏ được lưu trữ không nhằm mục đích đó. Nhưng khối điều khiển cũng sẽ tồn tại cho đến khi con trỏ yếu cuối cùng bị xóa.
AProgrammer

4

Từ std :: shared_ptr chúng ta có:

Khối điều khiển là một đối tượng được phân bổ động giữ:

  • hoặc là một con trỏ tới đối tượng được quản lý hoặc chính đối tượng được quản lý;
  • deleter (loại bị xóa);
  • bộ cấp phát (loại bị xóa);
  • số lượng shared_ptrs sở hữu đối tượng được quản lý;
  • số lượng yếu_ptrs đề cập đến đối tượng được quản lý.

Và từ std :: allocate_ Shared chúng tôi nhận được:

template< class T, class Alloc, class... Args >
shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );

Xây dựng một đối tượng loại T và bọc nó trong std :: shared_ptr [...] để sử dụng một cấp phát cho cả khối điều khiển của con trỏ dùng chung và đối tượng T.

Vì vậy, có vẻ như std :: allocate_ Shared sẽ phân bổ deletervới của bạn Alloc.

EDIT: Và từ n4810§20.11.3.6 Tạo [produc.smartptr. Shared.create]

1 Các yêu cầu chung áp dụng cho tất cả make_shared, allocate_shared, make_shared_default_init, và allocate_shared_default_initquá tải, trừ khi có quy định khác, được mô tả dưới đây.

[...]

7 Lưu ý: (7.1) - Việc triển khai sẽ thực hiện không quá một cấp phát bộ nhớ. [Lưu ý: Điều này cung cấp hiệu quả tương đương với một con trỏ thông minh xâm nhập. Lưu ý

[Nhấn mạnh tất cả của tôi]

Vì vậy, tiêu chuẩn đang nói rằng std::allocate_shared nên sử dụng Alloccho khối điều khiển.


1
Tôi xin lỗi bởi cppreference không phải là một văn bản quy phạm. Đó là một nguồn tài nguyên tuyệt vời, nhưng không nhất thiết cho các câu hỏi luật sư ngôn ngữ .
Người kể chuyện - Unslander Monica

@ StoryTeller-UnslanderMonica Hoàn toàn đồng ý - đã xem qua tiêu chuẩn mới nhất và không thể tìm thấy bất cứ điều gì đã xảy ra với cppreference.
Paul Evans


Tìm thấy n4810và cập nhật câu trả lời.
Paul Evans

1
Tuy nhiên, đây là nói về make_shared, không phải chính các nhà xây dựng. Tuy nhiên, tôi có thể sử dụng một thành viên cho các thông số nhỏ.
LF

3

Tôi tin rằng điều này là không xác định.

Đây là đặc điểm kỹ thuật của các nhà xây dựng có liên quan: [produc.smartptr. Shared.const] / 10

template<class Y, class D> shared_ptr(Y* p, D d);
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

Tác dụng: Xây dựng một shared_­ptrđối tượng sở hữu đối tượng pvà deleter d. Khi Tkhông phải là một kiểu mảng, các hàm tạo thứ nhất và thứ hai cho phép shared_­from_­thisvới p. Các nhà xây dựng thứ hai và thứ tư sẽ sử dụng một bản sao ađể phân bổ bộ nhớ cho sử dụng nội bộ . Nếu một ngoại lệ được ném, d(p)được gọi.

Bây giờ, giải thích của tôi là khi thực hiện cần bộ nhớ để sử dụng nội bộ, nó sẽ làm như vậy bằng cách sử dụng a. Điều đó không có nghĩa là việc triển khai phải sử dụng bộ nhớ này để đặt mọi thứ. Ví dụ: giả sử có triển khai kỳ lạ này:

template <typename T>
class shared_ptr : /* ... */ {
    // ...
    std::aligned_storage<16> _Small_deleter;
    // ...
public:
    // ...
    template <class _D, class _A>
    shared_ptr(nullptr_t, _D __d, _A __a) // for example
        : _Allocator_base{__a}
    {
        if constexpr (sizeof(_D) <= 16)
            _Construct_at(&_Small_deleter, std::move(__d));
        else
            // use 'a' to allocate storage for the deleter
    }
// ...
};

Việc triển khai này có "sử dụng một bản sao ađể phân bổ bộ nhớ cho sử dụng nội bộ" không? Vâng, nó làm. Nó không bao giờ phân bổ bộ nhớ ngoại trừ bằng cách sử dụng a. Có nhiều vấn đề với việc triển khai ngây thơ này, nhưng chúng ta hãy nói rằng nó chuyển sang sử dụng bộ cấp phát trừ trường hợp đơn giản nhất shared_ptrđược xây dựng trực tiếp từ một con trỏ và không bao giờ được sao chép hoặc di chuyển hoặc được tham chiếu theo cách khác và không có biến chứng nào khác. Vấn đề là, chỉ vì chúng ta không tưởng tượng ra một triển khai hợp lệ không tự nó chứng minh rằng nó không thể tồn tại trên lý thuyết. Tôi không nói rằng việc thực hiện như vậy thực sự có thể được tìm thấy trong thế giới thực, chỉ là tiêu chuẩn dường như không chủ động cấm nó.


IMO của bạn shared_ptrcho các loại nhỏ phân bổ bộ nhớ trên ngăn xếp. Và do đó, không đáp ứng các yêu cầu tiêu chuẩn
bartop

1
@bartop Nó không phân bổ cho bất kỳ bộ nhớ nào trên stack. _Smaller_deleter vô điều kiện là một phần của đại diện của shared_ptr. Gọi một nhà xây dựng trên không gian này không có nghĩa là phân bổ bất cứ điều gì. Nếu không, thậm chí việc giữ một con trỏ tới khối điều khiển cũng được tính là phân bổ bộ nhớ, đúng không? :-)
LF

Nhưng deleter không bắt buộc phải sao chép, vậy nó sẽ hoạt động như thế nào?
Nicol Bolas

@NicolBolas Umm ... Sử dụng std::move(__d)và quay lại allocatekhi cần sao chép.
LF
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.