std :: unique_ptr với kiểu không hoàn chỉnh sẽ không biên dịch


202

Tôi đang sử dụng thành ngữ pimpl với std::unique_ptr:

class window {
  window(const rectangle& rect);

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

Tuy nhiên, tôi gặp lỗi biên dịch liên quan đến việc sử dụng loại không hoàn chỉnh, trên dòng 304 trong <memory>:

Áp dụng không hợp lệ ' sizeof' cho loại không hoàn chỉnh ' uixx::window::window_impl'

Theo như tôi biết, std::unique_ptrcó thể được sử dụng với một loại không đầy đủ. Đây có phải là một lỗi trong libc ++ hay tôi đang làm gì đó sai ở đây?


Liên kết tham chiếu cho các yêu cầu về tính đầy đủ: stackoverflow.com/a/6089065/576911
Howard Hinnant

1
Một pimpl thường được xây dựng và không được sửa đổi kể từ đó. Tôi thường sử dụng std :: shared_ptr <const window_impl>
mfnx 23/11/18

Liên quan: Tôi rất muốn biết lý do tại sao điều này hoạt động trong MSVC và cách ngăn chặn nó hoạt động (để tôi không phá vỡ các phần tổng hợp của các đồng nghiệp GCC của tôi).
Len

Câu trả lời:


258

Dưới đây là một số ví dụ về std::unique_ptr với các loại không đầy đủ. Vấn đề nằm ở sự hủy diệt.

Nếu bạn sử dụng pimpl với unique_ptr, bạn cần khai báo hàm hủy:

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
};

bởi vì nếu không trình biên dịch tạo ra một mặc định và nó cần một khai báo đầy đủ về foo::impl cho việc này.

Nếu bạn có các hàm tạo mẫu, thì bạn đã bị lừa, ngay cả khi bạn không xây dựng impl_thành viên:

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

Ở phạm vi không gian tên, sử dụng unique_ptrsẽ không hoạt động:

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

vì trình biên dịch phải biết ở đây làm thế nào để phá hủy đối tượng thời lượng tĩnh này. Một cách giải quyết là:

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

3
Tôi tìm giải pháp đầu tiên của bạn (thêm hàm hủy foo ) cho phép chính lớp khai báo biên dịch, nhưng khai báo một đối tượng kiểu đó ở bất cứ đâu dẫn đến lỗi ban đầu ("ứng dụng không hợp lệ của 'sizeof' ...").
Jeff Trull

38
câu trả lời tuyệt vời, chỉ cần lưu ý; chúng ta vẫn có thể sử dụng hàm tạo / hàm hủy mặc định bằng cách đặt ví dụ foo::~foo() = default;trong tệp src
ráp

2
Một cách để sống với các hàm tạo mẫu sẽ là khai báo nhưng không định nghĩa hàm tạo trong thân lớp, định nghĩa nó ở đâu đó định nghĩa hàm hoàn chỉnh được nhìn thấy và khởi tạo rõ ràng tất cả các cảnh báo cần thiết ở đó.
enobayram 29/07/2015

2
Bạn có thể giải thích làm thế nào điều này sẽ làm việc trong một số trường hợp và sẽ không trong những người khác? Tôi đã sử dụng thành ngữ pimpl với unique_ptr và một lớp không có hàm hủy và trong một dự án khác, mã của tôi không thể biên dịch với lỗi OP đã đề cập ..
Tò mò

1
Có vẻ như nếu giá trị mặc định cho unique_ptr được đặt thành {nullptr} trong tệp tiêu đề của lớp với kiểu c ++ 11, thì cũng cần một khai báo hoàn chỉnh cho lý do trên.
feirainy

53

Như Alexandre C. đã đề cập, vấn đề bắt nguồn từ việc windowhủy diệt được xác định ngầm ở những nơi mà loại window_implvẫn chưa hoàn thành. Ngoài các giải pháp của mình, một cách giải quyết khác mà tôi đã sử dụng là khai báo hàm functor Deleter trong tiêu đề:

// 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;
}

Lưu ý rằng việc sử dụng chức năng Deleter tùy chỉnh sẽ loại trừ việc sử dụng std::make_unique(có sẵn từ C ++ 14), như đã thảo luận ở đây .


6
Đây là giải pháp chính xác theo như tôi nghĩ. Nó không phải là duy nhất để sử dụng thành ngữ pimpl, đó là một vấn đề chung với việc sử dụng std :: unique_ptr với các lớp không đầy đủ. Deleter mặc định được sử dụng bởi std :: unique_ptr <X> cố gắng thực hiện "xóa X", điều này không thể thực hiện nếu X là khai báo chuyển tiếp. Bằng cách chỉ định một hàm deleter, bạn có thể đặt hàm đó vào một tệp nguồn trong đó lớp X được xác định hoàn toàn. Các tệp nguồn khác sau đó có thể sử dụng std :: unique_ptr <X, DeleterFunc> mặc dù X chỉ là một khai báo chuyển tiếp miễn là chúng được liên kết với tệp nguồn chứa DeleterFunc.
sheltond

