Có phải std :: unique_ptr <T> để biết định nghĩa đầy đủ của T không?


248

Tôi có một số mã trong một tiêu đề trông như thế này:

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};

Nếu tôi bao gồm tiêu đề này trong một cpp không bao gồm Thingđịnh nghĩa loại, thì điều này không biên dịch theo VS2010-SP1:

1> C: \ Tệp chương trình (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): lỗi C2027: sử dụng loại không xác định 'Thing'

Thay thế std::unique_ptrbằng std::shared_ptrvà nó biên dịch.

Vì vậy, tôi đoán rằng đó std::unique_ptrlà triển khai hiện tại của VS2010 yêu cầu định nghĩa đầy đủ và hoàn toàn phụ thuộc vào triển khai.

Hoặc là nó? Có điều gì đó trong các yêu cầu tiêu chuẩn của nó khiến cho std::unique_ptrviệc triển khai không thể chỉ hoạt động với một tuyên bố chuyển tiếp không? Nó cảm thấy kỳ lạ vì nó chỉ nên giữ một con trỏ Thing, phải không?


20
Giải thích tốt nhất về thời điểm bạn làm và không cần một loại hoàn chỉnh với con trỏ thông minh C ++ 0x là "Các loại không đầy đủ và shared_ptr/ unique_ptr" Bảng ở cuối sẽ trả lời câu hỏi của bạn.
James McNellis

17
Cảm ơn con trỏ James. Tôi đã quên nơi tôi đặt cái bàn đó! :-)
Howard Hinnant


5
@JamesMcNellis Liên kết đến trang web của Howard Hinnant không hoạt động. Đây là phiên bản web.archive.org của nó. Trong mọi trường hợp, anh ấy đã trả lời nó hoàn hảo bên dưới với cùng một nội dung :-)
Ela782

Một lời giải thích tốt khác được đưa ra trong Mục 22 của C ++ hiện đại hiệu quả của Scott Meyers.
Fred Schoen

Câu trả lời:


328

Thông qua từ đây .

Hầu hết các mẫu trong thư viện chuẩn C ++ yêu cầu chúng phải được khởi tạo với các kiểu hoàn chỉnh. Tuy nhiên shared_ptrunique_ptrmột phần ngoại lệ. Một số, nhưng không phải tất cả các thành viên của họ có thể được khởi tạo với các loại không đầy đủ. Động lực cho việc này là để hỗ trợ các thành ngữ như pimpl sử dụng con trỏ thông minh và không có nguy cơ hành vi không xác định.

Hành vi không xác định có thể xảy ra khi bạn có một loại không đầy đủ và bạn gọi deletenó:

class A;
A* a = ...;
delete a;

Trên đây là mã hợp pháp. Nó sẽ biên dịch. Trình biên dịch của bạn có thể hoặc không thể phát ra cảnh báo cho mã ở trên như ở trên. Khi nó thực thi, những điều xấu có thể sẽ xảy ra. Nếu bạn rất may mắn chương trình của bạn sẽ sụp đổ. Tuy nhiên, một kết quả có thể xảy ra hơn là chương trình của bạn sẽ âm thầm rò rỉ bộ nhớ như ~A()sẽ không được gọi.

Sử dụng auto_ptr<A>trong ví dụ trên không giúp được gì. Bạn vẫn nhận được hành vi không xác định giống như bạn đã sử dụng một con trỏ thô.

Tuy nhiên, sử dụng các lớp không đầy đủ ở một số nơi là rất hữu ích! Đây là nơi shared_ptrunique_ptrgiúp đỡ. Việc sử dụng một trong những con trỏ thông minh này sẽ cho phép bạn thoát khỏi một loại không hoàn chỉnh, trừ khi cần phải có một loại hoàn chỉnh. Và quan trọng nhất, khi cần phải có một loại hoàn chỉnh, bạn sẽ gặp lỗi thời gian biên dịch nếu bạn cố gắng sử dụng con trỏ thông minh với một loại không đầy đủ tại thời điểm đó.

Không còn hành vi không xác định:

Nếu mã của bạn biên dịch, thì bạn đã sử dụng một loại hoàn chỉnh ở mọi nơi bạn cần.

class A
{
    class impl;
    std::unique_ptr<impl> ptr_;  // ok!

public:
    A();
    ~A();
    // ...
};

shared_ptrunique_ptryêu cầu một loại hoàn chỉnh ở những nơi khác nhau. Những lý do rất mơ hồ, phải làm với một deleter động so với một deleter tĩnh. Những lý do chính xác không quan trọng. Trong thực tế, trong hầu hết các mã, điều thực sự không quan trọng đối với bạn để biết chính xác nơi cần một loại hoàn chỉnh. Chỉ cần mã, và nếu bạn nhận được nó sai, trình biên dịch sẽ cho bạn biết.

Tuy nhiên, trong trường hợp nó hữu ích cho bạn, đây là bảng ghi lại một số thành viên shared_ptrunique_ptrliên quan đến các yêu cầu về tính đầy đủ. Nếu thành viên yêu cầu một loại hoàn chỉnh, thì mục nhập có "C", nếu không, mục nhập bảng được điền "I".

