digitalmars.D - Sane API design (AKA C's #ifdef hell)
- H. S. Teoh (111/111) Apr 16 In D circles we often talk about API design, and D lends itself quite
- matheus (8/9) Apr 16 Interesting, another solution would be Separate Files + Build
- H. S. Teoh (74/84) Apr 16 The nice thing about using separate files is that you avoid #ifdef /
In D circles we often talk about API design, and D lends itself quite
well to proper API design. But you still have to apply it correctly.
Just today, while investigating a bug at a work project, I discovered
this nasty piece of work:
```c
// file1.c:
int getMaxMacGuffin() { ... }
int getMaxMacGuffinNewPlatform() {
...
int n = getMaxMacGuffin();
... // do something else with n
return n;
}
// file2.c:
int myFunc1() {
...
#ifdef BUILDING_FOR_NEW_PLATFORM
int max = getMaxMacGuffinNewPlatform();
#else
int max = getMaxMacGuffin();
#endif
...
}
// file3.c:
int myFunc2() {
...
#ifdef BUILDING_FOR_NEW_PLATFORM
int max = getMaxMacGuffinNewPlatform();
#else
int max = getMaxMacGuffin();
#endif
...
}
// ... and so on, for like 4-5 different files in different
// unrelated functions.
```
The bug was that under some circumstances the max number of MacGuffins
displayed doesn't match the actual max number. Raise your hands, anyone
who guessed what the problem is.
...
Yeah you guessed it, in some obscure corner somewhere, somebody called
getMaxMacGuffin() without the accompanying #ifdef hell, thus obtaining
the wrong value.
The elephant in the room is, WHY ARE THERE TWO DIFFERENT FUNCTIONS FOR
COMPUTING THE MAX NUMBER OF MACGUFFINS?!?!?!
A bit of digging reveals that getMaxMacGuffin was the original function,
and getMaxMacGuffinNewPlatform was added later. Apparently, whoever
implemented the latter went a little overboard with the philosophy of
"don't touch the original code, just code around it". The chosen
solution, however, is totally insane. Why would you want to sprinkle your
#ifdef hell all over the project everywhere getMaxMacGuffin is called?!
The much saner solution is:
```c
// renamed from the original getMaxMacGuffin()
static int _getMaxMacGuffin() { ... }
static int getMaxMacGuffinNewPlatform() {
...
int n = _getMaxMacGuffin();
... // do something else with n
return n;
}
// New version of the function, containing the ONLY actually necessary
// #ifdef:
int getMaxMacGuffin() {
#ifdef BUILDING_FOR_NEW_PLATFORM
return getMaxMacGuffinNewPlatform();
#else
return _getMaxMacGuffin();
#endif
}
// Then everywhere else, just always call getMaxMacGuffin and delete all
// the rest of the #ifdef hell
```
After this refactoring, I didn't even need to know where the bug was; it
automatically fixed itself because now getMaxMacGuffin will always do
the right thing.
//
Thankfully, in D we don't have #ifdef hell...
... or do we? Although the example I encountered today was in C, one
can easily conceive of the D equivalent:
```d
// file1.c:
int getMaxMacGuffin() { ... }
int getMaxMacGuffinNewPlatform() {
...
auto n = getMaxMacGuffin();
... // do something else with n
return n;
}
// file1.c:
int myFunc1() {
...
version(NewPlatform)
auto max = getMaxMacGuffinNewPlatform();
else
auto max = getMaxMacGuffin();
...
}
... // and so on
```
So we don't have #ifdef hell in D, but we *do* have version hell here,
and we'd end up in exactly the same situation as in the C version. The
solution, of course, is also the same: move the version block inside the
function, and if needed move the original function body into a helper
function.
Thankfully, among D circles we generally hate boilerplate and like to
use metaprogramming to reduce or eliminate it. Still, used wrongly, even
D's better constructs can still lead to versioning hell, like above.
T
--
Frank disagreement binds closer than feigned agreement.
Apr 16
On Thursday, 16 April 2026 at 18:52:23 UTC, H. S. Teoh wrote:...Interesting, another solution would be Separate Files + Build System, but in this case we are moving the problem from source to building/compiling time. I recently asked a question about a similar problem to this one in Learn Forum, didn't get much traction. I think it's hard to define elegant way to do this. Matheus.
Apr 16
On Thu, Apr 16, 2026 at 09:13:30PM +0000, matheus via Digitalmars-d wrote:On Thursday, 16 April 2026 at 18:52:23 UTC, H. S. Teoh wrote:The nice thing about using separate files is that you avoid #ifdef / version hell altogether. The problem with this is that it makes the code hard to understand. When your source tree has multiple versions of the same file, how do you know which is the one that contains the bug you're trying to track down? Which file is the actual target of a particular function call? You have to understand the build system, and we all know that build systems tend to grow hairs over time, especially in large projects where the ability to quickly locate the target of a function call is especially important. If there's only 2-3 files, this isn't a problem. When there are 15 different files, it's a BIG problem: every function call going through any function in these files can end up in any one of the 15, and which exactly depends on fiddly details in the build system that you may not be privy to. (And lest you think I'm exaggerating: the project I work on targets *72* different platforms... we have 72 different files that specify how each platform is to be configured. Thankfully, there isn't any code in these files, and the #ifdef hell in files that *do* have code is mostly confined to feature-based #ifdef identifiers rather than individual platforms. But there are exceptions, and you do NOT want to have to deal with those...) Having code appear / disappear based on the build system is IMO a code smell, even though there are cases where this is unavoidable. Basically you're introducing an alternation that's invisible in the code itself, that can only be resolved by looking at build details. And build details can be arbitrarily complex. Or worse, if you use build systems with the modern philosophy that things are inferred for you automatically, you'll have to dive into the rabbit hole of figuring out what exactly was inferred, and why -- just to resolve the target of a function call. Not ideal, to put it mildly. (The same argument can be made for code that overuse dynamic binding, like heavily-OO code. Or in C, code that overuse function pointers. The former is a little more manageable because there are tools for working with OO; the latter can be really evil because the func ptrs are ad hoc rather than following, say, a class hierarchy, so they can literally point to *anything*. Good like figuring out where your function calls are going when there's a bug.)...Interesting, another solution would be Separate Files + Build System, but in this case we are moving the problem from source to building/compiling time.I recently asked a question about a similar problem to this one in Learn Forum, didn't get much traction. I think it's hard to define elegant way to do this.[...] In general, targeting individual features in #ifdef / version makes more sense than targeting more abstract entities like individual platforms (which are essentially a collection of features). E.g., like this: ```d version(linux) { version = featureA; version = featureB; version = featureC; } version(Windows) { version = featureA; version = featureD; version = featureF; } ... version(featureA) { int myFunc() { ... } } version(featureB) { int myFunc2() { ... } } ... ``` This is because sometimes platform definitions change (e.g., you decide to support feature X on platform Y that you didn't support before, even though it could be done on that platform -- maybe you didn't have the resources to do it before. Or you decide to remove a feature from platform Y because it was causing too many problems). If your #ifdefs / version blocks target platforms, you'll have to go through every occurrence of version(platformY) and review whether something needs to be changed. If you target individual features, all you have to do is to add/remove that feature from platformY's version block, and no other code will need to change. Much less messy. T -- I got an email explaining how to read maps backwards. It was spam.
Apr 16








"H. S. Teoh" <hsteoh qfbox.info>