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();
}

CookieJargetCookie() 方法对库的用户没有用,他们大概不喜欢 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 为其 friendcookiejar.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 也适用于摘要 类。