จำเป็นต้องใช้ std :: unique_ptr <T> เพื่อทราบความหมายแบบเต็มของ T หรือไม่?


248

ฉันมีรหัสในส่วนหัวที่มีลักษณะเช่นนี้:

#include <memory>

class Thing;

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

ถ้าฉันรวมส่วนหัวนี้ใน cpp ที่ไม่ได้มีการThingกำหนดประเภทแล้วนี่ไม่ได้รวบรวมภายใต้ VS2010-SP1:

1> ไฟล์ C: \ Program (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): ข้อผิดพลาด C2027: การใช้ประเภท 'สิ่งที่ไม่ได้กำหนด'

แทนที่std::unique_ptrด้วยstd::shared_ptrและมันจะรวบรวม

ดังนั้นฉันเดาว่านี่เป็นการนำไปใช้ของ VS2010 ในปัจจุบันstd::unique_ptrซึ่งต้องการคำจำกัดความเต็มรูปแบบและขึ้นอยู่กับการนำไปใช้โดยสิ้นเชิง

หรือมันคืออะไร? มีบางสิ่งในข้อกำหนดมาตรฐานที่ทำให้เป็นไปไม่ได้std::unique_ptrที่การนำไปใช้เพื่อทำงานกับการประกาศล่วงหน้าเท่านั้น มันรู้สึกแปลกที่ควรถือพอยน์เตอร์ไว้เท่านั้นThingไม่ใช่เหรอ?


20
คำอธิบายที่ดีที่สุดเมื่อคุณทำและไม่จำเป็นต้องใช้ตัวชี้สมาร์ท C ++ 0x แบบสมบูรณ์shared_ptrunique_ptrคือ Howard "ประเภทที่ไม่สมบูรณ์และ/ " ตารางของท้ายที่สุดควรตอบคำถามของคุณ
James McNellis

17
ขอบคุณสำหรับตัวชี้เจมส์ ฉันลืมที่ฉันวางโต๊ะนั้น! :-)
Howard Hinnant


5
@JamesMcNellis ลิงก์ไปยังเว็บไซต์ของ Howard Hinnant หยุดทำงาน นี่คือเวอร์ชัน web.archive.orgของมัน ไม่ว่าในกรณีใดเขาตอบอย่างสมบูรณ์แบบด้านล่างด้วยเนื้อหาเดียวกัน :-)
Ela782

คำอธิบายที่ดีอีกข้อหนึ่งอยู่ในข้อ 22 ของ C ++ ที่ทันสมัยของ Scott Meyers
Fred Schoen

คำตอบ:


328

บุญธรรมจากที่นี่

เทมเพลตส่วนใหญ่ในไลบรารีมาตรฐาน C ++ ต้องการให้อินสแตนซ์นั้นมีประเภทสมบูรณ์ อย่างไรก็ตามshared_ptrและunique_ptrเป็นข้อยกเว้นบางส่วน สมาชิกบางคน แต่ไม่ใช่ทั้งหมดสามารถสร้างอินสแตนซ์ที่มีชนิดไม่สมบูรณ์ แรงจูงใจในการทำเช่นนี้คือการสนับสนุนสำนวนเช่นpimplโดยใช้พอยน์เตอร์อัจฉริยะและไม่เสี่ยงต่อพฤติกรรมที่ไม่ได้กำหนด

พฤติกรรมที่ไม่ได้กำหนดสามารถเกิดขึ้นได้เมื่อคุณมีประเภทที่ไม่สมบูรณ์และคุณโทรหาdeleteมัน:

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

ข้างต้นเป็นรหัสทางกฎหมาย มันจะรวบรวม คอมไพเลอร์ของคุณอาจมีหรือไม่มีการปล่อยคำเตือนสำหรับโค้ดด้านบนเช่นด้านบน เมื่อดำเนินการสิ่งที่ไม่ดีอาจเกิดขึ้นได้ หากคุณโชคดีมากโปรแกรมของคุณจะขัดข้อง อย่างไรก็ตามผลลัพธ์ที่เป็นไปได้มากกว่าคือโปรแกรมของคุณจะปิดหน่วยความจำอย่างเงียบ ๆ โดย~A()ไม่เรียกใช้

การใช้auto_ptr<A>ในตัวอย่างด้านบนไม่ได้ช่วยอะไร คุณยังคงได้รับพฤติกรรมที่ไม่ได้กำหนดเหมือนกับว่าคุณใช้ตัวชี้แบบดิบ

