ChatGPT解决这个技术问题 Extra ChatGPT

类型不完整的 std::unique_ptr 不会编译

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

但是,我在 <memory> 的第 304 行收到关于使用不完整类型的编译错误:

'sizeof' 对不完整类型 'uixx::window::window_impl' 的无效应用

据我所知,std::unique_ptr 应该能够与不完整的类型一起使用。这是 libc++ 中的错误还是我在这里做错了什么?

完整性要求的参考链接:stackoverflow.com/a/6089065/576911
从那时起,一个 pimpl 经常被构建并且没有被修改。我通常使用 std::shared_ptr
相关:我非常想知道为什么这在 MSVC 中有效,以及如何防止它工作(这样我就不会破坏我的 GCC 同事的编译)。

A
Antonio

以下是类型不完整的 std::unique_ptr 的一些示例。问题在于破坏。

如果将 pimpl 与 unique_ptr 一起使用,则需要声明一个析构函数:

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

我发现您的第一个解决方案(添加 foo 析构函数)允许类声明本身进行编译,但是在任何地方声明该类型的对象会导致原始错误(“'sizeof' 的无效应用......”)。
当然!这只是您的第一个示例,在 main() 中实例化了该类:pastebin.com/65jMYzsi 后来我发现向 foo 添加默认构造函数会使错误消失 - 我不确定为什么。
很好的答案,请注意;我们仍然可以通过在 src 文件中放置例如 foo::~foo() = default; 来使用默认的构造函数/析构函数
使用模板构造函数的一种方法是在类主体中声明但不定义构造函数,在可以看到完整 impl 定义的某个地方定义它,并在那里显式实例化所有必要的实例化。
你能解释一下这在某些情况下是如何工作的,而在其他情况下是如何工作的?我使用了带有 unique_ptr 和没有析构函数的类的 pimpl 成语,在另一个项目中,我的代码无法编译,并出现提到的错误 OP..
n
nnunes

正如 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 获得),正如已经讨论过的 here


就我而言,这是正确的解决方案。使用 pimpl-idiom 并不是唯一的,使用带有不完整类的 std::unique_ptr 是一个普遍问题。 std::unique_ptr 使用的默认删除器尝试执行“删除 X”,如果 X 是前向声明,则无法执行此操作。通过指定删除函数,您可以将该函数放入完全定义类 X 的源文件中。然后其他源文件可以使用 std::unique_ptr,即使 X 只是一个前向声明,只要它们与包含 DeleterFunc 的源文件链接。
当您必须有一个内联函数定义来创建“Foo”类型的实例(例如,引用构造函数和析构函数的静态“getInstance”方法)并且您不想将它们移动到实现文件中时,这是一个很好的解决方法正如@adspx5 建议的那样。
在某些情况下,删除器类可能是唯一合适的解决方案。我个人使用删除器类的扩展 make_uniquetemplate<typename _Tp, typename _Deleter, typename... _Args> auto make_unique_with_deleter(_Args&&... __args) { return std::unique_ptr<_Tp, _Deleter>(new _Tp(std::forward<_Args>(__args)...), _Deleter{}); }
W
Walter

使用自定义删除器

问题是 unique_ptr<T> 必须在其自己的析构函数、其移动赋值运算符和 unique_ptr::reset() 成员函数(仅)中调用析构函数 T::~T()。但是,必须在几个 PIMPL 情况下(已经在外部类的析构函数和移动赋值运算符中)调用(隐式或显式)这些。

正如在另一个答案中已经指出的那样,避免这种情况的一种方法是将需要 unique_ptr::~unique_ptr()unique_ptr::operator=(unique_ptr&&)unique_ptr::reset()所有 操作移动到 pimpl 帮助程序类实际所在的源文件中定义。

然而,这相当不方便,并且在某种程度上违背了 pimpl idoim 的观点。一个更简洁的解决方案可以避免使用自定义删除器,并且只将其定义移动到 pimple helper 类所在的源文件中。这是一个简单的例子:

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

除了单独的删除器类之外,您还可以将自由函数或 foostatic 成员与 lambda 结合使用:

class foo {
    struct pimpl;
    struct deleter {
        operator()(pimpl*) const;
    };
    std::unique_ptr<pimpl,deleter> m_pimpl;
};

我喜欢你的最后一个例子。我很高兴它能像你写的那样工作。但是 std::unique_ptr 的声明期望删除器的类型作为第二个模板参数,而不是删除器对象本身。至少我的 MSVC v16 抱怨。
@Ivan_Bereziuk 是的,那个代码是错误的。现在修好了。感谢您指出了这一点。
a
adspx5

可能您在使用不完整类型的类中的 .h 文件中有一些函数体。

