Sử dụng con trỏ thông minh cho các thành viên trong lớp


159

Tôi gặp khó khăn trong việc hiểu cách sử dụng con trỏ thông minh như các thành viên lớp trong C ++ 11. Tôi đã đọc rất nhiều về con trỏ thông minh và tôi nghĩ rằng tôi hiểu cách thức unique_ptrshared_ptr/ weak_ptrcông việc nói chung. Những gì tôi không hiểu là sử dụng thực sự. Có vẻ như tất cả mọi người khuyên bạn nên sử dụng unique_ptrnhư là cách để đi gần như mọi lúc. Nhưng làm thế nào tôi có thể thực hiện một cái gì đó như thế này:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Hãy nói rằng tôi muốn thay thế con trỏ bằng con trỏ thông minh. A unique_ptrsẽ không hoạt động vì getDevice(), phải không? Vì vậy, đó là thời gian khi tôi sử dụng shared_ptrweak_ptr? Không có cách sử dụng unique_ptr? Có vẻ như đối với tôi, hầu hết các trường hợp shared_ptrcó ý nghĩa hơn trừ khi tôi sử dụng một con trỏ trong phạm vi thực sự nhỏ?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

đó là phải đường để đi không? Cảm ơn rất nhiều!


4
Nó giúp thực sự rõ ràng như suốt đời, quyền sở hữu và null có thể. Ví dụ, đã chuyển deviceđến hàm tạo của settings, bạn có muốn vẫn có thể tham chiếu đến nó trong phạm vi gọi điện thoại hay chỉ thông qua settings? Nếu sau này, unique_ptrlà hữu ích. Ngoài ra, bạn có một kịch bản trong đó giá trị trả về getDevice()null. Nếu không, chỉ cần trả lại một tài liệu tham khảo.
Keith

2
Có, a shared_ptrlà chính xác trong 8/10 trường hợp. 2/10 khác được phân chia giữa unique_ptrweak_ptr. Ngoài ra, weak_ptrthường được sử dụng để phá vỡ các tham chiếu tròn; Tôi không chắc chắn rằng việc sử dụng của bạn sẽ được coi là chính xác.
Collin Dauphinee

2
Trước hết, bạn muốn sở hữu gì cho devicethành viên dữ liệu? Trước tiên bạn phải quyết định điều đó.
juanchopanza

1
Ok, tôi hiểu rằng với tư cách là người gọi, tôi có thể sử dụng unique_ptrthay thế và từ bỏ quyền sở hữu khi gọi nhà xây dựng, nếu tôi biết tôi sẽ không cần nó nữa. Nhưng là người thiết kế của Settingslớp, tôi không biết liệu người gọi có muốn giữ một tài liệu tham khảo không. Có thể thiết bị sẽ được sử dụng ở nhiều nơi. Ok, có lẽ đó chính xác là quan điểm của bạn. Trong trường hợp đó, tôi sẽ không phải là chủ sở hữu duy nhất và đó là khi tôi sẽ sử dụng shared_ptr, tôi đoán vậy. Và: vì vậy điểm thông minh làm thay thế con trỏ, nhưng không tham chiếu, phải không?
michaelk

này-> thiết bị = thiết bị; Cũng sử dụng danh sách khởi tạo.
Nils

Câu trả lời:


202

A unique_ptrsẽ không hoạt động vì getDevice(), phải không?

Không, không nhất thiết. Điều quan trọng ở đây là xác định chính sách sở hữu phù hợp cho Deviceđối tượng của bạn , tức là ai sẽ là chủ sở hữu của đối tượng được trỏ bởi con trỏ (thông minh) của bạn.

Nó sẽ là ví dụ của Settingsđối tượng một mình ? Sẽ Deviceđối tượng phải bị phá hủy tự động khi Settingsđối tượng bị phá hủy, hoặc nó sẽ sống lâu hơn đối tượng đó?

Trong trường hợp đầu tiên, std::unique_ptrlà những gì bạn cần, vì nó làmSettings chủ sở hữu duy nhất (duy nhất) của đối tượng nhọn và là đối tượng duy nhất chịu trách nhiệm cho sự phá hủy của nó.