1
Đây là một cách giải quyết tốt khi bạn phải có định nghĩa hàm nội tuyến tạo một thể hiện của loại "Foo" của bạn (ví dụ: phương thức "getInstance" tĩnh tham chiếu hàm tạo và hàm hủy) và bạn không muốn chuyển chúng vào tệp thực thi như @ adspx5 gợi ý.
GameSalutes 20/03/2016

20

sử dụng một deleter tùy chỉnh

Vấn đề là unique_ptr<T>phải gọi hàm hủyT::~T() trong hàm của chính nó, toán tử gán chuyển động của nó vàunique_ptr::reset() hàm thành viên (chỉ). Tuy nhiên, chúng phải được gọi (ngầm hoặc rõ ràng) trong một số tình huống PIMPL (đã có trong toán tử gán và di chuyển gán của lớp ngoài).

Như đã chỉ ra trong câu trả lời khác, một cách để tránh điều đó là để di chuyển tất cả các hoạt động đòi hỏi unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)unique_ptr::reset()vào file nguồn nơi lớp pimpl helper là thực sự xác định.

Tuy nhiên, điều này khá bất tiện và bất chấp chính điểm của pimpl idoim ở một mức độ nào đó. Một giải pháp sạch hơn nhiều giúp tránh tất cả những điều đó là sử dụng một deleter tùy chỉnh và chỉ di chuyển định nghĩa của nó vào tệp nguồn nơi lớp người trợ giúp nổi mụn sống. Đây là một ví dụ đơn giản:

// 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; }

Thay vì một lớp deleter riêng biệt, bạn cũng có thể sử dụng một hàm miễn phí hoặc staticthành viên fookết hợp với lambda:

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

15

Có lẽ bạn có một số thân hàm trong tệp .h trong lớp sử dụng loại không đầy đủ.

Hãy chắc chắn rằng trong cửa sổ lớp .h của bạn, bạn chỉ có khai báo hàm. Tất cả các cơ quan chức năng cho cửa sổ phải ở trong tệp .cpp. Và đối với window_impl cũng ...

Btw, bạn phải thêm khai báo hàm hủy cho lớp windows trong tệp .h của mình.

Nhưng bạn KHÔNG THỂ đặt cơ thể dtor trống trong tệp tiêu đề của bạn:

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

Phải chỉ là một tuyên bố:

  class window {
    virtual ~window();
  }

Đây là giải pháp của tôi là tốt. Cách súc tích hơn. Chỉ cần có hàm tạo / hàm hủy của bạn được khai báo trong tiêu đề và được định nghĩa trong tệp cpp.
Kris Morness

2

Để thêm vào các câu trả lời của người khác về trình duyệt tùy chỉnh, trong "thư viện tiện ích" nội bộ của chúng tôi, tôi đã thêm một tiêu đề trợ giúp để triển khai mẫu chung này ( std::unique_ptrthuộc loại không hoàn chỉnh, chỉ được biết đến với một số TU để tránh thời gian biên dịch dài hoặc để cung cấp chỉ là một tay cầm mờ đục cho khách hàng).

Nó cung cấp giàn giáo chung cho mẫu này: một lớp deleter tùy chỉnh gọi hàm deleter được xác định bên ngoài, một bí danh loại cho a unique_ptrvới lớp deleter này và macro để khai báo hàm deleter trong TU có định nghĩa đầy đủ về kiểu. Tôi nghĩ rằng điều này có một số hữu ích chung, vì vậy đây là:

#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

Có thể không phải là một giải pháp tốt nhất, nhưng đôi khi bạn có thể sử dụng shared_ptr thay thế. Nếu tất nhiên đó là một chút quá mức, nhưng ... đối với unique_ptr, có lẽ tôi sẽ đợi thêm 10 năm nữa cho đến khi các nhà sản xuất tiêu chuẩn C ++ sẽ quyết định sử dụng lambda như một deleter.

Mặt khác. Theo mã của bạn, điều đó có thể xảy ra, rằng ở giai đoạn hủy window_impl sẽ không đầy đủ. Đây có thể là một lý do của hành vi không xác định. Xem điều này: Tại sao, thực sự, xóa một loại không đầy đủ là hành vi không xác định?

Vì vậy, nếu có thể tôi sẽ định nghĩa một đối tượng rất cơ bản cho tất cả các đối tượng của bạn, với hàm hủy ảo. Và bạn gần như tốt. Bạn chỉ nên nhớ rằng hệ thống sẽ gọi hàm hủy ảo cho con trỏ của bạn, vì vậy bạn nên xác định nó cho mọi tổ tiên. Bạn cũng nên định nghĩa lớp cơ sở trong phần thừa kế là ảo (xem phần này để biết chi tiết).

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.