pimpl 习语是否比使用 always unique_ptr 作为成员变量更好?
Is pimpl idiom better than using always unique_ptr as member variables?
在我的工作场所,我们有这样的约定:几乎每个 class(除了极少数例外)都使用 unique_ptr
s、原始指针或引用作为成员变量来实现。
这是因为编译时间:这样你只需要在你的头文件中为 class 做一个前向声明,你只需要在你的 cpp 中包含该文件。此外,如果您更改包含在 unique_ptr
中的 class 的 .h 或 .cpp,则无需重新编译。
我认为这种模式至少有以下缺点:
- 它迫使你编写自己的复制构造函数和赋值运算符,如果你想保持复制的语义,你必须单独管理每个变量。
- 代码的语法变得非常繁琐,例如您将使用
std::vector<std::unique_ptr<MyClass>>
而不是更简单的 std::vector<MyPimplClass>
。
- 指针的常量性不会传播到指向的对象,除非你使用 std::experimental::propagate_const,我不能使用它。
所以我想到建议使用 pImpl 惯用语作为指针包含的 classes 而不是在任何地方都使用指针。这样我认为我们可以两全其美:
- 更快的编译时间:pimpl 减少了编译依赖性
- 要编写复制构造函数和复制赋值运算符,您只需这样做:
A::A(const A& rhs) : pImpl(std::make_unique<Impl>(*rhs.pImpl)) {}
A& A::operator=(const A& rhs) {
*pImpl = *rhs.pImpl;
return *this;
}
- constness 传播到成员对象。
此时我和我的同事进行了讨论,他们认为 pImpl 并不比到处使用指针更好,原因如下:
- 它比使用指针减少了编译依赖性,因为如果您正在使用 pImpl,则当您更改 public 接口时,您必须重新编译包含您的 pImpl class 的 classes : 如果您只使用指针而不是 pImpl class,即使更改头文件也不需要重新编译。
现在我有点糊涂了。我认为我们的实际约定并不比 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
一段时间后,我对问题有了更广泛的了解,终于可以回答我自己的问题了。
原来我说的并不完全正确
其实下面的代码中只有Bp
class是pImpl。如果我们将 Ap
也更改为 pImpl 我们会得到,如果我们更改 Bp.h 我们只需要重新编译 Ap.cpp、Bp.cpp,这与相应的解决方案相同unique_ptr
s.
这么说,我想我可以说 pImpl 的解决方案总体上似乎比 unique_ptr
s 的解决方案更好(我们只需要 pImpl 正确的 classes!)。 =14=]
出于这个原因,我们决定将 pImpl 习语作为我们 classes 的默认用法。
在我的工作场所,我们有这样的约定:几乎每个 class(除了极少数例外)都使用 unique_ptr
s、原始指针或引用作为成员变量来实现。
这是因为编译时间:这样你只需要在你的头文件中为 class 做一个前向声明,你只需要在你的 cpp 中包含该文件。此外,如果您更改包含在 unique_ptr
中的 class 的 .h 或 .cpp,则无需重新编译。
我认为这种模式至少有以下缺点:
- 它迫使你编写自己的复制构造函数和赋值运算符,如果你想保持复制的语义,你必须单独管理每个变量。
- 代码的语法变得非常繁琐,例如您将使用
std::vector<std::unique_ptr<MyClass>>
而不是更简单的std::vector<MyPimplClass>
。 - 指针的常量性不会传播到指向的对象,除非你使用 std::experimental::propagate_const,我不能使用它。
所以我想到建议使用 pImpl 惯用语作为指针包含的 classes 而不是在任何地方都使用指针。这样我认为我们可以两全其美:
- 更快的编译时间:pimpl 减少了编译依赖性
- 要编写复制构造函数和复制赋值运算符,您只需这样做:
A::A(const A& rhs) : pImpl(std::make_unique<Impl>(*rhs.pImpl)) {}
A& A::operator=(const A& rhs) {
*pImpl = *rhs.pImpl;
return *this;
}
- constness 传播到成员对象。
此时我和我的同事进行了讨论,他们认为 pImpl 并不比到处使用指针更好,原因如下:
- 它比使用指针减少了编译依赖性,因为如果您正在使用 pImpl,则当您更改 public 接口时,您必须重新编译包含您的 pImpl class 的 classes : 如果您只使用指针而不是 pImpl class,即使更改头文件也不需要重新编译。
现在我有点糊涂了。我认为我们的实际约定并不比 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
一段时间后,我对问题有了更广泛的了解,终于可以回答我自己的问题了。
原来我说的并不完全正确
其实下面的代码中只有Bp
class是pImpl。如果我们将 Ap
也更改为 pImpl 我们会得到,如果我们更改 Bp.h 我们只需要重新编译 Ap.cpp、Bp.cpp,这与相应的解决方案相同unique_ptr
s.
这么说,我想我可以说 pImpl 的解决方案总体上似乎比 unique_ptr
s 的解决方案更好(我们只需要 pImpl 正确的 classes!)。 =14=]
出于这个原因,我们决定将 pImpl 习语作为我们 classes 的默认用法。