+8

Tìm hiểu SmartPointer trong C++ - Phần 2

Smart Pointer

Tiếp nối bài viết trước về Smart Pointer, trong bài này tôi sẽ giới thiệu với các bạn về một số loại smartpointer thường được sử dụng trong C++

  • unique_ptr
  • shared_ptr
  • weak_ptr
  • scoped_ptr

std::unique_ptr

Đây là loại smartpointer được sử dụng mặc định, nó là chuẩn chung trong C++11.

Như tên gọi, std::unique_ptr là quản lý duy nhất của vùng nhớ được nắm giữ, mỗi một std::unique_ptr giữ một con trỏ và sẽ xoá nó ở phương thức huỷ (trừ phi bạn customize lại phương thức huỷ).

Điều này cho phép bạn diễn đạt mục đích trong một interface. Ví dụ:

std::unique_ptr<House> buildAHouse();

Ví dụ này đưa cho bạn một con trỏ tới một ngôi nhà mà bạn là chủ sở hữu của nó, không ai khác được xoá con trỏ này trừ unique_ptr được trả lại thông qua hàm này. Khi bạn nhận được ownership của con trỏ này, bạn được phép chỉnh sửa dữ liệu trong vùng nhớ mà nó trỏ tới. Chú ý rằng, std::unique_ptr thường được sử dụng ở các hàm factory.

Thực tế, thay vì cần phải quản lý bộ nhớ một cách thủ công, std::unique_ptr bao bọc một con trỏ thông thường và do đó nó tương thích với tính đa hình của C++.

Chúng ta cũng có thể sử dụng std::unique_ptr như tham số hàm. Ví dụ

class House
{
public:
    House(std::unique_ptr<PileOfWood> wood);
    ...

Chú ý rằng, mặc dù bạn nhận được một unique_ptr nhưng bạn không thể đảm bảo sẽ không có ai truy cập vào con trỏ này. Thực tế, nếu trong một đoạn code hay ngữ cảnh khác cũng đang giữ con trỏ bên trong unique_ptr của bạn, họ có thể chỉnh sửa nội dung object trong unique_ptr và nó cũng ảnh hưởng tới ngữ cảnh hiện tại của bạn. Nhưng vì bạn là sở hữu của unique_ptr, bạn có thể cho phép chỉnh sửa nội dung của object, hoặc nếu không, bạn có thể sử dụng const để tránh những chỉnh sửa không mong muốn.

std::unique_ptr<const House> buildAHouse();

Để đảm bảo chỉ có duy nhất một unique_ptr nắm giữ vùng nhớ, std::unique_ptr không thể copy. Chủ sở hữu của unique_ptr có thể chuyển sang một chủ sở hữu khác thông qua moving

std::unique_ptr<int> p1 = std::make_unique(42);
std::unique_ptr<int> p2 = move(p1); // now p2 hold the resource and p1 no longer hold anything

std::shared_ptr

Khác với unique_ptr, một vùng nhớ có thể được nắm giữ bởi nhiều shared_ptr cùng lúc. shared_ptr bản thân nó sẽ nắm giữ só lượng shared_ptr đang trỏ tới cùng vùng nhớ, khi shared_ptr cuối cùng bị huỷ, nó sẽ giải phóng vùng nhớ dùng chung đó.

Do đó, shared_ptr cho phép copy, nhưng cơ chế reference-counting sẽ đảm bảo rằng vùng nhớ chỉ bị huỷ duy nhất một lần.

std::shared_ptr giường như rất tốt cho vấn đề quản lý bộ nhớ, nó có thể truyền đi qua lại giữa các hàm, các object và vẫn giữ vùng nhớ an toàn, nhưng shared_ptr không được sử dụng mặc định vì một số lý do sau:

  • Cùng lúc có nhiều holder quản lý tài nguyên làm cho hệ thống trở lên phức tạp hơn là sử dụng một holder duy nhất như unique_ptr.
  • Cùng lúc có nhiều holder sẽ làm cho việc quản lý thread khó khăn hơn
  • Nó làm cho mặc dù object mong muốn không ở trạng thái shared nhưng vẫn phải để là shared
  • shared_ptr làm ảnh hưởng tới hiệu năng của hệ thống vì liên quan tới reference-counting.

Tất nhiên shared_ptr sử dụng tốt đối với hệ thống cần sự chia sẻ.

std::weak_ptr

weak_ptr có thể giữ một tham chiếu tới một shared object được quản lý bởi shared_ptr, nhưng nó không làm tăng reference count. Điều này có nghĩa là nếu không còn shared_ptr nào nắm giữ đối tượng, đối tượng sẽ bị huỷ mặc dù có thể còn một số weak_ptr khác đang trỏ tới nó.

Vì lý do này, khi sử dụng một weak_ptr, chúng ta cần kiểm tra xem đối tượng mà nó trỏ tới còn valid hay không. Để làm điều đó, chúng ta có thể dùng một shared_ptr

void useMyWeakPointer(std::weak_ptr<int> wp)
{
    if (std::shared_ptr<int> sp = wp.lock())
    {
        // the resource is still here and can be used
    }
    else
    {
        // the resource is no longer here
    }
}

weak_ptr thường được sử dụng nhằm tránh việc tham chiếu vòng của shared_ptr. Chúng ta cùng xem xét ví dụ sau:

1. struct House
2. {
3.     std::shared_ptr<House> neighbour;
4. };
5.  
6. std::shared_ptr<House> house1 = std::make_shared<House>();
7. std::shared_ptr<House> house2 = std::make_shared<House>();
8. house1->neighbour = house2;
9. house2->neighbour = house1;

Sẽ không có đối tượng house nào được huỷ khi đến cuối đoạn code này, bởi vì shared_ptr đang trỏ tới đối tượng house còn lại. Mỗi đối tượng house sau khi khởi tạo thì refercence count được tăng lên 1, sau khi thực hiện lệnh gán ở dòng code 9 và 10, reference count của mỗi object tăng lên 2. Do đó, khi hết scope, 2 đối tượng house này sẽ không được huỷ.

Để tránh trường hợp này xảy ra, chúng ta cần sử dụng weak_ptr

1. struct House
2. {
3.     std::weak_ptr<House> neighbour;
4. };
5.  
6. std::shared_ptr<House> house1 = std::make_shared<House>();
7. std::shared_ptr<House> house2 = std::make_shared<House>();
8. house1->neighbour = house2;
9. house2->neighbour = house1;

boost::scoped_ptr

Nếu các bạn sử dụng thư viện boost thì sẽ thấy có thêm scoped_ptr. scoped_ptr đơn giản chỉ không cho phép sao chép hay di chuyển khi khởi tạo. Do đó scoped_ptr sẽ sở hữu resource và không cho phép thay đổi sở hữu. Vì vậy scoped_ptr chỉ dùng được trong một scope, hoặc dùng như một member của đối tượng. Tất nhiên, scoped_ptr cũng là một smart pointer, nó cũng sẽ huỷ đối tượng khi nó được huỷ.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí