C++ API 设计:清理 public 界面
C++ API design: Clearing up public interface
对于我的库,我想公开一个干净的 public API,不会分散实施细节的注意力。但是,正如您所拥有的,这些细节甚至会泄露到 public 领域:一些 classes 具有有效的 public 方法,这些方法被库的其余部分使用,但不是对于 API 的用户非常有用,因此不需要成为它的一部分。 public代码的简化示例:
class Cookie;
class CookieJar {
public:
Cookie getCookie();
}
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie();
}
}
bool isHungry();
}
CookieJar
的 getCookie()
方法对库的用户没有用,他们大概不喜欢 cookie。然而,当给定一个时,CookieMonster
会用它来喂养自己。
有一些习语可以帮助解决这个问题。 Pimpl 惯用语提供隐藏 class 的私有成员,但几乎没有掩饰不应属于 API 的 public 方法。也可以将它们移动到实现 class 中,但是您需要提供对它的直接访问以供库的其余部分使用。这样的 header 看起来像这样:
class Cookie;
class CookieJarImpl;
class CookieJar {
public:
CookieJarImpl* getImplementation() {
return pimpl.get();
}
private:
std::unique_ptr<CookieJarImpl> pimpl;
}
如果您真的需要阻止用户访问这些方法,这会很方便,但如果这只是一种烦恼,这就没有多大帮助。事实上,新方法现在比上一个更无用,因为用户无权访问 CookieJarImpl
.
的实现
另一种方法是将接口定义为抽象基础 class。这可以明确控制 public API 的一部分。任何私人细节都可以包含在该接口的实现中,用户无法访问。需要注意的是,由此产生的虚拟调用会影响性能,甚至比 Pimpl 习惯用法更严重。清洁器的交易速度 API 对于应该是高性能库的东西来说并不是很有吸引力。
为了详尽无遗,另一种选择是将有问题的方法设为私有,并在需要从外部访问它们的地方使用 friend classes。然而,这使目标 objects 也可以访问真正的私有成员,这在某种程度上破坏了封装。
到目前为止,对我来说最好的解决方案似乎是 Python 方式:不要试图隐藏实现细节,只需适当地命名它们,这样就可以很容易地将它们识别为不属于 public API 并且不要分散常规使用的注意力。想到的命名约定是使用下划线前缀,但显然这样的名称是为编译器保留的,不鼓励使用它们。
是否有任何其他 C++ 命名约定来区分不打算从库外部使用的成员?或者你会建议我使用上面的替代方法之一还是我错过的其他方法?
您应该在您的 CookieJar class 中使用一个私有容器,在调用构造函数时该容器中会充满 cookie。在下面的代码中,我使用了一个STL C++库的vector作为容器,因为使用方便,但是你也可以使用其他的东西(array,list,map等),并且把cookies的属性设为private .您也可以隐藏 monster isHungry
属性以获得更好的封装。
如果你想对库的用户隐藏 getCookie()
方法,那么你应该将此方法设为私有,并将 CookieMonster
class 视为好友 class 的 CookieJar
,因此 CookieMonster
将能够使用 getCookie()
方法,而用户将无法使用。
#include<vector>
using namespace std;
class Cookie
{
private:
string type;
string chocolateFlavor;
}
class CookieJar {
friend class CookieMonster;
public:
CookieJar(){
//loads a cookie jar with 10 cookies
for (int i = 0; i = 10; i++) {
Cookie cookie;
cookieContainer.push_back(cookie);
}
}
private:
vector<Cookie> cookieContainer;
Cookie getCookie(){
//returns a cookie to feed and deletes one in the container
Cookie toFeed = cookieContainer[0];
cookieContainer[0] = *cookieContainer.back();
cookieContainer.pop_back();
return toFeed;
}
}
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie();
}
}
private:
bool isHungry();
}
考虑以下代码:
struct Cookie {};
struct CookieJarData {
int count;
int cost;
bool whatever;
Cookie cookie;
};
struct CookieJarInternal {
CookieJarInternal(CookieJarData *d): data{d} {}
Cookie getCookie() { return data->cookie; }
private:
CookieJarData *data;
};
struct CookieJar {
CookieJar(CookieJarData *d): data{d} {}
int count() { return data->count; }
private:
CookieJarData *data;
};
template<typename... T>
struct CookieJarTemplate: CookieJarData, T... {
CookieJarTemplate(): CookieJarData{}, T(this)... {}
};
using CookieJarImpl = CookieJarTemplate<CookieJar, CookieJarInternal>;
class CookieMonster {
public:
void feed(CookieJarInternal &cookieJar) {
while (isHungry()) {
cookieJar.getCookie();
}
}
bool isHungry() {
return false;
}
};
void userMethod(CookieJar &cookieJar) {}
int main() {
CookieJarImpl impl;
CookieMonster monster;
monster.feed(impl);
userMethod(impl);
}
基本思想是创建一个 class,它同时是数据并从一堆子 class 派生。
因此,class 是 它的子 class ,您可以通过选择正确的类型随时使用它们。
这样,combining class 有一个完整的界面,如果几个组件共享相同的数据,则构建起来,但您可以轻松地 return 简化视图其中 class 仍然没有虚拟方法。
另一种可能的方法是使用一种双重调度,如下例所示:
struct Cookie {};
struct CookieJarBase {
Cookie getCookie() { return Cookie{}; }
};
struct CookieMonster;
struct CookieJar;
struct CookieJar: private CookieJarBase {
void accept(CookieMonster &);
};
struct CookieMonster {
void feed(CookieJarBase &);
bool isHungry();
};
void CookieJar::accept(CookieMonster &m) {
CookieJarBase &base = *this;
m.feed(base);
}
void CookieMonster::feed(CookieJarBase &cj) {
while (isHungry()) {
cj.getCookie();
}
}
bool CookieMonster::isHungry() { return false; }
int main() {
CookieMonster monster;
CookieJar cj;
cj.accept(monster);
// the following line doesn't compile
// for CookieJarBase is not accesible
// monster.feed(cj);
}
这样你就没有虚拟方法,getCookie
对 class CookieMonster
.
的用户是不可访问的
老实说,问题转移到 feed
,现在用户无法使用,直接使用 accept
方法。
解决您的问题的是虚拟模板方法,那根本不可能。
否则,如果您不想像上面的示例那样公开不可用的方法,则无法避免虚方法或友元声明。
无论如何,这至少有助于隐藏您不想提供的 getCookie
等内部方法。
回答我自己的问题:这个想法是基于接口-实现关系,其中 public API 明确定义为接口,而实现细节位于单独的 class 扩展它,用户无法访问,但库的其余部分可以访问。
在使用 CRTP 实现静态多态性作为 πìντα ῥεῖ 建议避免虚拟调用开销的过程中,我意识到这种设计实际上根本不需要多态性,只要只有一种类型会实现接口即可。这使得任何类型的动态调度都毫无意义。在实践中,这意味着扁平化所有你从静态多态性中获得的丑陋模板,并以非常简单的东西结束。没有朋友,没有模板,(几乎)没有虚拟电话。让我们把它应用到上面的例子中:
这是 header,仅包含 public API 以及示例用法:
class CookieJar {
public:
static std::unique_ptr<CookieJar> Create(unsigned capacity);
bool isEmpty();
void fill();
virtual ~CookieJar() = 0 {};
};
class CookieMonster {
public:
void feed(CookieJar* cookieJar);
bool isHungry();
};
void main() {
std::unique_ptr<CookieJar> jar = CookieJar::Create(20);
jar->fill();
CookieMonster monster;
monster.feed(jar.get());
}
这里唯一的变化是将 CookieJar
变成抽象 class 并使用工厂模式而不是构造函数。
实现:
struct Cookie {
const bool isYummy = true;
};
class CookieJarImpl : public CookieJar {
public:
CookieJarImpl(unsigned capacity) :
capacity(capacity) {}
bool isEmpty() {
return count == 0;
}
void fill() {
count = capacity;
}
Cookie getCookie() {
if (!isEmpty()) {
count--;
return Cookie();
} else {
throw std::exception("Where did all the cookies go?");
}
}
private:
const unsigned capacity;
unsigned count = 0;
};
// CookieJar implementation - simple wrapper functions replacing dynamic dispatch
std::unique_ptr<CookieJar> CookieJar::Create(unsigned capacity) {
return std::make_unique<CookieJarImpl>(capacity);
}
bool CookieJar::isEmpty() {
return static_cast<CookieJarImpl*>(this)->isEmpty();
}
void CookieJar::fill() {
static_cast<CookieJarImpl*>(this)->fill();
}
// CookieMonster implementation
void CookieMonster::feed(CookieJar* cookieJar) {
while (isHungry()) {
static_cast<CookieJarImpl*>(cookieJar)->getCookie();
}
}
bool CookieMonster::isHungry() {
return true;
}
这似乎是一个总体上可靠的解决方案。它强制使用工厂模式,如果您需要复制和移动,则需要以与上述类似的方式自己定义包装器。这对于我的用例来说是可以接受的,因为无论如何我需要使用它的 classes 都是重量级资源。
我注意到的另一件有趣的事情是,如果你觉得真的很冒险,你可以用 reinterpret_casts 替换 static_casts 并且只要接口的每个方法都是你定义的包装器,包括析构函数,您可以安全地将任意 object 分配给您定义的接口。用于制作不透明包装纸和其他恶作剧。
对此我有两个想法。在第一个中,您创建一个 CookieJarPrivate
class 以将私有 CookieJar
方法公开给库的其他部分。 CookieJarPrivate
将在 header 文件中定义,该文件不构成 public API 的一部分。 CookieJar
会声明 CookieJarPrivate
为其 friend
。 cookiejar.h
在技术上没有必要包含 cookiejarprivate.h
,但这样做可以阻止您的客户试图滥用 friend
通过定义他们自己的 CookieJarPrivate
来访问实施细节。
class Cookie;
class CookieJarPrivate {
public:
Cookie getCookie();
private:
CookieJarPrivate(CookieJar& jar) : m_jar(jar) {}
CookieJar& m_jar;
};
class CookieJar {
friend class CookieJarPrivate;
public:
CookieJarPrivate getPrivate() { return *this; }
private:
Cookie getCookie();
};
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getPrivate().getCookie();
}
}
bool isHungry();
};
Cookie CookieJarPrivate::getCookie() {
return m_jar.getCookie();
}
编译器应该能够内联 CookieJarPrivate
构造函数和 getPrivate()
方法,因此性能应该等同于直接调用私有 getCookie()
。如果编译器选择不在 CookieJarPrivate::getCookie()
的实现中内联对 m_jar.getCookie()
的调用,您可能会付出一次额外函数调用的代价。它 可以 选择这样做,如果两种方法都在同一个翻译单元中定义,特别是如果它可以证明私有 getCookie()
没有在其他任何地方被调用,但它肯定是不保证。
第二个想法是class类型的虚拟参数,带有私有构造函数和CookieMonster
上的friend
关系,这样该方法只能由代码调用可以构造此虚拟类型,即仅 CookieMonster
。这与普通 friend
类似,但粒度更细。
template <class T> class Restrict {
friend T;
private:
Restrict() {}
};
class Cookie;
class CookieMonster;
class CookieJar {
public:
Cookie getCookie(Restrict<CookieMonster>);
};
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie({});
}
}
bool isHungry();
};
它的一个变体是 non-template 假人,没有 friend
,在 non-public header 中定义。关于公开哪些方法仍然很精细,但它们会公开给您的整个库,而不仅仅是 CookieMonster
.
class PrivateAPI;
class Cookie;
class CookieJar {
public:
Cookie getCookie(PrivateAPI);
};
class CookieMonster {
public:
void feed(CookieJar cookieJar);
bool isHungry();
};
class PrivateAPI {};
void CookieMonster::feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie({});
}
}
我也想知道如何正确公开我的代码 API,我发现 PIMPL 习惯用法是最好的解决方案。你已经提到了,但我不同意这句话:
The Pimpl idiom offers to hide the private members of a class, but
does little to disguise the public methods that are not supposed to be
a part of the API.
假设我们有以下代码:
namespace Core {
class Cookie {
};
class CookieJar {
public:
CookieJar(unsigned _capacity): capacity(_capacity) {}
bool isEmpty() {
return count == 0;
}
void fill() {
count = capacity;
}
Cookie getCookie() {
if (!isEmpty()) {
this->count--;
return Cookie();
}
throw std::exception();
}
private:
const unsigned capacity;
unsigned count = 0;
};
class CookieMonster {
public:
void feedOne(CookieJar* cookieJar) {
cookieJar->getCookie();
return;
}
};
} // namespace Core
现在我们要添加API层,但要求是隐藏一些方法和类内部实现。这完全可以在不修改核心的情况下完成!只需 添加 以下代码:
namespace API {
class CookieJar {
friend class CookieMonster;
public:
CookieJar(unsigned _capacity) {
this->impl_ = std::make_unique<Core::CookieJar>(_capacity);
}
bool isEmpty() {
return impl_->isEmpty();
}
void fill() {
return impl_->fill();
}
protected:
std::experimental::propagate_const<std::unique_ptr<Core::CookieJar>> impl_;
};
class CookieMonster {
public:
CookieMonster() {
this->impl_ = std::make_unique<Core::CookieMonster>();
}
void feedOne(CookieJar* jar) {
return impl_->feedOne(jar->impl_);
}
protected:
std::experimental::propagate_const<std::unique_ptr<Core::CookieMonster>> impl_;
};
} // namespace API
用法示例:
int main() {
{
using namespace Core;
CookieJar* jar = new CookieJar(10);
jar->fill();
jar->getCookie();
CookieMonster monster;
monster.feedOne(jar);
new Cookie();
}
{
using namespace API;
CookieJar* jar = new CookieJar(10);
jar->fill();
//jar->getCookie(); // <- hidden from API
CookieMonster monster;
monster.feedOne(jar);
//new Cookie(); // <- hidden from API
}
return 0;
}
如您所见,使用 PIMPL 我们可以隐藏一些 类、一些 public 方法。也可以在不修改基本代码的情况下创建多个 API 层。 PIMPL 也适用于摘要 类。
对于我的库,我想公开一个干净的 public API,不会分散实施细节的注意力。但是,正如您所拥有的,这些细节甚至会泄露到 public 领域:一些 classes 具有有效的 public 方法,这些方法被库的其余部分使用,但不是对于 API 的用户非常有用,因此不需要成为它的一部分。 public代码的简化示例:
class Cookie;
class CookieJar {
public:
Cookie getCookie();
}
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie();
}
}
bool isHungry();
}
CookieJar
的 getCookie()
方法对库的用户没有用,他们大概不喜欢 cookie。然而,当给定一个时,CookieMonster
会用它来喂养自己。
有一些习语可以帮助解决这个问题。 Pimpl 惯用语提供隐藏 class 的私有成员,但几乎没有掩饰不应属于 API 的 public 方法。也可以将它们移动到实现 class 中,但是您需要提供对它的直接访问以供库的其余部分使用。这样的 header 看起来像这样:
class Cookie;
class CookieJarImpl;
class CookieJar {
public:
CookieJarImpl* getImplementation() {
return pimpl.get();
}
private:
std::unique_ptr<CookieJarImpl> pimpl;
}
如果您真的需要阻止用户访问这些方法,这会很方便,但如果这只是一种烦恼,这就没有多大帮助。事实上,新方法现在比上一个更无用,因为用户无权访问 CookieJarImpl
.
另一种方法是将接口定义为抽象基础 class。这可以明确控制 public API 的一部分。任何私人细节都可以包含在该接口的实现中,用户无法访问。需要注意的是,由此产生的虚拟调用会影响性能,甚至比 Pimpl 习惯用法更严重。清洁器的交易速度 API 对于应该是高性能库的东西来说并不是很有吸引力。
为了详尽无遗,另一种选择是将有问题的方法设为私有,并在需要从外部访问它们的地方使用 friend classes。然而,这使目标 objects 也可以访问真正的私有成员,这在某种程度上破坏了封装。
到目前为止,对我来说最好的解决方案似乎是 Python 方式:不要试图隐藏实现细节,只需适当地命名它们,这样就可以很容易地将它们识别为不属于 public API 并且不要分散常规使用的注意力。想到的命名约定是使用下划线前缀,但显然这样的名称是为编译器保留的,不鼓励使用它们。
是否有任何其他 C++ 命名约定来区分不打算从库外部使用的成员?或者你会建议我使用上面的替代方法之一还是我错过的其他方法?
您应该在您的 CookieJar class 中使用一个私有容器,在调用构造函数时该容器中会充满 cookie。在下面的代码中,我使用了一个STL C++库的vector作为容器,因为使用方便,但是你也可以使用其他的东西(array,list,map等),并且把cookies的属性设为private .您也可以隐藏 monster isHungry
属性以获得更好的封装。
如果你想对库的用户隐藏 getCookie()
方法,那么你应该将此方法设为私有,并将 CookieMonster
class 视为好友 class 的 CookieJar
,因此 CookieMonster
将能够使用 getCookie()
方法,而用户将无法使用。
#include<vector>
using namespace std;
class Cookie
{
private:
string type;
string chocolateFlavor;
}
class CookieJar {
friend class CookieMonster;
public:
CookieJar(){
//loads a cookie jar with 10 cookies
for (int i = 0; i = 10; i++) {
Cookie cookie;
cookieContainer.push_back(cookie);
}
}
private:
vector<Cookie> cookieContainer;
Cookie getCookie(){
//returns a cookie to feed and deletes one in the container
Cookie toFeed = cookieContainer[0];
cookieContainer[0] = *cookieContainer.back();
cookieContainer.pop_back();
return toFeed;
}
}
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie();
}
}
private:
bool isHungry();
}
考虑以下代码:
struct Cookie {};
struct CookieJarData {
int count;
int cost;
bool whatever;
Cookie cookie;
};
struct CookieJarInternal {
CookieJarInternal(CookieJarData *d): data{d} {}
Cookie getCookie() { return data->cookie; }
private:
CookieJarData *data;
};
struct CookieJar {
CookieJar(CookieJarData *d): data{d} {}
int count() { return data->count; }
private:
CookieJarData *data;
};
template<typename... T>
struct CookieJarTemplate: CookieJarData, T... {
CookieJarTemplate(): CookieJarData{}, T(this)... {}
};
using CookieJarImpl = CookieJarTemplate<CookieJar, CookieJarInternal>;
class CookieMonster {
public:
void feed(CookieJarInternal &cookieJar) {
while (isHungry()) {
cookieJar.getCookie();
}
}
bool isHungry() {
return false;
}
};
void userMethod(CookieJar &cookieJar) {}
int main() {
CookieJarImpl impl;
CookieMonster monster;
monster.feed(impl);
userMethod(impl);
}
基本思想是创建一个 class,它同时是数据并从一堆子 class 派生。
因此,class 是 它的子 class ,您可以通过选择正确的类型随时使用它们。
这样,combining class 有一个完整的界面,如果几个组件共享相同的数据,则构建起来,但您可以轻松地 return 简化视图其中 class 仍然没有虚拟方法。
另一种可能的方法是使用一种双重调度,如下例所示:
struct Cookie {};
struct CookieJarBase {
Cookie getCookie() { return Cookie{}; }
};
struct CookieMonster;
struct CookieJar;
struct CookieJar: private CookieJarBase {
void accept(CookieMonster &);
};
struct CookieMonster {
void feed(CookieJarBase &);
bool isHungry();
};
void CookieJar::accept(CookieMonster &m) {
CookieJarBase &base = *this;
m.feed(base);
}
void CookieMonster::feed(CookieJarBase &cj) {
while (isHungry()) {
cj.getCookie();
}
}
bool CookieMonster::isHungry() { return false; }
int main() {
CookieMonster monster;
CookieJar cj;
cj.accept(monster);
// the following line doesn't compile
// for CookieJarBase is not accesible
// monster.feed(cj);
}
这样你就没有虚拟方法,getCookie
对 class CookieMonster
.
的用户是不可访问的
老实说,问题转移到 feed
,现在用户无法使用,直接使用 accept
方法。
解决您的问题的是虚拟模板方法,那根本不可能。
否则,如果您不想像上面的示例那样公开不可用的方法,则无法避免虚方法或友元声明。
无论如何,这至少有助于隐藏您不想提供的 getCookie
等内部方法。
回答我自己的问题:这个想法是基于接口-实现关系,其中 public API 明确定义为接口,而实现细节位于单独的 class 扩展它,用户无法访问,但库的其余部分可以访问。
在使用 CRTP 实现静态多态性作为 πìντα ῥεῖ 建议避免虚拟调用开销的过程中,我意识到这种设计实际上根本不需要多态性,只要只有一种类型会实现接口即可。这使得任何类型的动态调度都毫无意义。在实践中,这意味着扁平化所有你从静态多态性中获得的丑陋模板,并以非常简单的东西结束。没有朋友,没有模板,(几乎)没有虚拟电话。让我们把它应用到上面的例子中:
这是 header,仅包含 public API 以及示例用法:
class CookieJar {
public:
static std::unique_ptr<CookieJar> Create(unsigned capacity);
bool isEmpty();
void fill();
virtual ~CookieJar() = 0 {};
};
class CookieMonster {
public:
void feed(CookieJar* cookieJar);
bool isHungry();
};
void main() {
std::unique_ptr<CookieJar> jar = CookieJar::Create(20);
jar->fill();
CookieMonster monster;
monster.feed(jar.get());
}
这里唯一的变化是将 CookieJar
变成抽象 class 并使用工厂模式而不是构造函数。
实现:
struct Cookie {
const bool isYummy = true;
};
class CookieJarImpl : public CookieJar {
public:
CookieJarImpl(unsigned capacity) :
capacity(capacity) {}
bool isEmpty() {
return count == 0;
}
void fill() {
count = capacity;
}
Cookie getCookie() {
if (!isEmpty()) {
count--;
return Cookie();
} else {
throw std::exception("Where did all the cookies go?");
}
}
private:
const unsigned capacity;
unsigned count = 0;
};
// CookieJar implementation - simple wrapper functions replacing dynamic dispatch
std::unique_ptr<CookieJar> CookieJar::Create(unsigned capacity) {
return std::make_unique<CookieJarImpl>(capacity);
}
bool CookieJar::isEmpty() {
return static_cast<CookieJarImpl*>(this)->isEmpty();
}
void CookieJar::fill() {
static_cast<CookieJarImpl*>(this)->fill();
}
// CookieMonster implementation
void CookieMonster::feed(CookieJar* cookieJar) {
while (isHungry()) {
static_cast<CookieJarImpl*>(cookieJar)->getCookie();
}
}
bool CookieMonster::isHungry() {
return true;
}
这似乎是一个总体上可靠的解决方案。它强制使用工厂模式,如果您需要复制和移动,则需要以与上述类似的方式自己定义包装器。这对于我的用例来说是可以接受的,因为无论如何我需要使用它的 classes 都是重量级资源。
我注意到的另一件有趣的事情是,如果你觉得真的很冒险,你可以用 reinterpret_casts 替换 static_casts 并且只要接口的每个方法都是你定义的包装器,包括析构函数,您可以安全地将任意 object 分配给您定义的接口。用于制作不透明包装纸和其他恶作剧。
对此我有两个想法。在第一个中,您创建一个 CookieJarPrivate
class 以将私有 CookieJar
方法公开给库的其他部分。 CookieJarPrivate
将在 header 文件中定义,该文件不构成 public API 的一部分。 CookieJar
会声明 CookieJarPrivate
为其 friend
。 cookiejar.h
在技术上没有必要包含 cookiejarprivate.h
,但这样做可以阻止您的客户试图滥用 friend
通过定义他们自己的 CookieJarPrivate
来访问实施细节。
class Cookie;
class CookieJarPrivate {
public:
Cookie getCookie();
private:
CookieJarPrivate(CookieJar& jar) : m_jar(jar) {}
CookieJar& m_jar;
};
class CookieJar {
friend class CookieJarPrivate;
public:
CookieJarPrivate getPrivate() { return *this; }
private:
Cookie getCookie();
};
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getPrivate().getCookie();
}
}
bool isHungry();
};
Cookie CookieJarPrivate::getCookie() {
return m_jar.getCookie();
}
编译器应该能够内联 CookieJarPrivate
构造函数和 getPrivate()
方法,因此性能应该等同于直接调用私有 getCookie()
。如果编译器选择不在 CookieJarPrivate::getCookie()
的实现中内联对 m_jar.getCookie()
的调用,您可能会付出一次额外函数调用的代价。它 可以 选择这样做,如果两种方法都在同一个翻译单元中定义,特别是如果它可以证明私有 getCookie()
没有在其他任何地方被调用,但它肯定是不保证。
第二个想法是class类型的虚拟参数,带有私有构造函数和CookieMonster
上的friend
关系,这样该方法只能由代码调用可以构造此虚拟类型,即仅 CookieMonster
。这与普通 friend
类似,但粒度更细。
template <class T> class Restrict {
friend T;
private:
Restrict() {}
};
class Cookie;
class CookieMonster;
class CookieJar {
public:
Cookie getCookie(Restrict<CookieMonster>);
};
class CookieMonster {
public:
void feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie({});
}
}
bool isHungry();
};
它的一个变体是 non-template 假人,没有 friend
,在 non-public header 中定义。关于公开哪些方法仍然很精细,但它们会公开给您的整个库,而不仅仅是 CookieMonster
.
class PrivateAPI;
class Cookie;
class CookieJar {
public:
Cookie getCookie(PrivateAPI);
};
class CookieMonster {
public:
void feed(CookieJar cookieJar);
bool isHungry();
};
class PrivateAPI {};
void CookieMonster::feed(CookieJar cookieJar) {
while (isHungry()) {
cookieJar.getCookie({});
}
}
我也想知道如何正确公开我的代码 API,我发现 PIMPL 习惯用法是最好的解决方案。你已经提到了,但我不同意这句话:
The Pimpl idiom offers to hide the private members of a class, but does little to disguise the public methods that are not supposed to be a part of the API.
假设我们有以下代码:
namespace Core {
class Cookie {
};
class CookieJar {
public:
CookieJar(unsigned _capacity): capacity(_capacity) {}
bool isEmpty() {
return count == 0;
}
void fill() {
count = capacity;
}
Cookie getCookie() {
if (!isEmpty()) {
this->count--;
return Cookie();
}
throw std::exception();
}
private:
const unsigned capacity;
unsigned count = 0;
};
class CookieMonster {
public:
void feedOne(CookieJar* cookieJar) {
cookieJar->getCookie();
return;
}
};
} // namespace Core
现在我们要添加API层,但要求是隐藏一些方法和类内部实现。这完全可以在不修改核心的情况下完成!只需 添加 以下代码:
namespace API {
class CookieJar {
friend class CookieMonster;
public:
CookieJar(unsigned _capacity) {
this->impl_ = std::make_unique<Core::CookieJar>(_capacity);
}
bool isEmpty() {
return impl_->isEmpty();
}
void fill() {
return impl_->fill();
}
protected:
std::experimental::propagate_const<std::unique_ptr<Core::CookieJar>> impl_;
};
class CookieMonster {
public:
CookieMonster() {
this->impl_ = std::make_unique<Core::CookieMonster>();
}
void feedOne(CookieJar* jar) {
return impl_->feedOne(jar->impl_);
}
protected:
std::experimental::propagate_const<std::unique_ptr<Core::CookieMonster>> impl_;
};
} // namespace API
用法示例:
int main() {
{
using namespace Core;
CookieJar* jar = new CookieJar(10);
jar->fill();
jar->getCookie();
CookieMonster monster;
monster.feedOne(jar);
new Cookie();
}
{
using namespace API;
CookieJar* jar = new CookieJar(10);
jar->fill();
//jar->getCookie(); // <- hidden from API
CookieMonster monster;
monster.feedOne(jar);
//new Cookie(); // <- hidden from API
}
return 0;
}
如您所见,使用 PIMPL 我们可以隐藏一些 类、一些 public 方法。也可以在不修改基本代码的情况下创建多个 API 层。 PIMPL 也适用于摘要 类。