Quinchilion
Quinchilion

Reputation: 922

C++ API design: Clearing up public interface

For my library, I want to expose a clean public API that does not distract with implementation details. As you have it, though, these details are leaking even to the public realm: Some classes have valid public methods that are used by the rest of the library, but aren't very useful for the user of the API and as such don't need to be a part of it. A simplified example of the public code:

class Cookie;

class CookieJar {
public:
    Cookie getCookie();
}

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

    bool isHungry();
}

The getCookie() method of a CookieJar is not useful to the user of the library, who presumably does not like cookies anyway. It is, however, used by the CookieMonster to feed itself, when given one.

There are some idioms that help solve this issue. 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. It is possible to move those into the implementation class as well, but you would then need to provide direct access to it for the rest of the library to use. Such a header would look like this:

class Cookie;
class CookieJarImpl;

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

It is handy if you really need to prevent user access to these methods, but if it's merely an annoyance, this doesn't help very much. In fact, new the method is now even more useless than the last, because the user does not have access to the implementation of CookieJarImpl.

An alternative approach is to define the interface as an abstract base class. This gives explicit control over what is a part of the public API. Any private details can be included in the implementation of this interface, which is inaccessible to the user. The caveat is that the resulting virtual calls impact performance, even more so than the Pimpl idiom. Trading speed for a cleaner API is not very attractive for what is supposed to be a high performance library.

To be exhaustive, yet another option is to make the problematic methods private and use friend classes where needed to access them from the outside. This, however, gives the target objects access to the truly private members as well, somewhat breaking encapsulation.

So far the best solution to me seems to be the Python way: Instead of trying to hide the implementation details, just name them appropriately, so that they are easily identifiable as not part of the public API and do not distract from regular usage. The naming convention that comes to mind is using an underscore prefix, but apparently such names are reserved for the compiler and their use is discouraged.

Are there any other c++ naming conventions for distinguishing members that are not intended to be used from outside the library? Or would you instead suggest me to use one of the alternatives above or something else I missed?

Upvotes: 8

Views: 5182

Answers (6)

andrzej1_1
andrzej1_1

Reputation: 1193

I was also wondering how to properly expose API of my code and I found PIMPL idiom is the best solution. You already mentioned it, but I disagree with sentence:

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.

Let's consider we have following code:

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

Now we want to add API layer, but the requirement is to hide some method and classes of internal implementation. And this can be done without modifying core at all! Just add following code:

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

Usage example:

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

As you can see, with PIMPL we can hide some classes, some public methods. It is also possible to create multiple API layers, without modifying base code. PIMPL works with abstract classes too.

Upvotes: 0

Quinchilion
Quinchilion

Reputation: 922

Answering my own question: This idea is based on the interface - implementation relationship, where the public API is explicitly defined as the interface, while the implementation details reside in a separate class extending it, inaccessible to the user, but accessible to the rest of the library.

Halfway through implementing static polymorphism using CRTP as πάντα ῥεῖ suggested to avoid virtual call overhead, I realized polymorphism is not actually needed at all for this kind of design, as long as only one type will ever implement the interface. That makes any kind of dynamic dispatch pointless. In practice, this means flattening all the ugly templates you get from static polymorphism and ending up with something very simple. No friends, no templates, (almost) no virtual calls. Let's apply it to the example above:

Here is the header, containing just the public API with example usage:

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

The only change here is turning CookieJar into an abstract class and using a factory pattern instead of a constructor.

The implementations:

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

This seems like a solid solution overall. It forces using a factory pattern and if you need copying and moving, you need to define the wrappers yourself in a similar fashion to the above. That is acceptable for my use case, since the classes I needed to use this for are heavyweight resources anyway.

Another interesting thing I noticed is that if you feel really adventurous, you can replace static_casts with reinterpret_casts and as long as every method of the interface is a wrapper you define, including the destructor, you can safely assign any arbitrary object to an interface you define. Useful for making opaque wrappers and other shenanigans.

Upvotes: 3

Oktalist
Oktalist

Reputation: 14714

I have two ideas for this. In the first one, you create a CookieJarPrivate class to expose the private CookieJar methods to other parts of your library. CookieJarPrivate would be defined in a header file which does not form part of your public API. CookieJar would declare CookieJarPrivate to be its friend. It's technically not necessary for cookiejar.h to include cookiejarprivate.h, but doing so stops your clients trying to abuse the friend to gain access to implementation details by defining their own 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();
}

The compiler should be able to inline the CookieJarPrivate constructor and the getPrivate() method, so performance should be equivalent to a direct call to the private getCookie(). You might pay the penalty of one extra function call if the compiler elects not to inline the call to m_jar.getCookie() in the implementation of CookieJarPrivate::getCookie(). It could elect to do so, if both methods were defined in the same translation unit, especially if it could prove that the private getCookie() is not called anywhere else, but it's certainly not guaranteed.


The second idea is a dummy parameter of class type, with a private constructor and a friend relation on CookieMonster, so that the method can only be called by code which can construct this dummy type, i.e. only CookieMonster. This is like a normal friend but with finer granularity.

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

A variation of this is a non-template dummy, with no friend, defined in a non-public header. It is still granular with respect to which methods are exposed, but they become exposed to your entire library, not just 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({});
    }
}

Upvotes: 1

skypjack
skypjack

Reputation: 50550

Another possible approach would be with a kind of double dispatching, like in the following example:

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

This way you don't have virtual methods and getCookie is not accessible to the user of the class CookieMonster.
To be honest, the problem moved to feed, that is now unusable from the users and is meant to be used directly be the accept method.

What would solve your problem is a virtual template method, that is simply impossible.
Otherwise, you cannot avoid virtual methods or friend declarations if you don't want to expose unusable methods like in the example above.

Anyway, this at least helps to hide internal methods like getCookie that you don't want to make available.

Upvotes: 0

skypjack
skypjack

Reputation: 50550

Consider the following code:

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

The basic idea is to create a class that is at the same time the data and derives from a bunch of subclasses.
Because of that, the class is its subclasses and you can use them whenever you want by choosing the right type. This way, the combining class has a full interface and is built up if a few components that share the same data, but you can easily return a reduced view of that class that still doesn't have virtual methods.

Upvotes: 2

wayland700
wayland700

Reputation: 48

You should use a private container in your CookieJar class that is being fulled with cookies when the constructor is called. In the code below, I used a vector of the STL C++ library as a container, because it is convenient to use, but you can use something else (array, list, map, etc.), and made the properties of the cookies private. You can hide the monster isHungry attribute too for better encapsulation.

If you want to hide the getCookie() method from the user of the library, then you should make this method private and consider the CookieMonster class as a friend class of the CookieJar, so the CookieMonster will be able to use the getCookie() method and the user will not be able.

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

Upvotes: 0

Related Questions