Theo giả định này, getDevice()nên trả về một con trỏ quan sát đơn giản (quan sát các con trỏ là các con trỏ không giữ cho vật nhọn) tồn tại. Loại con trỏ quan sát đơn giản nhất là con trỏ thô:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ LƯU Ý 1: Bạn có thể tự hỏi tại sao tôi sử dụng con trỏ thô ở đây, khi mọi người cứ nói rằng con trỏ thô là xấu, không an toàn và nguy hiểm. Trên thực tế, đó là một cảnh báo quý giá, nhưng điều quan trọng là phải đặt nó trong ngữ cảnh chính xác: con trỏ thô rất tệ khi được sử dụng để thực hiện quản lý bộ nhớ thủ công , tức là phân bổ và phân bổ các đối tượng thông qua newdelete. Khi được sử dụng hoàn toàn như một phương tiện để đạt được ngữ nghĩa tham chiếu và vượt qua các con trỏ không sở hữu, quan sát, về bản chất không có gì nguy hiểm trong các con trỏ thô, ngoại trừ có thể thực tế là người ta không nên coi thường con trỏ lơ lửng. - KẾT THÚC 1 ]

[ CHÚ THÍCH 2: Như đã xuất hiện trong các nhận xét, trong trường hợp cụ thể này, quyền sở hữu là duy nhất đối tượng sở hữu luôn được đảm bảo có mặt (tức là thành viên dữ liệu nội bộ devicesẽ không bao giờ tồn tại nullptr), chức năng getDevice()có thể (và có thể nên) trả về một tham chiếu chứ không phải là một con trỏ Trong khi điều này là đúng, tôi đã quyết định trả về một con trỏ thô ở đây bởi vì tôi muốn nói đây là một câu trả lời ngắn gọn mà người ta có thể khái quát cho trường hợp devicecó thể nullptr, và cho thấy rằng các con trỏ thô vẫn ổn miễn là người ta không sử dụng chúng cho quản lý bộ nhớ thủ công. - KẾT THÚC 2 ]


Tất nhiên, tình huống hoàn toàn khác, nếu Settingsđối tượng của bạn không có quyền sở hữu độc quyền của thiết bị. Đây có thể là trường hợp, ví dụ, nếu sự phá hủy của Settingsđối tượng không nên ngụ ý sự phá hủy của Devicevật nhọn .

Đây là điều mà chỉ bạn là người thiết kế chương trình của bạn có thể nói; từ ví dụ bạn cung cấp, thật khó để tôi biết liệu đây có phải là trường hợp hay không.

Để giúp bạn tìm ra nó, bạn có thể tự hỏi liệu có bất kỳ đối tượng nào khác ngoài Settingsviệc có quyền giữ cho Deviceđối tượng tồn tại miễn là họ giữ một con trỏ đến nó, thay vì chỉ là những người quan sát thụ động. Nếu đó thực sự là trường hợp, thì bạn cần một chính sách sở hữu chung , đó là những gì std::shared_ptrcung cấp:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Lưu ý, đó weak_ptrlà một con trỏ quan sát , không phải là một con trỏ sở hữu - nói cách khác, nó không giữ cho đối tượng nhọn tồn tại nếu tất cả các con trỏ sở hữu khác đến đối tượng nhọn đi ra khỏi phạm vi.

Ưu điểm của weak_ptrmột con trỏ thô thông thường là bạn có thể biết được liệu weak_ptrlơ lửng hay không (nghĩa là nó đang trỏ đến một đối tượng hợp lệ hay nếu đối tượng ban đầu được chỉ ra đã bị phá hủy). Điều này có thể được thực hiện bằng cách gọi expired()hàm thành viên trênweak_ptr đối tượng.


4
@LKK: Vâng, đúng rồi. A weak_ptrluôn luôn là một thay thế cho con trỏ quan sát thô. Nó an toàn hơn theo một nghĩa nào đó, bởi vì bạn có thể kiểm tra xem nó có bị treo lủng lẳng trước khi hội thảo không, nhưng nó cũng đi kèm với một số chi phí. Nếu bạn có thể dễ dàng đảm bảo rằng bạn sẽ không bỏ qua một con trỏ lơ lửng, thì bạn sẽ ổn khi quan sát các con trỏ thô
Andy Prowl 26/03/13

6
Trong trường hợp đầu tiên, có lẽ sẽ tốt hơn nếu getDevice()trả lại một tài liệu tham khảo, phải không? Vì vậy, người gọi sẽ không phải kiểm tra nullptr.
vobject

