C++ API设计:清理公共接口

Qui*_*ion 6 c++ api naming-conventions

对于我的库,我想公开一个干净的公共API,它不会分散实现细节.但是,正如你所知,这些细节甚至泄漏到了公共领域:有些类具有库的其余部分使用的有效公共方法,但对API的用户来说并不是非常有用,因此需要成为它的一部分.公共代码的简化示例:

class Cookie;

class CookieJar {
public:
    Cookie getCookie();
}

class CookieMonster {
public:
    void feed(CookieJar cookieJar) {
        while (isHungry()) {
            cookieJar.getCookie();
        }
    }

    bool isHungry();
}
Run Code Online (Sandbox Code Playgroud)

a的getCookie()方法CookieJar对于库的用户没用,他们可能不喜欢cookie.然而CookieMonster,当给出它时,它被用于喂养它自己.

有一些成语有助于解决这个问题.Pimpl习语提供隐藏类的私有成员,但几乎不掩饰不应该成为API一部分的公共方法.也可以将它们移动到实现类中,但是您需要提供对它的直接访问以供库的其余部分使用.这样的标题看起来像这样:

class Cookie;
class CookieJarImpl;

class CookieJar {
public:
    CookieJarImpl* getImplementation() {
        return pimpl.get();
    }
private:
    std::unique_ptr<CookieJarImpl> pimpl;
}
Run Code Online (Sandbox Code Playgroud)

如果你真的需要阻止用户访问这些方法,这很方便,但如果它只是一个烦恼,这没有多大帮助.实际上,新方法现在比上一个方法更无用,因为用户无权访问实现CookieJarImpl.

另一种方法是将接口定义为抽象基类.这可以明确控制公共API的一部分.任何私有细节都可以包含在此界面的实现中,这是用户无法访问的.需要注意的是,由此产生的虚拟调用会影响性能,甚至比Pimpl成语更具影响力.更清洁的API的交易速度对于应该是高性能库而言并不是很有吸引力.

为了详尽无遗,另一种选择是将有问题的方法设为私有,并在需要的地方使用朋友类从外部访问它们.但是,这使得目标对象也可以访问真正的私有成员,这有点破坏了封装.

到目前为止,对我来说最好的解决方案似乎是Python方式:不要试图隐藏实现细节,只需恰当地命名它们,这样它们很容易识别为不是公共API的一部分,也不会分散常规用法的注意力.想到的命名约定是使用下划线前缀,但显然这些名称是为编译器保留的,不鼓励使用它们.

是否有任何其他c ++命名约定用于区分不打算从库外使用的成员?或者你会建议我使用上面的替代品之一或我错过的其他东西?

Qui*_*ion 3

回答我自己的问题:这个想法基于接口 - 实现关系,其中公共 API 被显式定义为接口,而实现细节则驻留在扩展它的单独类中,用户无法访问,但其他人可以访问图书馆。

\n\n

使用 CRTP 作为 \xcf\x80\xce\xac\xce\xbd\xcf\x84\xce\xb1 \xe1\xbf\xa5\xce\xb5\xe1\xbf\x96 实现静态多态性的一半建议避免虚拟调用开销,我意识到这种设计实际上根本不需要多态性,只要只有一种类型会实现该接口即可。这使得任何类型的动态调度都毫无意义。在实践中,这意味着扁平化从静态多态性中获得的所有丑陋模板,并最终得到非常简单的东西。没有朋友,没有模板,(几乎)没有虚拟通话。让我们将其应用到上面的示例中:

\n\n

这是标头,仅包含公共 API 和示例用法:

\n\n
class CookieJar {\npublic:\n    static std::unique_ptr<CookieJar> Create(unsigned capacity);\n\n    bool isEmpty();\n    void fill();\n\n    virtual ~CookieJar() = 0 {};\n};\n\nclass CookieMonster {\npublic:\n    void feed(CookieJar* cookieJar);\n    bool isHungry();\n};\n\nvoid main() {\n    std::unique_ptr<CookieJar> jar = CookieJar::Create(20);\n    jar->fill();\n    CookieMonster monster;\n    monster.feed(jar.get());\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里唯一的变化是变成CookieJar一个抽象类并使用工厂模式而不是构造函数。

\n\n

实施:

\n\n
struct Cookie {\n    const bool isYummy = true;\n};\n\nclass CookieJarImpl : public CookieJar {\npublic:\n    CookieJarImpl(unsigned capacity) :\n        capacity(capacity) {}\n\n    bool isEmpty() {\n        return count == 0;\n    }\n\n    void fill() {\n        count = capacity;\n    }\n\n    Cookie getCookie() {\n        if (!isEmpty()) {\n            count--;\n            return Cookie();\n        } else {\n            throw std::exception("Where did all the cookies go?");\n        }\n    }\n\nprivate:\n    const unsigned capacity;\n    unsigned count = 0;\n};\n\n// CookieJar implementation - simple wrapper functions replacing dynamic dispatch\nstd::unique_ptr<CookieJar> CookieJar::Create(unsigned capacity) {\n    return std::make_unique<CookieJarImpl>(capacity);\n}\n\nbool CookieJar::isEmpty() {\n    return static_cast<CookieJarImpl*>(this)->isEmpty();\n}\n\nvoid CookieJar::fill() {\n    static_cast<CookieJarImpl*>(this)->fill();\n}\n\n// CookieMonster implementation\nvoid CookieMonster::feed(CookieJar* cookieJar) {\n    while (isHungry()) {\n        static_cast<CookieJarImpl*>(cookieJar)->getCookie();\n    }\n}\n\nbool CookieMonster::isHungry() {\n    return true;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

总的来说,这似乎是一个可靠的解决方案。它强制使用工厂模式,如果您需要复制和移动,您需要以与上述类似的方式自己定义包装器。这对于我的用例来说是可以接受的,因为无论如何我需要使用它的类都是重量级资源。

\n\n

我注意到的另一件有趣的事情是,如果您真的很冒险,您可以用reinterpret_casts替换static_casts,只要接口的每个方法都是您定义的包装器,包括析构函数,您就可以安全地将任何任意对象分配给您定义的接口。可用于制作不透明的包装纸和其他恶作剧。

\n