อย่างไรก็ตามการใช้คลาสที่ไม่สมบูรณ์ในบางสถานที่นั้นมีประโยชน์มาก! นี่คือที่shared_ptrและunique_ptrความช่วยเหลือ การใช้พอยน์เตอร์อัจฉริยะอย่างใดอย่างหนึ่งเหล่านี้จะช่วยให้คุณสามารถหลีกเลี่ยงประเภทที่ไม่สมบูรณ์ยกเว้นในกรณีที่จำเป็นต้องมีประเภทที่สมบูรณ์ และที่สำคัญที่สุดเมื่อจำเป็นต้องมีชนิดที่สมบูรณ์คุณจะได้รับข้อผิดพลาดในการคอมไพล์เวลาหากคุณพยายามใช้ตัวชี้สมาร์ทที่มีประเภทไม่สมบูรณ์ในจุดนั้น

ไม่มีพฤติกรรมที่ไม่ได้กำหนดเพิ่มเติม:

หากรหัสของคุณรวบรวมแสดงว่าคุณใช้ประเภทสมบูรณ์ทุกที่ที่คุณต้องการ

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

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

shared_ptrและunique_ptrต้องการประเภทที่สมบูรณ์ในสถานที่ต่างกัน เหตุผลที่คลุมเครือเกี่ยวข้องกับ deleter แบบไดนามิกและ deleter แบบคงที่ เหตุผลที่แม่นยำนั้นไม่สำคัญ ในความเป็นจริงในรหัสส่วนใหญ่มันไม่ได้เป็นสิ่งสำคัญสำหรับคุณที่จะรู้ว่าประเภทที่สมบูรณ์ต้อง เพียงแค่โค้ดและถ้าคุณเข้าใจผิดคอมไพเลอร์จะบอกคุณ

อย่างไรก็ตามในกรณีที่เป็นประโยชน์กับคุณนี่คือตารางที่จัดทำเอกสารสมาชิกหลายคนshared_ptrและunique_ptrเกี่ยวกับข้อกำหนดครบถ้วน หากสมาชิกต้องการชนิดที่สมบูรณ์รายการจะมี "C" มิฉะนั้นรายการในตารางจะเต็มไปด้วย "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        |
+------------------------+---------------+---------------+

ดำเนินการใด ๆ ที่กำหนดให้แปลงตัวชี้ต้องใช้ชนิดที่สมบูรณ์แบบสำหรับทั้งสองและunique_ptrshared_ptr

unique_ptr<A>{A*}คอนสตรัคได้รับไปด้วยไม่สมบูรณ์แต่ถ้าคอมไพเลอร์ไม่จำเป็นต้องตั้งค่าการเรียกร้องให้A ~unique_ptr<A>()ตัวอย่างเช่นถ้าคุณใส่ในกองที่คุณสามารถรับไปกับการไม่สมบูรณ์unique_ptr Aรายละเอียดเพิ่มเติมในประเด็นนี้สามารถพบได้ในBarryTheHatchet ของคำตอบที่นี่


3
คำตอบที่ยอดเยี่ยม ฉันจะเพิ่ม 5 ถ้าทำได้ ฉันแน่ใจว่าฉันจะอ้างอิงถึงเรื่องนี้ในโครงการต่อไปของฉันซึ่งฉันกำลังพยายามใช้ประโยชน์จากพอยน์เตอร์อัจฉริยะอย่างเต็มรูปแบบ
matthias

4
ถ้าใครสามารถอธิบายได้ว่าตารางหมายความว่าฉันเดาว่ามันจะช่วยให้ผู้คนจำนวนมากขึ้น
Ghita

8
อีกหนึ่งหมายเหตุ: ตัวสร้างคลาสจะอ้างอิง destructors ของสมาชิก (สำหรับกรณีที่มีข้อยกเว้นเกิดขึ้นจะต้องเรียก destructors เหล่านั้น) ดังนั้นในขณะที่ destructor ของ unique_ptr ต้องการชนิดที่สมบูรณ์ แต่ก็ไม่เพียงพอที่จะมี destructor ที่ผู้ใช้กำหนดในคลาส - แต่ยังต้องการ constructor
Johannes Schaub - litb

