Tại sao std :: shared_ptr <void> hoạt động


129

Tôi tìm thấy một số mã bằng cách sử dụng std :: shared_ptr để thực hiện dọn dẹp tùy ý khi tắt máy. Lúc đầu tôi nghĩ mã này không thể hoạt động được, nhưng sau đó tôi đã thử như sau:

#include <memory>
#include <iostream>
#include <vector>

class test {
public:
  test() {
    std::cout << "Test created" << std::endl;
  }
  ~test() {
    std::cout << "Test destroyed" << std::endl;
  }
};

int main() {
  std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>" 
            << std::endl;
  std::vector<std::shared_ptr<void>> v;
  {
    std::cout << "Creating test" << std::endl;
    v.push_back( std::shared_ptr<test>( new test() ) );
    std::cout << "Leaving scope" << std::endl;
  }
  std::cout << "Leaving main" << std::endl;
  return 0;
}

Chương trình này cung cấp đầu ra:

At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed

Tôi có một số ý tưởng về lý do tại sao điều này có thể hoạt động, liên quan đến nội bộ của std :: shared_ptrs như được triển khai cho G ++. Kể từ khi các đối tượng này quấn lại với nhau con trỏ nội bộ với các truy cập các diễn viên từ std::shared_ptr<test>đến std::shared_ptr<void>có lẽ không gây trở ngại cho các cuộc gọi của destructor. Giả định này có đúng không?

Và tất nhiên, câu hỏi quan trọng hơn nhiều: Điều này có được đảm bảo để hoạt động theo tiêu chuẩn không, hoặc có thể thay đổi thêm đối với phần bên trong của std :: shared_ptr, các triển khai khác thực sự phá vỡ mã này?


2
Thay vào đó, bạn đã mong đợi điều gì xảy ra?
Các cuộc đua nhẹ nhàng trong quỹ đạo

1
Không có diễn viên nào ở đó - đó là một chuyển đổi từ shared_ptr <test> sang shared_ptr <void>.
Alan Stokes

FYI: đây là liên kết đến một bài viết về std :: shared_ptr trong MSDN: msdn.microsoft.com/en-us/l Library / bb982026.aspx
yasouser

Câu trả lời:


98

Bí quyết là std::shared_ptrthực hiện loại tẩy. Về cơ bản, khi một cái mới shared_ptrđược tạo, nó sẽ lưu trữ bên trong một deleterhàm (có thể được đưa ra làm đối số cho hàm tạo nhưng nếu không có mặc định để gọi delete). Khi shared_ptrbị hủy, nó gọi hàm được lưu trữ đó và nó sẽ gọi hàm deleter.

Một bản phác thảo đơn giản về kiểu xóa đang diễn ra được đơn giản hóa với hàm std :: và tránh tất cả các phép đếm tham chiếu và các vấn đề khác có thể xem tại đây:

template <typename T>
void delete_deleter( void * p ) {
   delete static_cast<T*>(p);
}

template <typename T>
class my_unique_ptr {
  std::function< void (void*) > deleter;
  T * p;
  template <typename U>
  my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> ) 
     : p(p), deleter(deleter) 
  {}
  ~my_unique_ptr() {
     deleter( p );   
  }
};

int main() {
   my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double>
}
// ~my_unique_ptr calls delete_deleter<double>(p)

Khi một shared_ptrbản sao được sao chép (hoặc được xây dựng mặc định) từ một cái khác, deleter được truyền xung quanh, để khi bạn xây dựng shared_ptr<T>từ một shared_ptr<U>thông tin về hàm hủy nào sẽ gọi cũng được truyền xung quanh trong deleter.


Dường như có một sai lầm : my_shared. Tôi sẽ sửa nó nhưng chưa có đặc quyền chỉnh sửa.
Alexey Kukanov

@Alexey Kukanov, @Dennis Zickefoose: Cảm ơn vì đã chỉnh sửa tôi đã đi vắng và không thấy nó.
David Rodríguez - dribeas

2
@ user102008 bạn không cần 'std :: function' nhưng nó linh hoạt hơn một chút (có lẽ không quan trọng ở đây), nhưng điều đó không thay đổi cách thức xóa hoạt động, nếu bạn lưu trữ 'xóa_deleter <T>' như con trỏ hàm 'void (void *)' bạn đang thực hiện kiểu xóa ở đó: T đi từ loại con trỏ được lưu trữ.
David Rodríguez - dribeas

1
Hành vi này được đảm bảo theo tiêu chuẩn C ++, phải không? Tôi cần loại xóa trong một trong các lớp của mình và std::shared_ptr<void>cho phép tôi tránh khai báo một lớp bao bọc vô dụng chỉ để tôi có thể kế thừa nó từ một lớp cơ sở nhất định.
Hươu cao cổ Violet

1
@AngelusMortis: Deleter chính xác không phải là một phần của loại my_unique_ptr. Khi trong mainmẫu được khởi tạo với doubledeleter đúng được chọn nhưng đây không phải là một phần của loại my_unique_ptrvà không thể được truy xuất từ ​​đối tượng. Kiểu của deleter bị xóa khỏi đối tượng, khi một hàm nhận được một my_unique_ptr(nói theo tham chiếu rvalue), hàm đó không và không cần biết deleter là gì.
David Rodríguez - dribeas

