Skip to content

Commit 243334d

Browse files
Update C++ style guide to allow C++11 features and remove internal links
1 parent 5ea2a16 commit 243334d

File tree

1 file changed

+375
-0
lines changed

1 file changed

+375
-0
lines changed

STYLE_GUIDE.md

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
# C++ API Guidelines
2+
3+
**WIP** - *please feel free to improve*
4+
5+
Intended for Firebase APIs, but also applicable to any C++ or Game APIs.
6+
7+
# Code Style
8+
9+
Please comply with the
10+
[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html)
11+
as much as possible. Refresh your memory of this document before you start :)
12+
13+
# C++ API Design
14+
15+
### Don't force any particular usage pattern upon the client.
16+
17+
C++ is a huge language, with a great variety of ways in which things can be
18+
done, compared to other languages. As a consequence, C++ projects can be very
19+
particular about what features of the language they use or don't use, how they
20+
represent their data, and structure their code.
21+
22+
An API that forces the use of a feature or structure the client doesn't use
23+
will be very unwelcome. A good API uses only the simplest common denominator
24+
data types and features, and will be useable by all. This can generally be done
25+
with minimal impact on your API’s simplicity, or at least should form the
26+
baseline API.
27+
28+
Examples of typical Do's and Don'ts:
29+
30+
* Don't force the use of a particular data structure to supply or receive data.
31+
Typical examples:
32+
* `std::vector<T>`: If the client doesn't have the data already in a
33+
`std::vector` (or only wants to use part of a vector), they are forced
34+
to copy/allocate a new one, and C++ programmers don't like unnecessary
35+
copies/allocations.
36+
Instead, your primary interface should always take a
37+
`(const T *, size_t)` instead. You can still supply an optional helper
38+
method that takes a `std::vector<T> &` and then calls the former if
39+
you anticipate it to be called very frequently.
40+
* `std::string`: Unlike Java, these things aren't pooled, they're mutable
41+
and copied. A common mistake is to take a `const std::string &` argument,
42+
which forces all callers that supply a `const char *` to go thru a
43+
strlen+malloc+copy that is possibly of no use to the callee. Prefer to
44+
take a `const char *` instead for things that are names/identifiers,
45+
especially if they possibly are compile-time constant. If you're
46+
unsetting a string property, prefer to pass nullptr rather than an
47+
empty string. (There are
48+
[COW implementations](https://en.wikipedia.org/wiki/Copy-on-write),
49+
but you can't rely on that).
50+
* `std::map<K,V>`: This is a costly data structure involving many
51+
allocations. If all you wanted is for the caller to supply a list of
52+
key/value pairs, take a `const char **` (yes, 2 stars!). Or
53+
`const SimpleStruct *` instead, which allows the user to create this data
54+
statically.
55+
* Per-product configuration should be accomplished using an options struct
56+
passed to the library's `firebase::<library>::Initialize` function. Default
57+
options should be provided by the options struct's default constructor. The
58+
`Initialize` function should be overloaded with a version that does not take
59+
the options struct (which is how the Google style guide prefers that we pass
60+
default parameters).
61+
62+
For example,
63+
64+
```c++
65+
struct LibraryOptions {
66+
LibraryOptions() : do_the_thing(false) {}
67+
68+
bool do_the_thing;
69+
};
70+
71+
InitResult Initialize(const App& app) {
72+
return Initialize(app, LibraryOptions());
73+
}
74+
InitResult Initialize(const App& app, const LibraryOptions& options);
75+
```
76+
77+
* Don't make your client responsible for data you allocate or take ownership
78+
of client data. Typical C++ APIs are written such that both the client
79+
and the library own their own memory, and they take full responsibility for
80+
managing it, regardless of what the other does. Any data exchanged is
81+
typically done through weak references and copies.
82+
An exception may be a file loading API where buffers exchanged may be really
83+
big. If you are going to pass ownership, make this super obvious in all
84+
comments and documentation (C++ programmers typically won't expect it),
85+
and document which function should be used to free the data (free, delete,
86+
or a custom one).
87+
Alternatively, a simple way to pass ownership of a large new buffer to the
88+
client is to ask the client to supply a std::string *, which you then
89+
resize(), and write directly into its owned memory. This somewhat violates
90+
the rule about use of std::string above, though.
91+
92+
* Don't use exceptions. This one is worth mentioning separately. Though
93+
exceptions are great in theory, in C++ hardly any libraries use them, and
94+
many code-bases disallow them entirely. They also require the use of RTTI
95+
which some environments turn off. Oh, yes, also don't use RTTI.
96+
97+
* Go easy on templates when possible. Yes, they make your code more general,
98+
but they also pull a lot of implementation detail into your API, lengthen
99+
compile times and bloat binaries. In C++ they are glorified macros, so they
100+
result in hard to understand errors, and can make correct use of your API
101+
harder to understand.
102+
103+
* Utilize C++11 features where appropriate. This project has adopted C++11,
104+
and features such as `std::unique_ptr`, `std::shared_ptr`, `std::make_unique`,
105+
and `std::move` are encouraged to improve code safety and readability.
106+
However, avoid features from C++14 or newer standards.
107+
108+
* Go easy on objectifying everything, and prefer value types. In languages
109+
like Java it is common to give each "concept" your API deals with its own
110+
class, such that methods on it have a nice home. In C++ this isn't always
111+
desirable, because objects need to be managed, stored and allocated, and you
112+
run into ownership/lifetime questions mentioned above. Instead:
113+
114+
* For simple data, prefer their management to happen in the parent class
115+
that owns them. Actions on them are methods in the parent. If at all
116+
possible, prefer not to refer to them by pointer/reference (which
117+
creates ownership and lifetime issues) but by index/id, or string
118+
if not performance sensitive (for example, when referring to file
119+
resources, since the cost of loading a file dwarfs the cost of a string
120+
lookup).
121+
122+
* If you must create objects, and objects are not heavyweight (only scalars
123+
and non-owned pointers), make use of these objects by value (return by
124+
value, receive by const reference). This makes ownership and lifetime
125+
management trivial and efficient.
126+
127+
* If at all possible, don't depend on external libraries. C++ compilation,
128+
linking, dependency management, testing (especially cross platform) are
129+
generally way harder than any other language. Every dependency is a potential
130+
source of build complexity, conflicts, efficiency issues, and in general more
131+
dependencies means less adoption.
132+
133+
* Don't pull in large libraries (e.g. BOOST) just for your convenience,
134+
especially if their use is exposed in headers.
135+
136+
* Only use external libraries that have hard to replicate essential
137+
functionality (e.g. compression, serialization, image loading, networking
138+
etc.). Make sure to only access them in implementation files.
139+
140+
* If possible, make a dependency optional, e.g. if what your API does
141+
benefits from compression, make the client responsible for doing so,
142+
or add an interface for it. Add sample glue code or an optional API for
143+
working with the external library that is by default off in the build
144+
files, and can be switched on if desired.
145+
146+
* Take cross-platform-ness seriously: design the API to work on ALL platforms
147+
even if you don't intend to supply implementations for all. Hide platform
148+
issues in the implementation. Don't ever include platform specific headers in
149+
your own headers. Have graceful fallback for platforms you don't support, such
150+
that some level of building / testing can happen anywhere.
151+
152+
* If your API is meant to be VERY widespread in use, VERY general, and very
153+
simple (e.g. a compression API), consider making at least the API (if not all
154+
of it) in C, as you'll reach an even wider audience. C has a more consistent
155+
ABI and is easier to access from a variety of systems / languages. This is
156+
especially useful if the library implementation is intended to be provided in
157+
binary.
158+
159+
* Be careful not to to use idioms from other languages that may be foreign to
160+
C++.
161+
162+
* An example of this is a "Builder" API (common in Java). Prefer to use
163+
regular constructors, with additional set_ methods for less common
164+
parameters if the number of them gets overwhelming.
165+
166+
* Do not expose your own UI to the user as part of an API. Give the developer
167+
the data to work with, and let them handle displaying it to the user in the
168+
way they see fit.
169+
170+
* Rare exceptions can be made to this rule on a case-by-case basis. For
171+
example, authentication libraries may need to display a sign-in UI for the
172+
user to enter their credentials. Your API may work with data owned by
173+
Google or by the user (e.g. the user's contacts) that we don't want to
174+
expose to the app; in those cases, it is appropriate to expose a UI (but
175+
to limit the scope of the UI to the minimum necessary).
176+
177+
* In these types of exceptional cases, the UI should be in an isolated
178+
component, separate from the rest of the API. We do allow UIs to be
179+
exposed to the user UI-specific libraries, e.g. FirebaseUI, which should
180+
be open-source so developers can apply any customizations they need.
181+
182+
# Game API Design
183+
184+
### Performance matters
185+
186+
Most of this is already encoded in C++ API design above, but it bears repeating:
187+
C++ game programmers can be more fanatic about performance than you expect.
188+
189+
It is easy to add a layer of usability on top of fast code, it is very hard to
190+
impossible to "add performance" to an API that has performance issues baked into
191+
its design.
192+
193+
### Don't rely on state persisting for longer than one frame.
194+
195+
Games have an interesting program structure very unlike apps or web pages: they
196+
do all processing (and rendering) of almost all functionality of the game within
197+
a *frame* (usually 1/60th of a second), and then start anew for the next frame.
198+
199+
It is common for all or part of the state of a game to be wiped out from one
200+
frame to the next (e.g when going into the menu, loading a new level, starting a
201+
cut-scene..).
202+
203+
The consequence of this is that the state kept between frames is the only record
204+
of what is currently going on, and that managing this state is a source of
205+
complexity, especially when part of it is reflected in external code:
206+
207+
* Prefer API design that is stateless, or if it is stateful, is so only within a
208+
frame (i.e. between the start of the frame and the start of the next one).
209+
This really simplifies the client's use of your API: they can't forget to
210+
"reset" your API's state whenever they change state themselves.
211+
212+
* Prefer not to use cross-frame callbacks at all (non-escaping callbacks are
213+
fine). Callbacks can be problematic in other contexts, but they're even more
214+
problematic in games. Since they will execute at a future time, there's no
215+
guarantee that the state that was there when the callback started will still
216+
be there. There's no easy way to robustly "clear" pending callbacks that don't
217+
make sense anymore when state changes. Instead, make your API based on
218+
*polling*.
219+
Yes, everyone learned in school that polling is bad because it uses CPU, but
220+
that's what games are based on anyway: they check a LOT of things every frame
221+
(and only at 60hz, which is very friendly compared to busy-wait polling). If
222+
your API can be in various states of a state machine (e.g. a networking based
223+
API), make sure the client can poll the state you're in. This can then easily
224+
be translated to user feedback.
225+
If you have to use asynchronous callbacks, see the section on async operations
226+
below.
227+
228+
* Be robust to the client needing to change state. If work done in your API
229+
involves multiple steps, and the client never gets to the end of those steps
230+
before starting a new sequence, don't be "stuck", but deal with this
231+
gracefully. If the game's state got reset, it will have no record of what it
232+
was doing before. Try to not make the client responsible for knowing what it
233+
was doing.
234+
235+
* Interaction with threading:
236+
237+
* If you are going to use threading at all, make sure the use of that is
238+
internal to the library, and any issues of thread-safety don't leak into
239+
the client. Allow what appears to be synchronous access to a possibly
240+
asynchronous implementation. If the asynchronous nature will be
241+
externally visible, see the section on async operations below.
242+
243+
* Games are typically hard to thread (since it’s hard to combine with its
244+
per-frame nature), so the client typically should have full control over
245+
it: it is often better to make a fully synchronous single-threaded library
246+
and leave threading it to the client. Do not try to make your API itself
247+
thread-safe, as your API is unlikely the threading boundary (if your
248+
client is threaded, use of your library is typically isolated to one
249+
thread, and they do not want to pay for synchronization cost they don't
250+
use).
251+
252+
* When you do spin up threads to reduce a workload, it is often a good idea
253+
to do that once per frame, as avoid the above mentioned state based
254+
problems, and while starting threads isn't cheap, you may find it not a
255+
problem to do 60x per second. Alternatively you can pool them, and make
256+
sure you have an explicit way to wait for their idleness at the end of a
257+
frame.
258+
259+
* Games typically use their own memory allocator (for efficiency, but also to be
260+
able to control and budget usage on memory constrained systems). For this
261+
reason, most game APIs tend to provide allocation hooks that will be used for
262+
all internal allocation. This is even more important if you wish to be able to
263+
transfer ownership of memory.
264+
265+
* Generally prefer solutions that are low on total memory usage. Games are
266+
always constrained on memory, and having your game be killed by the OS because
267+
the library you use has decided it is efficient to cache everything is
268+
problematic.
269+
270+
* Prefer to recompute values when possible.
271+
272+
* When you do cache, give the client control over total memory used for
273+
this purpose.
274+
275+
* Your memory usage should be predictable and ideally have no peaks.
276+
277+
# Async Operations
278+
279+
### Application Initiated Async Operations
280+
281+
* Use the Future / State Pattern.
282+
* Add a `*Result()` method for each async operation method to allow the caller
283+
to poll and not save state.
284+
285+
e.g.
286+
287+
```c++
288+
// Start async operation.
289+
Future<SignInWithCrendentialResult> SignInWithCredential(...);
290+
// Get the result of the pending / last async operation for the method.
291+
Future<SignInWithCrendentialResult> GetSignInWithCredentialResult();
292+
293+
Usage examples:
294+
// call and register callback
295+
auto& result = SignInWithCredential();
296+
result.set_callback([](result) { if (result == kComplete) { do_something_neat(); wake_up(); } });
297+
// wait
298+
299+
// call and poll #1 (saving result)
300+
auto& result = SignInWithCredential();
301+
while (result.value() != kComplete) {
302+
// wait
303+
}
304+
305+
// call and poll #2 (result stored in API)
306+
SignInWithCredential();
307+
while (GetSignInWithCredentialResult().value() != kComplete) {
308+
}
309+
```
310+
311+
### API Initiated Async Event Handling
312+
313+
* Follow the
314+
[listener / observer pattern](https://en.wikipedia.org/wiki/Observer_pattern)
315+
for API initiated (i.e where the caller doesn't initiate the event)
316+
async events.
317+
* Provide a queued interface to allow users to poll for events.
318+
319+
e.g.
320+
321+
```c++
322+
class GcmListener {
323+
public:
324+
virtual void OnDeletedMessage() {}
325+
virtual void OnMessageReceived(const MessageReceivedData* data) {}
326+
};
327+
328+
class GcmListenerQueue : private GcmListener {
329+
public:
330+
enum EventType {
331+
kEventTypeMessageDeleted,
332+
kEventTypeMessageReceived,
333+
};
334+
335+
struct Event {
336+
EventType type;
337+
MessageReceivedData data; // Set when type == kEventTypeMessageReceived
338+
};
339+
340+
// Returns true when an event is retrieved from the queue.
341+
bool PollEvent(Event *event);
342+
};
343+
344+
// Wait for callbacks
345+
class MyListener : public GcmListener {
346+
public:
347+
virtual void OnDeletedMessage() { /* do stuff */ }
348+
virtual void OnMessageReceived() { /* display message */ }
349+
};
350+
MyListener listener;
351+
gcm::Initialize(app, &listener);
352+
353+
// Poll
354+
GcmListenerQueue queued_listener;
355+
gcm::Initialize(app, &queued_listener);
356+
GcmListenerQueue::Event event;
357+
while (queued_listener(&event)) {
358+
switch (event.type) {
359+
case kEventTypeMessageDeleted:
360+
// do stuff
361+
break;
362+
case kEventTypeMessageReceived:
363+
// display event.data
364+
break;
365+
}
366+
}
367+
```
368+
369+
# Data Types
370+
371+
* Time: [Milliseconds since the epoch](https://en.wikipedia.org/wiki/Unix_time)
372+
373+
# More on C++ and games
374+
375+
* Performance Oriented C++ for Game Developers (very incomplete).

0 commit comments

Comments
 (0)