std :: unique_ptr ด้วยประเภทที่ไม่สมบูรณ์จะไม่รวบรวม


203

ฉันใช้ pimpl-idiom กับstd::unique_ptr:

class window {
  window(const rectangle& rect);

private:
  class window_impl; // defined elsewhere
  std::unique_ptr<window_impl> impl_; // won't compile
};

อย่างไรก็ตามฉันได้รับข้อผิดพลาดในการคอมไพล์เกี่ยวกับการใช้ประเภทที่ไม่สมบูรณ์ในบรรทัด 304 ใน<memory>:

แอปพลิเคชันไม่ถูกต้องของ ' sizeof' เป็นประเภทไม่สมบูรณ์ ' uixx::window::window_impl'

เท่าที่ฉันรู้std::unique_ptrควรใช้กับประเภทที่ไม่สมบูรณ์ นี่เป็นข้อบกพร่องใน libc ++ หรือฉันทำอะไรผิดที่นี่?


ลิงค์อ้างอิงสำหรับความต้องการครบถ้วน: stackoverflow.com/a/6089065/576911
Howard Hinnant

1
pimpl มักถูกสร้างขึ้นและไม่ได้แก้ไขตั้งแต่นั้น ฉันมักจะใช้มาตรฐาน :: shared_ptr <const window_impl>
mfnx

ที่เกี่ยวข้อง: ฉันอยากรู้ว่าทำไมสิ่งนี้ถึงทำงานได้ใน MSVC และวิธีการป้องกันไม่ให้ทำงาน (เพื่อที่ฉันจะได้ไม่ทำลายการรวบรวมเพื่อนร่วมงานของ GCC)
เลน

คำตอบ:


260

นี่คือตัวอย่างของstd::unique_ptrประเภทที่ไม่สมบูรณ์ ปัญหาอยู่ที่การทำลายล้าง

หากคุณใช้ pimpl ด้วยunique_ptrคุณต้องประกาศ destructor:

class foo
{ 
    class impl;
    std::unique_ptr<impl> impl_;

public:
    foo(); // You may need a def. constructor to be defined elsewhere

    ~foo(); // Implement (with {}, or with = default;) where impl is complete
};

เพราะมิฉะนั้นคอมไพเลอร์สร้างค่าเริ่มต้นและมันต้องการการประกาศที่สมบูรณ์foo::implสำหรับสิ่งนี้

หากคุณมีตัวสร้างเทมเพลตคุณก็จะเมาแม้ว่าคุณจะไม่ได้สร้างimpl_สมาชิก:

template <typename T>
foo::foo(T bar) 
{
    // Here the compiler needs to know how to
    // destroy impl_ in case an exception is
    // thrown !
}

ที่ขอบเขตเนมสเปซการใช้unique_ptrจะไม่ทำงานอย่างใดอย่างหนึ่ง:

class impl;
std::unique_ptr<impl> impl_;

ตั้งแต่คอมไพเลอร์ต้องรู้ที่นี่วิธีการทำลายวัตถุระยะเวลาคงที่นี้ วิธีแก้ปัญหาคือ:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;

3
ฉันค้นหาวิธีแก้ปัญหาแรกของคุณ (การเพิ่มfoo destructor) ทำให้การประกาศคลาสสามารถคอมไพล์ได้ แต่การประกาศออบเจกต์ประเภทนั้นทุกที่ทำให้เกิดข้อผิดพลาดเดิม ("การใช้งานที่ไม่ถูกต้องของ 'sizeof' ... ")
Jeff Trull

38
คำตอบที่ยอดเยี่ยมเพียงที่จะต้องทราบ; เรายังคงสามารถใช้ตัวสร้าง / destructor เริ่มต้นโดยการวางเช่นfoo::~foo() = default;ในไฟล์ src
assem

2
วิธีหนึ่งที่จะมีชีวิตอยู่กับเทมเพลตคอนสตรัคเตอร์คือการประกาศ แต่ไม่ได้กำหนดคอนสตรัคเตอร์ในคลาสของร่างกายกำหนดที่ใดที่คำจำกัดความที่สมบูรณ์ถูกมองเห็นและทำให้อินสแตนซ์ที่จำเป็นทั้งหมด
enobayram

2
คุณสามารถอธิบายได้ว่าวิธีนี้จะทำงานในบางกรณีและจะไม่ในคนอื่น ๆ ? ฉันได้ใช้สำนวน pimpl กับ unique_ptr และชั้นที่ไม่มี destructor และในโครงการอื่นรหัสของฉันล้มเหลวในการรวบรวม OP ข้อผิดพลาดดังกล่าว ..
อยากรู้อยากเห็น

1
ดูเหมือนว่าหากค่าเริ่มต้นสำหรับ unique_ptr ถูกตั้งค่าเป็น {nullptr} ในไฟล์ส่วนหัวของคลาสที่มีสไตล์ c ++ 11 จำเป็นต้องมีการประกาศที่สมบูรณ์ด้วยเหตุผลด้านบน
feirainy

53