确保在类窗口的 .h 中只有函数声明。 window 的所有函数体都必须在 .cpp 文件中。对于 window_impl 也是如此......

顺便说一句,您必须在 .h 文件中显式添加 windows 类的析构函数声明。

但是您不能将空的 dtor 正文放入头文件中:

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

必须只是一个声明:

  class window {
    virtual ~window();
  }

这也是我的解决方案。方式更简洁。只需在标头中声明构造函数/析构函数并在 cpp 文件中定义即可。
M
Matteo Italia

为了添加其他关于自定义删除器的回复,在我们的内部“实用程序库”中,我添加了一个帮助头来实现这个常见的模式(std::unique_ptr 类型不完整,只有某些 TU 知道,例如避免长编译时间或只为客户提供一个不透明的句柄)。

它为此模式提供了通用的脚手架:调用外部定义的删除器函数的自定义删除器类,具有此删除器类的 unique_ptr 的类型别名,以及在具有完整的 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

S
Stepan Dyatkovskiy

可能不是最佳解决方案,但有时您可以改用 shared_ptr。当然,这有点矫枉过正,但是……至于 unique_ptr,我可能还要再等 10 年,直到 C++ 标准制定者决定使用 lambda 作为删除器。

另一边。根据您的代码,可能会发生,在销毁阶段 window_impl 将不完整。这可能是未定义行为的原因。看到这个:Why, really, deleting an incomplete type is undefined behaviour?

所以,如果可能的话,我会用虚拟析构函数为你的所有对象定义一个非常基础的对象。而且你几乎很好。您只需要记住,系统将为您的指针调用虚拟析构函数,因此您应该为每个祖先定义它。您还应该将继承部分中的基类定义为虚拟(有关详细信息,请参阅 this)。


H
H. Rittich

使用外部模板

T 是不完整类型的情况下使用 std::unique_ptr<T> 的问题是 unique_ptr 需要能够删除 T 的实例以进行各种操作。类 unique_ptr 使用 std::default_delete<T> 删除实例。因此,在理想世界中,我们只写

extern template class std::default_delete<T>;

以防止 std::default_delete<T> 被实例化。然后,声明

template class std::default_delete<T>;

T 完成的地方,实例化模板。

这里的问题是 default_delete 实际上定义了不会被实例化的内联方法。所以,这个想法行不通。但是,我们可以解决这个问题。

首先,让我们定义一个不内联调用运算符的删除器。

/* --- opaque_ptr.hpp ------------------------------------------------------- */
#ifndef OPAQUE_PTR_HPP_
#define OPAQUE_PTR_HPP_

#include <memory>

template <typename T>
class opaque_delete {
public:
  void operator() (T* ptr);
};

// Do not move this method into opaque_delete, or it will be inlined!
template <typename T>
void opaque_delete<T>::operator() (T* ptr) {
  std::default_delete<T>()(ptr);
}

此外,为了便于使用,定义一个类型 opaque_ptr,它结合了 unique_ptropaque_delete,类似于 std::make_unique,我们定义了 make_opaque

/* --- opaque_ptr.hpp cont. ------------------------------------------------- */
template <typename T>
using opaque_ptr = std::unique_ptr<T, opaque_delete<T>>;

template<typename T, typename... Args>
inline opaque_ptr<T> make_opaque(Args&&... args)
{
  return opaque_ptr<T>(new T(std::forward<Args>(args)...));
}

#endif

opaque_delete 类型现在可以与 extern template 构造一起使用。这是一个例子。

/* --- foo.hpp -------------------------------------------------------------- */
#ifndef FOO_HPP_
#define FOO_HPP_

#include "opaque_ptr.hpp"

class Foo {
public:
  Foo(int n);
  void print();
private:
  struct Impl;
  opaque_ptr<Impl> m_ptr;
};

// Do not instantiate opaque_delete.
extern template class opaque_delete<Foo::Impl>;

#endif

由于我们阻止 opaque_delete 被实例化,因此此代码编译时不会出错。为了让链接器满意,我们在 foo.cpp 中实例化 opaque_delete

/* --- foo.cpp -------------------------------------------------------------- */

#include "foo.hpp"
#include <iostream>

struct Foo::Impl {
  int n;
};

// Force instantiation of opaque_delete.
template class opaque_delete<Foo::Impl>;

其余方法可以如下实现。

/* --- foo.cpp cont. -------------------------------------------------------- */
Foo::Foo(int n)
  : m_ptr(new Impl)
{
  m_ptr->n = n;
}

void Foo::print() {
  std::cout << "n = " << m_ptr->n << std::endl;
}

这种解决方案的优点是,一旦定义了 opaque_delete,所需的样板代码就相当小。


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