C++ API 设计:清理公共接口
C++ API design: Clearing up public interface
对于我的库,我想公开一个干净的公共 API,它不会分散实现细节的注意力。但是,正如您所看到的那样,这些细节甚至会泄漏到公共领域:某些类具有有效的公共方法,这些方法由库的其余部分使用,但对 API 的用户不是很有用,因此不需要成为它的一部分。公共代码的简化示例:
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 习惯用法提供了隐藏类的私有成员的功能,但对伪装不应该成为 API 一部分的公共方法几乎没有作用。也可以将它们移动到实现类中,但随后需要提供对它的直接访问,供库的其余部分使用。这样的标头如下所示:
class Cookie;
class CookieJarImpl;
class CookieJar {
public:
CookieJarImpl* getImplementation() {
return pimpl.get();
}
private:
std::unique_ptr<CookieJarImpl> pimpl;
}
如果您确实需要阻止用户访问这些方法,这很方便,但如果它只是一个烦恼,这并没有多大帮助。事实上,新的方法现在比上一个更无用,因为用户无法访问CookieJarImpl
的实现。
另一种方法是将接口定义为抽象基类。这样可以显式控制公共 API 的一部分。任何私人详细信息都可以包含在此接口的实现中,用户无法访问该接口。需要注意的是,由此产生的虚拟调用对性能的影响甚至比 Pimpl 习语还要大。对于应该是一个高性能库来说,一个更干净的 API 的交易速度并不是很有吸引力。
为了详尽无遗,另一种选择是将有问题的方法设为私有,并在需要从外部访问它们的地方使用友元类。但是,这也使目标对象可以访问真正的私有成员,这在某种程度上破坏了封装。
到目前为止,对我来说最好的解决方案似乎是 Python 方式:与其试图隐藏实现细节,不如适当地命名它们,以便它们很容易识别为不是公共 API 的一部分,并且不会分散常规使用的注意力。想到的命名约定是使用下划线前缀,但显然这些名称是为编译器保留的,不鼓励使用它们。
是否有任何其他 C++ 命名约定来区分不打算从库外部使用的成员?或者你会建议我使用上面的替代方案之一或我错过的其他东西吗?
回答我自己的问题:这个想法基于接口 - 实现关系,其中公共 API 被显式定义为接口,而实现细节驻留在一个单独的类中扩展它,用户无法访问,但库的其余部分可以访问。
在使用 CRTP 实现静态多态性的过程中,按照 πάντα ῥεῖ 的建议避免虚拟调用开销,我意识到这种设计实际上根本不需要多态性,只要只有一种类型可以实现接口。这使得任何类型的动态调度都毫无意义。在实践中,这意味着扁平化你从静态多态性中获得的所有丑陋的模板,并最终得到一些非常简单的东西。没有朋友,没有模板,(几乎(没有虚拟通话。让我们将其应用于上面的示例:
下面是标头,仅包含带有示例用法的公共 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
转换为抽象类,并使用工厂模式而不是构造函数。
实现:
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;
}
总的来说,这似乎是一个可靠的解决方案。它强制使用工厂模式,如果您需要复制和移动,则需要以与上述类似的方式自己定义包装器。这对于我的用例来说是可以接受的,因为无论如何,我需要使用它的类都是重量级资源。
我注意到的另一件有趣的事情是,如果你真的很喜欢冒险,你可以用reinterpret_casts替换static_casts,只要接口的每个方法都是你定义的包装器,包括析构函数,你可以安全地将任意对象分配给你定义的接口。可用于制作不透明的包装纸和其他恶作剧。
请考虑以下代码:
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);
}
基本思想是创建一个类,该类同时是数据并派生自一堆子类。
因此,该类是其子类,您可以通过选择正确的类型随时使用它们。这样,组合类就有一个完整的接口,并且如果几个组件共享相同的数据,则构建,但您可以轻松地返回仍然没有虚拟方法的该类的简化视图。
我对此有两个想法。在第一个中,您将创建一个 CookieJarPrivate
类,以向库的其他部分公开私有CookieJar
方法。 CookieJarPrivate
将在头文件中定义,该文件不构成公共 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()
的直接调用。如果编译器选择不内联对m_jar.getCookie()
的调用,则可能会支付一次额外函数调用的代价 CookieJarPrivate::getCookie()
。如果两种方法都在同一翻译单元中定义,它可以选择这样做,特别是如果它可以证明私有getCookie()
没有在其他任何地方调用,但肯定不能保证。
第二个想法是类类型的虚拟参数,在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();
};
其变体是非模板假人,没有friend
,在非公共标头中定义。对于公开哪些方法,它仍然是精细的,但它们会公开到您的整个库,而不仅仅是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({});
}
}
您应该在 CookieJar 类中使用一个私有容器,该容器在调用构造函数时被 cookie 填满。在下面的代码中,我使用了 STL C++ 库的向量作为容器,因为它使用起来很方便,但您可以使用其他东西(数组、列表、映射等(,并将 cookie 的属性设为私有。您也可以隐藏怪物isHungry
属性以获得更好的封装。
如果要对库的用户隐藏 getCookie()
方法,则应将此方法设为私有,并将CookieMonster
类视为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 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);
}
这样,您就没有虚拟方法,并且类CookieMonster
的用户无法访问getCookie
。
老实说,问题转移到 feed
,现在用户无法使用,旨在直接用作accept
方法。
解决您的问题的是虚拟模板方法,这根本不可能。
否则,如果您不想公开不可用的方法(如上例所示(,则无法避免虚拟方法或友元声明。
无论如何,这至少有助于隐藏您不想提供的内部方法,例如getCookie
。
我也想知道如何正确公开我的代码的 API,我发现 PIMPL 习语是最好的解决方案。你已经提到过了,但我不同意这句话:
Pimpl 成语提供隐藏班级的私人成员,但是 几乎没有掩饰不应该的公共方法 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,我们可以隐藏一些类,一些公共方法。还可以创建多个 API 层,而无需修改基本代码。PIMPL也适用于抽象类。
- 用于访问容器<T>数据成员的正确 API
- 如何使用Luacneneneba API正确读取字符串和表参数
- C++MySQL C api用户输入行
- 如何使用 AWS Transcribe C++ API 中的'StartTranscriptionJobRequest'?
- Asio如何包含基于BSD套接字API的低级套接字接口
- API 抽象层 - 避免混合使用 API 接口
- 可视化 使用 C++ API 注册 COM DLL 的所有接口
- 如何定义在多个 cpp 文件中使用的接口/API
- 用于修改网络接口属性的 Win32 API
- 函数指针问题.如何有效地与C API接口(即.GSL)来自C++类
- 将函数指针作为API接口传递到已编译的库
- 构建一个RESTFul C++api来与Python接口
- 如何通过Python C api在QT和Python之间进行接口
- 如何在公共 API 接口类中将 auto getter&setter 与 PIMPL 设计模式相结合
- Win32音频API endpointVolume接口返回错误的通道计数
- C/MATLAB API接口环境变量设置影响OS X中的其他应用程序
- 生成器将c++ API更改为C接口
- 如何使用包括派生接口在内的接口正确地公开C++API
- C++ API 设计:清理公共接口
- 编写语言脚本并与C API接口