ดังที่Alexandre C.กล่าวถึงปัญหาดังกล่าวเกิดขึ้นกับwindowผู้ทำลายล้างที่ถูกกำหนดโดยปริยายในสถานที่ซึ่งwindow_implยังคงไม่สมบูรณ์ประเภท นอกเหนือจากวิธีแก้ปัญหาของเขาแล้ววิธีแก้ปัญหาอื่นที่ฉันเคยใช้ก็คือการประกาศหมอ Deleter ในส่วนหัว:

// Foo.h

class FooImpl;
struct FooImplDeleter
{
  void operator()(FooImpl *p);
};

class Foo
{
...
private:
  std::unique_ptr<FooImpl, FooImplDeleter> impl_;
};

// Foo.cpp

...
void FooImplDeleter::operator()(FooImpl *p)
{
  delete p;
}

หมายเหตุว่าการใช้ฟังก์ชั่นที่กำหนดเอง Deleter ติ๊ดการใช้std::make_unique(สามารถใช้ได้จาก C ++ 14) ตามที่กล่าวไว้แล้วที่นี่


6
นี่คือทางออกที่ถูกต้องเท่าที่ฉันกังวล ไม่เฉพาะการใช้ pimpl-idiom มันเป็นปัญหาทั่วไปของการใช้ std :: unique_ptr กับคลาสที่ไม่สมบูรณ์ ค่าเริ่มต้น deleter ที่ใช้โดย std :: unique_ptr <X> พยายามทำ "delete X" ซึ่งไม่สามารถทำได้ถ้า X คือการประกาศไปข้างหน้า โดยการระบุฟังก์ชั่น deleter คุณสามารถใส่ฟังก์ชั่นนั้นในไฟล์ต้นฉบับที่คลาส X นั้นถูกกำหนดไว้อย่างสมบูรณ์ ไฟล์ต้นฉบับอื่นสามารถใช้ std :: unique_ptr <X, DeleterFunc> แม้ว่า X เป็นเพียงการประกาศไปข้างหน้าตราบใดที่ไฟล์เหล่านั้นเชื่อมโยงกับไฟล์ต้นฉบับที่มี DeleterFunc
sheltond

1
นี่เป็นวิธีแก้ปัญหาที่ดีเมื่อคุณต้องมีคำจำกัดความของฟังก์ชั่นอินไลน์ที่สร้างอินสแตนซ์ของ "Foo" ประเภทของคุณ (เช่นวิธี "getInstance" แบบคงที่ที่อ้างอิงตัวสร้างและ destructor) และคุณไม่ต้องการย้ายสิ่งเหล่านี้ลงในไฟล์ ตามที่ @ adspx5 แนะนำ
GameSalutes

20

ใช้ deleter ที่กำหนดเอง

ปัญหาคือunique_ptr<T>ต้องเรียก destructor T::~T()ในdestructor ของตนเองตัวดำเนินการกำหนดย้ายและunique_ptr::reset()ฟังก์ชันสมาชิก (เท่านั้น) อย่างไรก็ตามสิ่งเหล่านี้จะต้องถูกเรียก (โดยปริยายหรือโดยชัดแจ้ง) ในหลาย ๆ สถานการณ์ PIMPL (มีอยู่แล้วในตัวทำลายของคลาสภายนอก

เป็นแหลมออกไปแล้วในคำตอบอีกวิธีหนึ่งที่จะหลีกเลี่ยงที่จะย้ายทั้งหมดการดำเนินงานที่จำเป็นต้องมีunique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)และunique_ptr::reset()เข้าไปในแฟ้มแหล่งที่มาที่ระดับผู้ช่วย pimpl ถูกกำหนดจริง

อย่างไรก็ตามมันค่อนข้างไม่สะดวกและท้าทายจุด pimpl idoim ในระดับหนึ่ง วิธีแก้ปัญหาที่สะอาดกว่ามากที่จะหลีกเลี่ยงสิ่งที่ต้องทำทั้งหมดคือการใช้deleter แบบกำหนดเองและย้ายคำจำกัดความของมันไปยังไฟล์ต้นฉบับที่ผู้ช่วยสิวใช้ชีวิตเท่านั้น นี่คือตัวอย่างง่ายๆ:

// file.h
class foo
{
  struct pimpl;
  struct pimpl_deleter { void operator()(pimpl*) const; };
  std::unique_ptr<pimpl,pimpl_deleter> m_pimpl;
public:
  foo(some data);
  foo(foo&&) = default;             // no need to define this in file.cc
  foo&operator=(foo&&) = default;   // no need to define this in file.cc
//foo::~foo()          auto-generated: no need to define this in file.cc
};

// file.cc
struct foo::pimpl
{
  // lots of complicated code
};
void foo::pimpl_deleter::operator()(foo::pimpl*ptr) const { delete ptr; }

แทนที่จะเป็นคลาส deleter แยกต่างหากคุณสามารถใช้ฟังก์ชั่นฟรีหรือstaticสมาชิกfooร่วมกับแลมบ์ดา:

class foo {
  struct pimpl;
  static void delete_pimpl(pimpl*);
  std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};

15

อาจเป็นไปได้ว่าคุณมีฟังก์ชั่นบางอย่างภายในไฟล์. h ภายในคลาสที่ใช้ประเภทที่ไม่สมบูรณ์