5
@chico: Không chắc ý bạn là gì. auto myDevice = settings.getDevice()sẽ tạo một thể hiện mới của kiểu Deviceđược gọi myDevicevà sao chép-xây dựng nó từ một tham chiếu được tham chiếu bởi tham chiếu getDevice()trả về. Nếu bạn muốn myDevicetrở thành một tài liệu tham khảo, bạn cần phải làm auto& myDevice = settings.getDevice(). Vì vậy, trừ khi tôi thiếu một cái gì đó, chúng tôi trở lại trong tình huống tương tự mà chúng tôi không có auto.
Andy Prowl

2
@Purrformance: Bởi vì bạn không muốn trao quyền sở hữu đối tượng - trao một unique_ptrkhách hàng có thể sửa đổi cho khách hàng sẽ mở ra khả năng khách hàng sẽ chuyển từ đó, do đó có được quyền sở hữu và để lại cho bạn một con trỏ null (duy nhất).
Andy Prowl

7
@Purrformance: Mặc dù điều đó sẽ ngăn khách hàng di chuyển (trừ khi khách hàng là một nhà khoa học điên rồ quan tâm đến const_casts), cá nhân tôi sẽ không làm điều đó. Nó cho thấy một chi tiết thực hiện, tức là thực tế rằng quyền sở hữu là duy nhất và được hiện thực hóa thông qua a unique_ptr. Tôi thấy mọi thứ theo cách này: nếu bạn muốn / cần vượt qua / trả lại quyền sở hữu, vượt qua / trả lại một con trỏ thông minh ( unique_ptrhoặc shared_ptr, tùy thuộc vào loại quyền sở hữu). Nếu bạn không muốn / cần chuyển / trả lại quyền sở hữu, hãy sử dụng một constcon trỏ ( đủ tiêu chuẩn) hoặc tham chiếu, chủ yếu tùy thuộc vào việc đối số có thể là null hay không.
Andy Prowl

0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrchỉ được sử dụng cho các vòng tham chiếu. Biểu đồ phụ thuộc phải là đồ thị acyclicdirected. Trong các con trỏ chia sẻ có 2 số tham chiếu: 1 cho shared_ptrs và 1 cho tất cả các con trỏ ( shared_ptrweak_ptr). Khi tất cả shared_ptrs được loại bỏ, con trỏ sẽ bị xóa. Khi con trỏ là cần thiết weak_ptr, locknên được sử dụng để có được con trỏ, nếu nó tồn tại.


Vì vậy, nếu tôi hiểu chính xác câu trả lời của bạn, con trỏ thông minh sẽ thay thế con trỏ thô, nhưng không nhất thiết phải là tài liệu tham khảo?
michaelk

Có thực sự có hai số tham chiếu trong một shared_ptr? Bạn có thể vui lòng giải thích tại sao? Theo tôi hiểu, weak_ptrkhông cần phải tính bởi vì nó chỉ tạo ra một cái mới shared_ptrkhi hoạt động trên đối tượng (nếu đối tượng cơ bản vẫn tồn tại).
Bjorn Pollex

@ BjornPollex: Tôi đã tạo một ví dụ ngắn cho bạn: link . Tôi đã không thực hiện mọi thứ chỉ là các nhà xây dựng sao chép và lock. phiên bản boost cũng là chủ đề an toàn khi đếm tham chiếu ( deletechỉ được gọi một lần).
Naszta

@Naszta: Ví dụ của bạn cho thấy có thể thực hiện điều này bằng cách sử dụng hai số tham chiếu, nhưng câu trả lời của bạn cho thấy điều này là bắt buộc , điều mà tôi không tin là có. Bạn có thể vui lòng làm rõ điều này trong câu trả lời của bạn?
Bjorn Pollex

1
@ BjornPollex, weak_ptr::lock()để biết đối tượng đã hết hạn hay chưa, nó phải kiểm tra "khối điều khiển" có chứa số tham chiếu đầu tiên và con trỏ tới đối tượng, vì vậy không được phá hủy khối điều khiển trong khi vẫn còn bất kỳ weak_ptrđối tượng nào đang sử dụng, vì vậy số lượng weak_ptrđối tượng phải được theo dõi, đó là những gì số lượng tham chiếu thứ hai làm. Đối tượng bị phá hủy khi số lượng ref đầu tiên giảm xuống 0, khối điều khiển bị phá hủy khi số ref thứ hai giảm xuống 0.
Jonathan Wakely
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.