35

shared_ptr<T> về mặt logic [*] có (ít nhất) hai thành viên dữ liệu có liên quan:

  • một con trỏ tới đối tượng đang được quản lý
  • một con trỏ tới hàm deleter sẽ được sử dụng để phá hủy nó.

Hàm deleter của bạn shared_ptr<Test>, được đưa ra theo cách bạn xây dựng nó, là hàm bình thường cho Test, nó chuyển đổi con trỏ thành Test*deletes nó.

Khi bạn đẩy bạn shared_ptr<Test>vào vectơ shared_ptr<void>, cả hai đều được sao chép, mặc dù cái đầu tiên được chuyển đổi thành void*.

Vì vậy, khi phần tử vectơ bị phá hủy lấy tham chiếu cuối cùng với nó, nó chuyển con trỏ đến một deleter phá hủy nó một cách chính xác.

Nó thực sự phức tạp hơn một chút so với điều này, bởi vì shared_ptrcó thể lấy một hàm functor thay vì chỉ là một hàm, do đó, thậm chí có thể có dữ liệu theo từng đối tượng được lưu trữ thay vì chỉ là một con trỏ hàm. Nhưng trong trường hợp này không có dữ liệu bổ sung như vậy, chỉ cần lưu trữ một con trỏ để khởi tạo một hàm mẫu, với một tham số mẫu có thể nắm bắt loại mà con trỏ phải bị xóa.

[*] về mặt logic theo nghĩa là nó có quyền truy cập vào chúng - chúng có thể không phải là thành viên của chính shared_ptr mà thay vào đó là một số nút quản lý mà nó trỏ đến.


2
+1 để đề cập rằng hàm deleter / functor được sao chép vào các trường hợp shared_ptr khác - một phần thông tin bị bỏ lỡ trong các câu trả lời khác.
Alexey Kukanov

Điều này có nghĩa là các hàm hủy cơ sở ảo không cần thiết khi sử dụng shared_ptrs?
ronag

@ronag Vâng. Tuy nhiên, tôi vẫn khuyên bạn nên tạo công cụ hủy ảo, ít nhất là nếu bạn có bất kỳ thành viên ảo nào khác. (Nỗi đau của việc vô tình quên một lần vượt xa mọi lợi ích có thể có.)
Alan Stokes

Vâng, tôi sẽ đồng ý. Thú vị không kém. Tôi biết về loại tẩy xóa chỉ chưa xem xét "tính năng" này của nó.
ronag

2
@ronag: không yêu cầu hủy bỏ ảo nếu bạn tạo shared_ptr sử dụng trực tiếp với loại thích hợp hoặc nếu bạn sử dụng make_shared. Tuy nhiên, vẫn là một ý tưởng tốt vì loại con trỏ có thể thay đổi từ khi xây dựng cho đến khi nó được lưu trữ trong shared_ptr: base *p = new derived; shared_ptr<base> sp(p);, shared_ptrđối với các đối tượng thì basekhông derived, vì vậy bạn cần một hàm hủy ảo. Mẫu này có thể phổ biến với các mẫu nhà máy, ví dụ.
David Rodríguez - dribeas

10

Nó hoạt động bởi vì nó sử dụng loại tẩy.

Về cơ bản, khi bạn xây dựng một shared_ptr, nó sẽ vượt qua một đối số phụ (mà bạn thực sự có thể cung cấp nếu bạn muốn), đó là functor deleter.

Functor mặc định này chấp nhận làm đối số một con trỏ để nhập loại bạn sử dụng shared_ptr, do đó voidở đây, đưa nó phù hợp với kiểu tĩnh bạn đã sử dụng testở đây và gọi hàm hủy trên đối tượng này.

Bất kỳ khoa học đủ tiên tiến đều cảm thấy như ma thuật, phải không?


5

Các nhà xây dựng shared_ptr<T>(Y *p)thực sự dường như đang gọi shared_ptr<T>(Y *p, D d)nơid một deleter được tạo tự động cho đối tượng.

Khi điều này xảy ra, loại đối tượng Yđược biết đến, do đó, deleter cho shared_ptrđối tượng này biết nên gọi hàm hủy nào và thông tin này không bị mất khi con trỏ được lưu trữ trong một vectơshared_ptr<void> .

Thật vậy, thông số kỹ thuật yêu cầu rằng để một shared_ptr<T>đối tượng nhận được phải chấp nhận một shared_ptr<U>đối tượng thì nó phải đúng và U*phải được chuyển đổi hoàn toàn thành a T*và điều này chắc chắn là T=voidvì bất kỳ con trỏ nào cũng có thể được chuyển đổi thành void*ẩn. Không có gì được nói về deleter sẽ không hợp lệ vì vậy thực sự các thông số kỹ thuật đang bắt buộc rằng điều này sẽ hoạt động chính xác.

Về mặt kỹ thuật, IIRC a shared_ptr<T>giữ một con trỏ tới một đối tượng ẩn có chứa bộ đếm tham chiếu và một con trỏ tới đối tượng thực tế; bằng cách lưu trữ deleter trong cấu trúc ẩn này, có thể làm cho tính năng ma thuật rõ ràng này hoạt động trong khi vẫn giữ được độ shared_ptr<T>lớn như một con trỏ thông thường (tuy nhiên, việc hủy bỏ con trỏ đòi hỏi một sự gián tiếp kép

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*được chuyển đổi hoàn toàn sang void*, do đó shared_ptr<Test>hoàn toàn có thể chuyển đổi thànhshared_ptr<void> từ, từ bộ nhớ. Điều này hoạt động vì shared_ptrđược thiết kế để kiểm soát sự hủy diệt trong thời gian chạy, không phải thời gian biên dịch, chúng sẽ sử dụng nội bộ kế thừa để gọi hàm hủy thích hợp như lúc phân bổ.


Bạn có thể giải thích thêm? Tôi vừa mới đăng một câu hỏi tương tự, thật tuyệt nếu bạn có thể giúp!
Bruce

3

Tôi sẽ trả lời câu hỏi này (2 năm sau) bằng cách sử dụng shared_ptr rất đơn giản mà người dùng sẽ hiểu.

Đầu tiên tôi sẽ đến một vài lớp bên, shared_ptr_base, sp_counted_base sp_counted_impl, và check_deleter cuối cùng là một mẫu.

class sp_counted_base
{
 public:
    sp_counted_base() : refCount( 1 )
    {
    }

    virtual ~sp_deleter_base() {};
    virtual void destruct() = 0;

    void incref(); // increases reference count
    void decref(); // decreases refCount atomically and calls destruct if it hits zero

 private:
    long refCount; // in a real implementation use an atomic int
};

template< typename T > class sp_counted_impl : public sp_counted_base
{
 public:
   typedef function< void( T* ) > func_type;
    void destruct() 
    { 
       func(ptr); // or is it (*func)(ptr); ?
       delete this; // self-destructs after destroying its pointer
    }
   template< typename F >
   sp_counted_impl( T* t, F f ) :
       ptr( t ), func( f )

 private:

   T* ptr; 
   func_type func;
};

template< typename T > struct checked_deleter
{
  public:
    template< typename T > operator()( T* t )
    {
       size_t z = sizeof( T );
       delete t;
   }
};

class shared_ptr_base
{
private:
     sp_counted_base * counter;

protected:
     shared_ptr_base() : counter( 0 ) {}

     explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {}

     ~shared_ptr_base()
     {
        if( counter )
          counter->decref();
     }

     shared_ptr_base( shared_ptr_base const& other )
         : counter( other.counter )
     {
        if( counter )
            counter->addref();
     }

     shared_ptr_base& operator=( shared_ptr_base& const other )
     {
         shared_ptr_base temp( other );
         std::swap( counter, temp.counter );
     }

     // other methods such as reset
};

Bây giờ tôi sẽ tạo hai hàm "miễn phí" được gọi là make_sp_counted_impl, nó sẽ trả về một con trỏ tới một hàm mới được tạo.

template< typename T, typename F >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func )
{
    try
    {
       return new sp_counted_impl( ptr, func );
    }
    catch( ... ) // in case the new above fails
    {
        func( ptr ); // we have to clean up the pointer now and rethrow
        throw;
    }
}

template< typename T > 
sp_counted_impl<T> * make_sp_counted_impl( T* ptr )
{
     return make_sp_counted_impl( ptr, checked_deleter<T>() );
}

Ok, hai hàm này rất cần thiết cho những gì sẽ xảy ra tiếp theo khi bạn tạo shared_ptr thông qua chức năng templated.

template< typename T >
class shared_ptr : public shared_ptr_base
{

 public:
   template < typename U >
   explicit shared_ptr( U * ptr ) :
         shared_ptr_base( make_sp_counted_impl( ptr ) )
   {
   }

  // implement the rest of shared_ptr, e.g. operator*, operator->
};

Lưu ý những gì xảy ra ở trên nếu T không có giá trị và U là lớp "kiểm tra" của bạn. Nó sẽ gọi make_sp_counted_impl () bằng một con trỏ tới U, không phải là một con trỏ tới T. Việc quản lý hủy diệt hoàn toàn được thực hiện thông qua đây. Lớp shared_ptr_base quản lý việc đếm tham chiếu liên quan đến sao chép và gán, v.v ... Chính lớp shared_ptr quản lý việc sử dụng an toàn các kiểu quá tải toán tử (->, * vv).

Do đó, mặc dù bạn có shared_ptr để bỏ trống, bên dưới bạn đang quản lý một con trỏ thuộc loại bạn đã chuyển sang mới. Lưu ý rằng nếu bạn chuyển đổi con trỏ của bạn thành một khoảng trống * trước khi đưa nó vào shared_ptr, nó sẽ không biên dịch được trên check_delete để bạn thực sự an toàn ở đó.

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.