Complete type requirements for unique_ptr and shared_ptr

                            unique_ptr       shared_ptr
+------------------------+---------------+---------------+
|          P()           |      I        |      I        |
|  default constructor   |               |               |
+------------------------+---------------+---------------+
|      P(const P&)       |     N/A       |      I        |
|    copy constructor    |               |               |
+------------------------+---------------+---------------+
|         P(P&&)         |      I        |      I        |
|    move constructor    |               |               |
+------------------------+---------------+---------------+
|         ~P()           |      C        |      I        |
|       destructor       |               |               |
+------------------------+---------------+---------------+
|         P(A*)          |      I        |      C        |
+------------------------+---------------+---------------+
|  operator=(const P&)   |     N/A       |      I        |
|    copy assignment     |               |               |
+------------------------+---------------+---------------+
|    operator=(P&&)      |      C        |      I        |
|    move assignment     |               |               |
+------------------------+---------------+---------------+
|        reset()         |      C        |      I        |
+------------------------+---------------+---------------+
|       reset(A*)        |      C        |      C        |
+------------------------+---------------+---------------+

Bất kỳ hoạt động nào yêu cầu chuyển đổi con trỏ đều yêu cầu các loại hoàn chỉnh cho cả hai unique_ptrshared_ptr.

Hàm unique_ptr<A>{A*}tạo có thể thoát khỏi chỉ với phần chưa hoàn thành Anếu trình biên dịch không bắt buộc phải thiết lập lệnh gọi đến ~unique_ptr<A>(). Ví dụ, nếu bạn đặt unique_ptrheap, bạn có thể thoát khỏi một cách không đầy đủ A. Thông tin chi tiết về điểm này có thể được tìm thấy trong câu trả lời của BarryTheHatchet tại đây .


3
Câu trả lời tuyệt vời. Tôi sẽ +5 nếu tôi có thể. Tôi chắc chắn rằng tôi sẽ đề cập lại điều này trong dự án tiếp theo của tôi, trong đó tôi đang cố gắng sử dụng đầy đủ các con trỏ thông minh.
matthias

4
Nếu ai đó có thể giải thích ý nghĩa của cái bàn thì tôi đoán nó sẽ giúp được nhiều người hơn
Ghita

8
Thêm một lưu ý: Một hàm tạo của lớp sẽ tham chiếu các hàm hủy của các thành viên của nó (đối với trường hợp ném ngoại lệ, các hàm hủy đó cần được gọi). Vì vậy, trong khi hàm hủy của unique_ptr cần một kiểu hoàn chỉnh, thì nó không đủ để có một hàm hủy được xác định bởi người dùng trong một lớp - nó cũng cần một hàm tạo.
Julian Schaub - litb

7
@Mehrdad: Quyết định này được đưa ra cho C ++ 98, trước thời của tôi. Tuy nhiên, tôi tin rằng quyết định này xuất phát từ mối quan tâm về khả năng thực thi và độ khó của đặc tả kỹ thuật (tức là chính xác các bộ phận của một container làm hoặc không yêu cầu một loại hoàn chỉnh). Ngay cả ngày nay, với 15 năm kinh nghiệm kể từ C ++ 98, sẽ là một nhiệm vụ không hề nhỏ để vừa thư giãn đặc tả container trong lĩnh vực này, vừa đảm bảo rằng bạn không vi phạm các kỹ thuật triển khai hoặc tối ưu hóa quan trọng. Tôi nghĩ rằng nó có thể được thực hiện. Tôi biết nó sẽ là rất nhiều công việc. Tôi biết một người đang cố gắng.
Howard Hinnant

9
Bởi vì không rõ ràng từ các ý kiến ​​trên, đối với bất kỳ ai gặp vấn đề này vì họ định nghĩa một unique_ptrbiến thành viên của một lớp, chỉ cần khai báo rõ ràng một hàm hủy (và hàm tạo) trong khai báo lớp (trong tệp tiêu đề) và tiến hành xác định chúng trong tệp nguồn (và đặt tiêu đề với khai báo đầy đủ của lớp được trỏ vào tệp nguồn) để ngăn trình biên dịch tự động chèn vào hàm tạo hoặc hàm hủy trong tệp tiêu đề (gây ra lỗi). stackoverflow.com/a/13414884/368896 cũng giúp nhắc nhở tôi về điều này.
Dan Nissenbaum

42

Trình biên dịch cần định nghĩa của Thing để tạo hàm hủy mặc định cho MyClass. Nếu bạn khai báo rõ ràng hàm hủy và di chuyển triển khai (trống) của nó sang tệp CPP, mã sẽ biên dịch.


5
Tôi nghĩ rằng đây là cơ hội hoàn hảo để sử dụng một chức năng mặc định. MyClass::~MyClass() = default;trong tệp thực thi dường như ít có khả năng vô tình bị xóa sau khi xuống đường bởi ai đó cho rằng cơ thể của kẻ hủy diệt đã bị xóa chứ không phải cố tình để trống.
Dennis Zickefoose

