【问题标题】:Is pimpl idiom better than using always unique_ptr as member variables?pimpl idiom 比总是使用 unique_ptr 作为成员变量更好吗?
【发布时间】:2021-08-09 10:18:35
【问题描述】:

在我的工作场所,我们有这样的约定:几乎每个类(除了极少数例外)都使用unique_ptrs、原始指针或引用作为成员变量来实现。

这是因为编译时间:这样你只需要在你的头文件中为类做一个前向声明,你只需要在你的 cpp.xml 中包含这个文件。此外,如果您更改 unique_ptr 包含的类的 .h 或 .cpp,则无需重新编译。

我认为这种模式至少有以下缺点:

  • 它迫使您编写自己的复制构造函数和赋值运算符,如果您想保持复制的语义,您必须单独管理每个变量。
  • 代码的语法变得非常繁琐,例如您将使用std::vector<std::unique_ptr<MyClass>> 而不是更简单的std::vector<MyPimplClass>
  • 指针的常量不会传播到指向的对象,除非您使用 std::experimental::propagate_const,我不能使用它。

因此,我想到建议将 pImpl 习惯用法用于作为指针包含的类,而不是在任何地方都使用指针。通过这种方式,我认为我们可以两全其美:

  • 更快的编译时间:pimpl 减少了编译依赖
  • 要编写复制构造函数和复制赋值运算符,您只需这样做:
A::A(const A& rhs) : pImpl(std::make_unique<Impl>(*rhs.pImpl)) {}
A& A::operator=(const A& rhs) {
  *pImpl = *rhs.pImpl;
  return *this;
}
  • 常量被传播到成员对象。

此时我与我的同事进行了讨论,他们认为 pImpl 并不比在各处使用指针更好,原因如下:

  • 与使用指针相比,它减少了编译依赖性,因为如果您使用 pImpl,当您更改公共接口时,您必须重新编译包含您的 pImpl 类的类:如果您只使用指针而不是 pImpl 类,您就赢了'即使更改头文件也不需要重新编译。

现在我有点困惑。我认为我们的实际约定并不比 pImpl 更好,但我无法解释为什么。

所以我有一些问题:

  • 在这种情况下,pImpl 习语是一个不错的选择吗?
  • 除了我提到的那些之外,我们使用的模式还有其他缺点吗?

编辑: 我正在添加一些示例来阐明这两种方法。

  • unique_ptr 为成员的方法:
// B.h
#pragma once
class B {
    int i = 42;
public:
    void print();
};

// B.cpp
#include "B.h"
#include <iostream>
void B::print() { std::cout << i << '\n'; }

// A.h
#pragma once
#include <memory>
class B;
class A {
    std::unique_ptr<B> b;
public:
    A();
    ~A();
    void printB();
};

// A.cpp
#include "A.h"
#include "B.h"
A::A() : b{ std::make_unique<B>() } {}
A::~A() = default;
void A::printB() {  b->print(); }
  • 使用 pImpl 的方法:
// Bp.h
#pragma once
#include <memory>
class Bp {
    struct Impl;
    std::unique_ptr<Impl> m_pImpl;
public:
    Bp();
    ~Bp();
    void print();
};

// Bp.cpp
#include "Bp.h"
#include <iostream>
struct Bp::Impl {
    int i = 42;
};
Bp::Bp() : m_pImpl{ std::make_unique<Impl>() } {}
Bp::~Bp() = default;
void Bp::print() {  std::cout << m_pImpl->i << '\n'; }

// Ap.h
#pragma once
#include <memory>
#include "Bp.h"
class Ap {
    Bp b;
public:
    void printB();
};

// Ap.cpp
#include "Ap.h"
#include "Bp.h"
void Ap::printB() { b.print(); }

主要:

// main.cpp
#include "Ap.h"
#include "A.h"

int main(int argc, char** argv) {
    A a{};
    a.printB();

    Ap aPimpl{};
    aPimpl.printB();
}

此外,当我说第一种方法我们不需要重新编译时,我想更准确地说,这是不准确的。确实需要重新编译less文件:

  • 如果我们更改 B.h,我们只需要重新编译 A.cpp、B.cpp。
  • 如果我们改变 Bp.h 我们需要重新编译 Ap.cpp、Bp.cpp 和 main.cpp

【问题讨论】:

  • "如果您只使用指针而不是 pImpl 类,即使更改头文件也不需要重新编译。"不,这不是真的。如果你只是传递一个指针,那么是的,这是真的,但如果你的类是用 pimpl 模式实现的,你仍然可以传递指针。否则,如果你真的需要对类做一些事情,那么如果你改变了头文件,你仍然需要重新编译。
  • 为了更方便地使用unique_ptr,您可以使用using MyClass_ptr = std::unique_ptr&lt;MyClass&gt;,也可以使用= default设置您提到的功能。
  • std::vector&lt;MyClass&gt;std::vector&lt;std::unique_ptr&lt;MyClass&gt;&gt; 是完全不同的东西
  • @JMRC 它不会与 = default 一起编译。
  • 它不能回答您的问题,但对于这些方法中的任何一种,使用建议的std::indirect_value 之类的方法可能会有所帮助。

标签: c++ compile-time pimpl


【解决方案1】:

一段时间后,我对问题有了更广泛的了解,终于可以回答自己的问题了。

原来我说的并不完全正确。

其实在下面的代码中只有Bp类是pImpl。如果我们把Ap也改成pImpl就得到了,如果我们把Bp.h改成只需要重新编译Ap.cpp、Bp.cpp就可以了,这和unique_ptrs对应的解法是一样的。

话虽如此,我想我可以说 pImpl 的解决方案总体上似乎比 unique_ptrs 的解决方案更好(我们只需要 pImpl 正确的类!)。

出于这个原因,我们决定将 pImpl 惯用语作为我们类的默认设置。

【讨论】:

    猜你喜欢
    • 2015-07-16
    • 2015-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-08
    • 2012-10-26
    相关资源
    最近更新 更多