Api Design for Cpp
Introduction
More Robust Code
1 | Hides implementation. |
1.3.3 Parallel Development
By stubbing out the API early on, you can write unit tests to validate the desired functionality and run these continuously
to make sure that you haven’t broken your contract with your colleague.
1.6 FILE FORMATS AND NETWORK PROTOCOLS
Whenever you create a file format or client/server protocol, you should also create an API for it. This allows details
of the specification, and any future changes to it, to be centralized and hidden
Qualities
2.2 HIDE IMPLEMENTATION DETAILS
physical and logical hiding. Physical hiding means that the private source code is simply not made available to users.
Physical hiding means storing internal details in a separate file (.cpp) from the public interface (.h).
Logical hiding entails the use of language features to limit access to certain elements of the API.
Logical hiding means using the C++ language features of protected and private to restrict access to internal details.
2.2.3 Hide Member Variables
Data members of a class should always be declared private, never public or protected.
If you make a variable protected, then it can be accessed directly by any clients that subclass your class,
and then exactly the same arguments apply as for the public case.
2.2.4 Hide Implementation Methods
Never return non-const pointers or references to private data members. This breaks encapsulation.
Prefer declaring private functionality as static functions within the .cpp file rather than exposing them in public headers as private methods.
(Using the Pimpl idiom is even better though.)
2.3.2 Add Virtual Functions Judiciously
As a general rule of thumb, if your API does not call a particular method internally, then that method probably should not be virtual.
interfaces should be non-virtual and they should use the Template Method design pattern where appropriate.
virtual function call is typically represented as an integer offset into the vtable for the class.
This is often referred to as the Non-Virtual Interface idiom NVI.
Add convenience APIs as separate modules or libraries that sit on top of your minimal core API.
2.4 EASY TO USE
2.4.3 Consistent
Instead, you should explicitly
design for this by manually identifying the common concepts across your classes and using the same conventions to represent these concepts in each class.
This is often referred to as static polymorphism.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15template <typename T>
class Coord2D
{
public:
Coord2D(T x, T y) : mX(x), mY(y) {};
T GetX() const { return mX; }
T GetY() const { return mY; }
void SetX(T x) { mX ¼ x; }
void SetY(T y) { mY ¼ y; }
void Add(T dx, T dy) { mX þ¼ dx; mY þ¼ dy; }
void Multiply(T dx, T dy) { mX *¼ dx; mY *¼ dy; }
private:
T mX;
T mY;
};
2.4.4 Orthogonal
- Reduce redundancy. Ensure that the same information is not represented in more than one way.
There should be a single authoritative source for each piece of knowledge. - Increase independence. Ensure that there is no overlapping of meaning in the concepts that are
exposed. Any overlapping concepts should be decomposed into their basal components.
2.4.5 Robust Resource Allocation
In general, if you have a function that returns a pointer that your clients should delete or if you
expect the client to need the pointer for longer than the life of your object,
then you should return it using a smart pointer, such as a boost::shared_ptr.
However, if ownership of the pointer will be retained by your object, then you can return a standard pointer.1
2
3
4// ownership of MyObject* is transferred to the caller
boost::shared_ptr<MyObject> GetObject() const;
// ownership of MyObject* is retained by the API
MyObject* GetObject() const;
Think of resource allocation and deallocation as object construction and destruction.
2.4.6 Platform Independent
Never put platform-specific #if or #ifdef statements into your public APIs. 写到实现中去。
It exposes implementation details and makes your API appear different on different platforms.
2.5 LOOSELY COUPLED
2.5.2 Reducing Class Coupling
Prefer using non-member non-friend functions instead of member functions to reduce coupling.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// myobject.h
class MyObject
{
public:
std::string GetName() const;
. . .
protected:
. . .
private:
. . .
};
// myobjecthelper.h
namespace MyObjectHelper
{
void PrintName(const MyObject &obj);
};
2.5.4 Manager Classes
Manager classes can reduce coupling by encapsulating several lower-level classes.
2.5.5 Callbacks, Observers, and Notifications
1 | • Reentrancy. When writing an API that calls out to unknown user code, you have to consider that |
Patterns
3.1.1 Using Pimpl
When using the pimpl idiom use a private nested implementation class.
Only use a public nested Impl class (or a public non-nested class) if other classes or free functions in the .cpp must access Impl members.
3.1.2 Copy Semantics
Make your class uncopyable. OR Explicitly define the copy semantics. 避免对象被浅拷贝导致两次析构同一对象导致crash.
3.1.5 Disadvantages of Pimpl
3.2 SINGLETON
3.2.1 Implementing Singletons in C++
The relative order of initialization of non-local static objects in different translation units is undefined (Meyers, 2005).
This means that it would be dangerous to initialize our singleton using a non-local static variable.
A non-local object is one that is declared outside of a function.
Static objects include global objects and objects declared as static inside of a class, function, or a file scope.1
2
3
4
5Singleton &Singleton::GetInstance()
{
static Singleton instance;
return instance;
}
3.2.2 Making Singletons Thread Safe
Creating a thread-safe Singleton in C++ is hard. Consider initializing it with a static constructor or an API initialization function.
3.2.3 Singleton versus Dependency Injection
设置成员指针通过传递相关对象进行绑定。通过dependency container建立关系。
3.2.4 Singleton versus Monostate
Consider using Monostate instead of Singleton if you don’t need lazy initialization of global data or if you want the singular nature of the class to be transparent.
3.2.5 Singleton versus Session State
There are several alternatives to the Singleton pattern, including dependency injection, the Monostate pattern, and use of a session context.
3.3 FACTORY METHODS
Constructors in C++ have several limitations, such as the following.
- No return result.
- Constrained naming.
- Statically bound creation. When constructing an object, you must specify the name of a concrete
class that is known at compile time, for example, you might write: Foo* f = new Foo(),
where Foo is a specific type that must be known by the compiler. There is no concept of dynamic
binding at run time for constructors in C++. - No virtual constructors.
3.3.1 Abstract Base Classes
Should always declare the destructor of an abstract base class to be virtual.