7
@ Mehrdad: การตัดสินใจครั้งนี้ทำขึ้นสำหรับ C ++ 98 ซึ่งอยู่ก่อนเวลาของฉัน อย่างไรก็ตามฉันเชื่อว่าการตัดสินใจมาจากความกังวลเกี่ยวกับความสามารถในการใช้งานและความยากลำบากในการกำหนด (เช่นส่วนใดของคอนเทนเนอร์ที่ทำหรือไม่ต้องการชนิดที่สมบูรณ์) แม้กระทั่งทุกวันนี้ด้วยประสบการณ์ 15 ปีตั้งแต่ C ++ 98 มันเป็นงานที่ไม่สำคัญที่จะผ่อนคลายข้อกำหนดของภาชนะบรรจุในพื้นที่นี้ ฉันคิดว่ามันสามารถทำได้ ฉันรู้ว่ามันจะทำงานมาก ฉันตระหนักถึงความพยายามของคนคนหนึ่ง
Howard Hinnant

9
เพราะมันไม่ชัดเจนจากความคิดเห็นข้างต้นสำหรับทุกคนที่มีปัญหานี้เพราะพวกเขากำหนดunique_ptrเป็นตัวแปรสมาชิกของคลาสเพียงแค่ประกาศ destructor (และตัวสร้าง) อย่างชัดเจนในการประกาศคลาส (ในไฟล์ส่วนหัว) และดำเนินการเพื่อกำหนดพวกเขา ในไฟล์ต้นฉบับ (และใส่ส่วนหัวด้วยการประกาศที่สมบูรณ์ของคลาสชี้ไปที่ในไฟล์ต้นฉบับ) เพื่อป้องกันการคอมไพเลอร์อัตโนมัติ - inlining คอนสตรัคหรือ destructor ในไฟล์ส่วนหัว (ซึ่งก่อให้เกิดข้อผิดพลาด) stackoverflow.com/a/13414884/368896ยังช่วยเตือนฉันถึงสิ่งนี้
Dan Nissenbaum

42

คอมไพเลอร์ต้องการคำจำกัดความของสิ่งเพื่อสร้าง destructor เริ่มต้นสำหรับ MyClass หากคุณประกาศ destructor อย่างชัดเจนและย้ายการใช้งาน (ว่าง) ไปยังไฟล์ CPP รหัสควรรวบรวม


5
ฉันคิดว่านี่เป็นโอกาสที่สมบูรณ์แบบในการใช้ฟังก์ชันที่ผิดนัด MyClass::~MyClass() = default;ในไฟล์การใช้งานดูเหมือนว่ามีโอกาสน้อยที่จะถูกลบโดยไม่ตั้งใจในภายหลังโดยคนที่ถือว่าร่างกาย destuctor ถูกลบมากกว่าปล่อยว่างไว้โดยเจตนา
Dennis Zickefoose

@Dennis Zickefoose: น่าเสียดายที่ OP กำลังใช้ VC ++ และ VC ++ ยังไม่รองรับสมาชิกdefaulted และdeleted
ildjarn

6
+1 สำหรับวิธีย้ายประตูไปสู่ไฟล์. cpp นอกจากนี้ดูเหมือนว่าMyClass::~MyClass() = defaultจะไม่ย้ายไปยังไฟล์การใช้งานบน Clang (ยัง?)
Eonil

คุณต้องย้ายการใช้งาน Constructor ไปยังไฟล์ CPP อย่างน้อยใน VS 2017 ดูตัวอย่างสำหรับคำตอบนี้: stackoverflow.com/a/27624369/5124002
jciloa

15

สิ่งนี้ไม่ได้ขึ้นอยู่กับการนำไปใช้งาน เหตุผลที่ใช้งานได้เพราะเป็นshared_ptrตัวกำหนด destructor ที่ถูกต้องในการเรียกใช้รันไทม์ - มันไม่ได้เป็นส่วนหนึ่งของลายเซ็นประเภท อย่างไรก็ตามunique_ptrdestructor ของเป็นส่วนหนึ่งของชนิดและจะต้องทราบเวลารวบรวม


8

ดูเหมือนว่าคำตอบในปัจจุบันไม่ได้เป็นอย่างแน่นอนว่าทำไมตัวสร้างเริ่มต้น (หรือ destructor) เป็นปัญหา แต่ไม่มีคำสั่งว่างใน cpp

นี่คือสิ่งที่เกิดขึ้น:

ถ้าคลาสภายนอก (เช่น MyClass) ไม่มีตัวสร้างหรือ destructor คอมไพเลอร์จะสร้างค่าเริ่มต้น ปัญหานี้คือคอมไพเลอร์เป็นหลักแทรกตัวสร้างว่างเปล่า / destructor เริ่มต้นในไฟล์. hpp ซึ่งหมายความว่ารหัสสำหรับตัวเริ่มต้น / destructor ที่ได้รับการคอมไพล์พร้อมกับไบนารีของแฟ้มที่ปฏิบัติการได้ของโฮสต์ไม่พร้อมกับไบนารีของห้องสมุด อย่างไรก็ตามคำจำกัดความนี้ไม่สามารถสร้างคลาสบางส่วนได้ ดังนั้นเมื่อ linker เข้าไปในฐานข้อมูลไลบรารีของคุณและพยายามรับตัวสร้าง / ตัวทำลายมันจะไม่พบสิ่งใดและคุณได้รับข้อผิดพลาด หากรหัส Constructor / destructor อยู่ใน. cpp ของคุณไลบรารี binary ของคุณจะพร้อมใช้งานสำหรับการลิงก์

นี่คืออะไรจะทำอย่างไรกับการใช้ unique_ptr หรือ shared_ptr และคำตอบอื่น ๆ ที่ดูเหมือนว่าจะเป็นไปได้สับสนใน VC ++ เก่าสำหรับการใช้งาน unique_ptr (VC ++ 2015 ทำงานได้ดีบนเครื่องของฉัน)

ดังนั้นคุณธรรมของเรื่องราวก็คือส่วนหัวของคุณจะต้องปราศจากข้อจำกัดความของตัวสร้าง / ตัวทำลาย มันสามารถมีการประกาศของพวกเขาเท่านั้น ตัวอย่างเช่น~MyClass()=default;ใน hpp จะไม่ทำงาน หากคุณอนุญาตให้คอมไพเลอร์แทรกตัวสร้างเริ่มต้นหรือ destructor คุณจะได้รับข้อผิดพลาด linker

หมายเหตุอีกด้านหนึ่ง: หากคุณยังคงได้รับข้อผิดพลาดนี้แม้ว่าคุณจะมี Constructor และ Destructor ในไฟล์ cpp ก็ตามสาเหตุส่วนใหญ่ก็คือห้องสมุดของคุณไม่ได้รับการรวบรวมอย่างถูกต้อง ตัวอย่างเช่นครั้งหนึ่งฉันเพียงแค่เปลี่ยนประเภทโครงการจาก Console เป็น Library ใน VC ++ และฉันได้รับข้อผิดพลาดนี้เพราะ VC ++ ไม่ได้เพิ่มสัญลักษณ์ตัวประมวลผลก่อน _LIB และที่ทำให้เกิดข้อความแสดงข้อผิดพลาดเดียวกัน


ขอบคุณ! นั่นเป็นคำอธิบายสั้น ๆ เกี่ยวกับการเล่นโวหาร C ++ ที่ไม่น่าเชื่อ ช่วยชีวิตฉันด้วยปัญหามากมาย
JPNotADragon

5

เพียงเพื่อความสมบูรณ์:

ส่วนหัว: อา

class B; // forward declaration

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

แหล่ง A.cpp:

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

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

คำจำกัดความของคลาส B จะต้องเห็นโดย constructor, destructor และสิ่งใดก็ตามที่อาจลบ B. โดยปริยาย (แม้ว่า Constructor ไม่ปรากฏในรายการด้านบนใน VS2017 แม้ Constructor ต้องการนิยามของ B และสิ่งนี้สมเหตุสมผลเมื่อพิจารณา ในกรณีที่มีข้อยกเว้นในตัวสร้างที่ unique_ptr จะถูกทำลายอีกครั้ง)


1

ต้องการคำจำกัดความทั้งหมดของสิ่งที่จุดอินสแตนซ์ของแม่แบบ นี่คือเหตุผลที่แน่นอนว่าทำไมสำนวน pimpl รวบรวม

ถ้ามันเป็นไปไม่ได้ที่คนจะไม่ถามคำถามเช่นนี้



-7

สำหรับฉัน,

QList<QSharedPointer<ControllerBase>> controllers;

เพียงรวมส่วนหัว ...

#include <QSharedPointer>

คำตอบไม่เกี่ยวข้องและไม่เกี่ยวข้องกับคำถาม
Mikus
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.