@Dennis Zickefoose: Thật không may, OP đang sử dụng VC ++ và VC ++ chưa hỗ trợ các thành viên lớp defaulted và deleted.
ildjarn

6
+1 cho cách di chuyển cửa vào tệp .cpp. Ngoài ra, có vẻ như MyClass::~MyClass() = defaultkhông chuyển nó vào tập tin thực hiện trên Clang. (chưa?)
Eonil

Bạn cũng cần chuyển việc triển khai hàm tạo sang tệp CPP, ít nhất là trên VS 2017. Xem ví dụ câu trả lời này: stackoverflow.com/a/27624369/5124002
jciloa

15

Điều này không phụ thuộc vào việc thực hiện. Lý do nó hoạt động là vì shared_ptrxác định hàm hủy chính xác để gọi trong thời gian chạy - nó không phải là một phần của chữ ký loại. Tuy nhiên, hàm unique_ptrhủy một phần của kiểu của nó và nó phải được biết tại thời điểm biên dịch.


8

Có vẻ như các câu trả lời hiện tại không chính xác tại sao hàm tạo (hoặc hàm hủy) mặc định là vấn đề nhưng các câu trả lời trống được khai báo trong cpp thì không.

Đây là những gì đang xảy ra:

Nếu lớp bên ngoài (ví dụ MyClass) không có hàm tạo hoặc hàm hủy thì trình biên dịch sẽ tạo các lớp mặc định. Vấn đề với điều này là về cơ bản trình biên dịch sẽ chèn hàm tạo / hàm hủy mặc định trong tệp .hpp. Điều này có nghĩa là mã cho bộ điều khiển / bộ hủy mặc định được biên dịch cùng với nhị phân của máy chủ thực thi, không cùng với các nhị phân của thư viện của bạn. Tuy nhiên định nghĩa này không thể thực sự xây dựng các lớp một phần. Vì vậy, khi trình liên kết đi vào nhị phân của thư viện của bạn và cố gắng lấy hàm tạo / hàm hủy, nó không tìm thấy bất kỳ và bạn gặp lỗi. Nếu mã xây dựng / mã hủy nằm trong .cpp của bạn thì tệp nhị phân thư viện của bạn có sẵn để liên kết.

Điều này không liên quan gì đến việc sử dụng unique_ptr hoặc shared_ptr và các câu trả lời khác dường như có thể là lỗi khó hiểu trong VC ++ cũ để triển khai unique_ptr (VC ++ 2015 hoạt động tốt trên máy của tôi).

Vì vậy, đạo đức của câu chuyện là tiêu đề của bạn cần không có bất kỳ định nghĩa hàm tạo / hàm hủy. Nó chỉ có thể chứa tuyên bố của họ. Ví dụ, ~MyClass()=default;trong hpp sẽ không hoạt động. Nếu bạn cho phép trình biên dịch chèn hàm tạo hoặc hàm hủy mặc định, bạn sẽ gặp lỗi liên kết.

Một lưu ý khác: Nếu bạn vẫn gặp lỗi này ngay cả sau khi bạn có hàm tạo và hàm hủy trong tệp cpp thì rất có thể lý do là thư viện của bạn không được biên dịch đúng. Ví dụ, một lần tôi chỉ đơn giản thay đổi loại dự án từ Bảng điều khiển sang Thư viện trong VC ++ và tôi đã gặp lỗi này vì VC ++ không thêm ký hiệu tiền xử lý _LIB và điều đó tạo ra chính xác thông báo lỗi.


Cảm ơn bạn! Đó là một lời giải thích rất ngắn gọn về một trò hề C ++ khó hiểu. Cứu tôi rất nhiều rắc rối.
JPNotADragon

4

Chỉ để hoàn thiện:

Tiêu đề: À

class B; // forward declaration

class A
{
    std::unique_ptr<B> ptr_;  // ok!  
public:
    A();
    ~A();
    // ...
};

Nguồn A.cpp:

class B {  ...  }; // class definition

A::A() { ... }
A::~A() { ... }

Định nghĩa của lớp B phải được xem bởi hàm tạo, hàm hủy và bất cứ thứ gì có thể xóa hoàn toàn B. (Mặc dù hàm tạo không xuất hiện trong danh sách trên, trong VS2017, ngay cả hàm tạo cũng cần định nghĩa của B. Và điều này có ý nghĩa khi xem xét trong trường hợp có ngoại lệ trong hàm tạo, unique_ptr lại bị hủy.)


1

Định nghĩa đầy đủ của Điều được yêu cầu tại điểm khởi tạo mẫu. Đây là lý do chính xác tại sao thành ngữ pimpl biên dịch.

Nếu không thể, mọi người sẽ không hỏi những câu như thế này .


-2

Câu trả lời đơn giản là chỉ sử dụng shared_ptr thay thế.


-7

Đôi vơi tôi,

QList<QSharedPointer<ControllerBase>> controllers;

Chỉ cần bao gồm tiêu đề ...

#include <QSharedPointer>

Trả lời không liên quan và không liên quan đến câu hỏi.
Mikus
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.