> Always prefix your names to avoid name collisions
Solved by C++ namespaces.
> Use header guards instead of #pragma once.
#pragma once is supported by all mayor compilers, header guards can introduce bugs, also #pragma once builds measurably faster: https://github.com/electronicarts/EASTL/blob/3.15.00/include...
> Expose constants to the user using constexpr variables.
Modern C++ has strongly-typed scoped enums for such constants.
Regarding prefixes, I advice that you start by writing the library in C and then wrap it in C++ for a variety of reasons that you might want to consider. In C++ you should indeed always use namespace.
Header guards have the advantage over pragma once that they are standard and you can also use them to check if a library is included. I might remove that since maybe it's not that important and people might different views.
Regarding constants, I was referring to things such as numeric constants for which you would constexpr in C++. Maybe I can be more explicit there. Thanks for the feedback.
It’s very hard to write correct C code which does IO and supports multithreading. Take a look at Microsoft’s implementation of fprintf, copy-pasted from Windows 10 SDK: https://gist.github.com/Const-me/f1bb320969adde6c79694265ea6... They use RAII to set & revert the locale, and to lock RAM buffer to avoid corruption by another threads. They use C++ lambda for exception handling. They even use C++ template to avoid code duplication between printf and wprintf.
But these C++ shenanigans are not exposed to user, user calls their `printf` (possibly in a code built by C compiler) and it just works.
I've never understood what's the practical difference between NS::foo() and NS_foo() with regards to preventing name collisions. Can someone enlighten me?
I know some disadvantages of the namespacing variant, though. There are now multiple names for the items defined in the namespace: the qualified one and the unqualified one. The latter is often not unique in practice, since the programmer relies on the qualified name for uniqueness. In effect, making simple text searches for identifiers is very unreliable. Note I do use IDEs, but I also code in vim, and I need to do simple text searches even when working in Visual Studio.
Additionally, there's no guarantee that there are no spaces around the scoper. I believe "NS :: foo" is just as valid, which makes me uneasy with regards to text search, as well.
Another issue I have:
$ cat test.cpp
namespace NS { int foo() { return 0; } };
$ g++ -c -o test.o test.cpp
$ nm test.o
0000000000000000 T _ZN2NS3fooEv # I hate my life
> Modern C++ has strongly-typed scoped enums for such constants.I haven't found those working for me. Apart from the namespacing issue described above, I have issues with explicit enum types. One issue is that I often need to put sentinel / "missing" values (typically the value is -1) where an enum value is expected. Even more often, I want to iterate over the values of an enum. C++'s enum "type safety" makes working like this really unergonomic.
The way I go about this is I don't even use names for my enum types, and I fully qualify the enumeration values.
enum {
FPGAPARAM_BLA,
FPGAPARAM_BLUB,
FPGAPARAM_FOO,
NUM_FPGAPARAM_KINDS
};
struct ASDF {
int fpgaparamKind; // obvious what kind of values are expected here...
};
for (int i = 0; i < NUM_FPGAPARAM_KINDS; i++) {
struct ASDF asdf;
asdf.fpgaParamKind = i;
do_asdf(&asdf);
}
In programming, the slightest mistakes, like putting a "-" instead of a "+", result in program bugs. These mistakes are much more likely to be made (and much harder to spot) than mistakes involving enum values from the wrong set. I won't let programming ergonomics be ruined in the name of "type safety".They both do the job. There’re 2 practical differences.
1. You can write `using namespace` inside functions or the whole .cpp files. This often makes the consuming code more readable.
2. Sometimes you want to replace implementations. With prefixes it gonna be massive changes likely to introduce new bugs. With namespaces, replace `using std::vector` with `using eastl::vector` and you’re done.
> Even more often, I want to iterate over the values of an enum.
I only need to do that rarely. When I do, I cast types like this:
enum struct eParamKind : uint8_t
{
Bla, Blub, Foo, valuesCount
};
for( uint8_t i = 0; i < (uint8_t)eParamKind::valuesCount; i++ )
{
const eParamKind pk = (eParamKind)i;
// Whatever
}
> I won't let programming ergonomics be ruined in the name of "type safety".I disagree on ergonomics. VS makes much easier to consume API with strongly typed enums: ePar<Ctrl+Space>::f<Enter>, to type eParamKind::Foo It’s similar with namespaces versus prefixes BTW, IDE will first auto-complete the namespace, then only list members of that namespace once you type the `::`
Update: another C++ feature relevant for game development is overloaded operators. Games often do non-trivial amount of math on small vectors, matrices and quaternions. Overloaded operators make sense for them.
C++ defines don’t respect namespaces so you are pretty much screwed in anycase.
BTW, you can disable these windows.h macros by defining NOMINMAX. I usually do, because I prefer min/max from <algorithm>; in some edge cases std::min / std::max can be twice as fast because compiler guarantees to compute arguments exactly once.
When it comes to exceptions and rtti the sad reality is that about 50% of the C++ community doesn't use (for one reason or another), so if you do use them in a library it can have an impact.
> pleasant
Nice joke. Hour-long compilations, type-unsafe templates with unreadable errors, and circular references are considered pleasant now?
- templates are type safe. it is just the error messages are not helpful a lot of times. C++20's concepts should help
- circular references. that's a thing in C as well. as soon as you need interact with your libraries user to allocate/deallocate resources.
C++ has a large surface to interact with. Yes you can shot yourself in your foot. However, the more you know you can use the features to your advantage instead of shotting yourself in the foot. At the point you gain more than you lose.
If your project takes hours to compile them you only have yourself to blame.
> type-unsafe templates
There is no such thing.
> with unreadable errors,
That's an implementation issue, not a language issue.
> and circular references
If you write buggy code then you only have yourself to blame.
Point in case for lack of portability, UE4 doesn't support exceptions unless you do a custom build.
If, on the other hand, you’re in the habit of shotgun debugging, where you make repeated changes to the problem code until it appears to work, you’re quite likely to leave behind various problems that will be hard to figure out.
Often, experience is the best teacher. If you’re not exposing your program to malicious users (aka. the public at large), the most serious issue you’re likely to run into is either a program crash or data corruption— nothing that will really harm your computer, but may cause you grief as you try to figure it out. In that process, though, you’ll learn an awful lot about how everything works. So, go write some programs for your own use and see how they crash and burn so that the next thing you make is more stable. Eventually you’ll start to intuitively spot trouble before it actually happens.
Writing Secure Code
https://www.amazon.com/Writing-Secure-Second-Developer-Pract...
Secure Programming Cookbook for C and C++
http://shop.oreilly.com/product/9780596003944.do
SEI CERT C Coding Standard
If you are interested in learning more about writing complex software in C consider checking out HandmadeHero.
Sadly there need to be more good resources on learning how to write good C and low level software. I am hoping my article can be a starting point for people who wish to learn about library design for example.
While UB quirks exist, they are WAY off the beaten path and it takes an effort to run into them. Doubly so if you are just starting with the language.
Just treat C as a thin convenient layer over the hardware that expects you to think and act responsibly in exchange for this nearly raw access.
Not to mention compilers make fun-times out of this by sometimes zeroing memory in debug and then not doing so for release builds (Hi MSVC!)..
The "thin layer over hardware" idea is a thing of the past as soon as optimizations come into the picture, and even then.
That's important here. Some of my projects have something around 10 cross-compilation toolchains for various platforms. If a library is written in standard C99, adding it to the project is a breeze. Otherwise...
Functions and opaque handles are often way simpler for the user.
Anyone providing a library written in pure C, better be serious about security and prove that they have taken all the required steps to handle memory corruption and UB exploits.
We really need more liability on software development.
The lowest common denominator for build instructions that works everywhere is literally "set your include paths like so and compile these sources".
> The lowest common denominator for build instructions that works everywhere is literally "set your include paths like so and compile these sources".
Yes, but that's quite inconvenient and error-prone ; a typo in a -D flag is very likely to not compile what you wanted.
Make is not a multi-megabyte program with a frack tonne of dependencies. Having your users to install it in order to build your project is far from being completely outrageous (unless for coughmicrosoftcough cults that consider the command-line is evil).
In languages that do have "real" macros, you don't see the same problem. (On the other hand, in most of them, you see a need to call out to a C compiler to generate FFI code, which is sometimes just as bad when you're using a PL with a managed runtime and your build-env didn't otherwise have any need for a C toolchain.)
C is easier in this regard just because the language evolves so slowly that even though the build and library systems are bad, theyre a known bad that people can work around. No such luck with C++, where its bad and ever changing.
I usually recommend to write library-level code in the "subset of C99 which compiles both in C and C++ mode on GCC, clang and MSVC". This is basically a version of C that's somewhere C89 and C99 (basically a "C95").
Another quite valid option is to define a pure C-API first, but implement the "inner library code" in a simple C++ (just be careful with "modern C++", since this usually results in increased compilation time and binary size).
enum {
FPGAPARAM_FOO,
FPGAPARAM_BAR,
FPGAPARAM_BAZ,
NUM_FPGAPARAM_KINDS
};
struct FpgaparamInfo {
const char *name;
int address;
int args;
};
static struct FpgaparamInfo fpgaParamInfo[NUM_FPGAPARAM_KINDS] = {
[FPGAPARAM_FOO] = { "FOO", 0x1337, -1 },
[FPGAPARAM_BAR] = { "BAR", 0x666, -1 },
[FPGAPARAM_BAZ] = { "BAZ", 0x42, -1 },
};
The FpgaParamInfo is basically a mapping from a FPGAPARAM_??? value to additional information. We can make as many of these mappings as we want, and can define them where we want, which means it's all nicely modular. That's not really possible when modelling in a OOP fashion.I really like this style of programming since it's data first and it minimizes the amount of actual code. Designated initializers are important because the order in which the items in "fpgaParamInfo" are given doesn't matter. Without designated initializers, programming in this style would probably lead to many hard to find bugs when the enum is changed and not all associated data items are updated.
This could be very well flipped over. If I have the source available, I'd much rather deal with C++ bindings.
C++ ABIs are rather complex and much harder to interface with. The existence of concepts like virtual functions and exceptions significantly complicates the implementation of foreign language interfaces.
https://wiki.osdev.org/System_V_ABI https://itanium-cxx-abi.github.io/cxx-abi/abi.html
It does require some discipline, but C++ has many portable constructs that make life significantly easier when writing business logic.
I don't have the exact numbers, but this seems debatable.
Six of the eight most popular programming languages (from https://www.tiobe.com/tiobe-index/) have C-like syntax. I think most people would agree.
Edit: I think that in this context, Reelin's comment is more appropriate: This is not about understanding, but about interoperability.
Why not write it in Rust, and provide C99 bindings? Using stock C for the actual library doesn't sound like a good idea to me, when today there are better alternatives.
And for sure avoid using Metal lock-in directly. Use Vulkan, which Apple should have supported from the beginning. For lock-in targets, there are translation options from Vulkan.
Also if people don't have a Rust toolchain setup, compiling the library and using it from source would be hard. In some cases integrating Rust in their toolchain could be hard.
Regarding metal and vulkan I will edit the article to mention Vulkan there too alongside metal. Thanks for your feedback.
As for toolchain, Rust can be set up basically anywhere llvm can, which is quite a lot. There are rare cases where llvm wasn't ported yet to, but I don't think they are enough to make C a compelling option in general, and I don't think any of them are gaming related.
If you are in such case - then sure, but otherwise, I'd still prefer Rust.
For usage from C code, there's cimgui, which is an automatically generated C-API wrapper:
Might be worth touching on error callbacks/logging as an error handling strategy.
Sometimes an error is not recoverable in the sense that the calling code can't really do anything about it, but the library should attempt to make progress anyway instead of halting the entire program.
By allowing users to specify an error callback, this means they can log errors, capture stack traces, assert, or whatever.
This isn't that helpful for smaller libraries with smaller-scoped processes, but if it's something like a renderer or interactive audio lib, those often just need to be given a bunch of frame time to do work with the complex input you've prepped and fed to it, and trying to propagate error codes up out of that simulation step would both contort the inner code and not be as helpful as an error callback.
With an error callback you can assert, set breakpoints, or do whatever. But more importantly, by default you can have it just log so when you inevitably in a bug it doesn't prevent everyone else from getting work done while it keeps asserting until you fix your shit.
This will be a less common need than the other standard error reporting mechanisms, but is important to get right if your library has these complex internal preconditions that you want to make visible to the client when violated.
I've used some libraries that do some of these things, but I can't think of any that does all of them.