ตรวจสอบให้แน่ใจว่าภายใน. h สำหรับหน้าต่างคลาสคุณมีการประกาศฟังก์ชันเท่านั้น ฟังก์ชันเนื้อหาทั้งหมดสำหรับหน้าต่างต้องอยู่ในไฟล์. cpp และสำหรับ window_impl เช่นกัน ...

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

แต่คุณไม่สามารถใส่ร่างกาย dtor เปล่าในไฟล์ส่วนหัวของคุณ:

class window {
    virtual ~window() {};
  }

ต้องเป็นเพียงการประกาศ:

  class window {
    virtual ~window();
  }

นี่คือทางออกของฉันเช่นกัน กระชับขึ้น เพียงแค่กำหนด Constructor / Destructor ของคุณในส่วนหัวและกำหนดไว้ในไฟล์ cpp
Kris Morness

2

เพื่อเพิ่มคำตอบของคนอื่นเกี่ยวกับ deleter ที่กำหนดเองใน "ห้องสมุดสาธารณูปโภค" ภายในของเราฉันได้เพิ่มส่วนหัวของผู้ช่วยในการนำรูปแบบทั่วไปนี้ไปใช้ (รูปแบบstd::unique_ptrที่ไม่สมบูรณ์ซึ่งเป็นที่รู้จักเฉพาะบาง TU เท่านั้นเช่นหลีกเลี่ยงเวลารวบรวมที่ยาวนาน เพียงจัดการทึบแสงให้กับลูกค้า)

มันจัดเตรียมนั่งร้านทั่วไปสำหรับรูปแบบนี้: คลาส deleter แบบกำหนดเองที่เรียกใช้ฟังก์ชัน deleter ที่กำหนดภายนอก, นามแฝงชนิดสำหรับ a unique_ptrกับคลาส deleter นี้และแมโครเพื่อประกาศฟังก์ชัน deleter ใน TU ที่มีคำจำกัดความที่สมบูรณ์ของ ชนิด ฉันคิดว่านี่มีประโยชน์ทั่วไปดังนั้นนี่คือ:

#ifndef CZU_UNIQUE_OPAQUE_HPP
#define CZU_UNIQUE_OPAQUE_HPP
#include <memory>

/**
    Helper to define a `std::unique_ptr` that works just with a forward
    declaration

    The "regular" `std::unique_ptr<T>` requires the full definition of `T` to be
    available, as it has to emit calls to `delete` in every TU that may use it.

    A workaround to this problem is to have a `std::unique_ptr` with a custom
    deleter, which is defined in a TU that knows the full definition of `T`.

    This header standardizes and generalizes this trick. The usage is quite
    simple:

    - everywhere you would have used `std::unique_ptr<T>`, use
      `czu::unique_opaque<T>`; it will work just fine with `T` being a forward
      declaration;
    - in a TU that knows the full definition of `T`, at top level invoke the
      macro `CZU_DEFINE_OPAQUE_DELETER`; it will define the custom deleter used
      by `czu::unique_opaque<T>`
*/

namespace czu {
template<typename T>
struct opaque_deleter {
    void operator()(T *it) {
        void opaque_deleter_hook(T *);
        opaque_deleter_hook(it);
    }
};

template<typename T>
using unique_opaque = std::unique_ptr<T, opaque_deleter<T>>;
}

/// Call at top level in a C++ file to enable type %T to be used in an %unique_opaque<T>
#define CZU_DEFINE_OPAQUE_DELETER(T) namespace czu { void opaque_deleter_hook(T *it) { delete it; } }

#endif

1

อาจไม่ใช่วิธีที่ดีที่สุด แต่บางครั้งคุณอาจใช้shared_ptrแทน ถ้าแน่นอนว่ามันช่างเกินความจริง แต่ ... สำหรับ unique_ptr ฉันอาจจะรออีก 10 ปีกว่าผู้สร้างมาตรฐาน C ++ จะตัดสินใจใช้แลมบ์ดาเป็นเครื่องมือ

อีกด้านหนึ่ง. ตามรหัสของคุณมันอาจเกิดขึ้นว่าในขั้นตอนการทำลาย window_impl จะไม่สมบูรณ์ นี่อาจเป็นสาเหตุของพฤติกรรมที่ไม่ได้กำหนด ดูสิ่งนี้: ทำไมการลบประเภทที่ไม่สมบูรณ์นั้นเป็นพฤติกรรมที่ไม่ได้กำหนด

ดังนั้นถ้าเป็นไปได้ฉันจะกำหนดวัตถุฐานให้กับวัตถุทั้งหมดของคุณด้วยตัวทำลายเสมือน และคุณเกือบจะดี คุณควรจำไว้ว่าระบบจะเรียกตัวทำลายเสมือนสำหรับตัวชี้ของคุณดังนั้นคุณควรกำหนดให้กับบรรพบุรุษทุกตัว คุณควรกำหนดคลาสพื้นฐานในส่วนการสืบทอดเป็นเสมือน (ดูรายละเอียดนี้ )

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