new post: header files are cool, actually
This commit is contained in:
parent
122f611d6d
commit
d8d3ebdd38
2 changed files with 178 additions and 4 deletions
167
content/h.dj
Normal file
167
content/h.dj
Normal file
|
@ -0,0 +1,167 @@
|
|||
title = "header files are cool, actually"
|
||||
|
||||
+++
|
||||
|
||||
You know what.
|
||||
I actually kinda like header files.
|
||||
|
||||
Yeah, _those_ things.
|
||||
From C and C++.
|
||||
|
||||
I've heard lots of opinions from fellow programmers, saying it's annoying to have to update your signatures in _two_ places.
|
||||
However, I believe a lot of the extra repetition comes from _the bad design of C++'s classes_, rather than the idea of header files itself.
|
||||
|
||||
I've mentioned this briefly before in [C++ without Classes][page:programming/cxx-without-classes], but a lot of redundant editing is born out of C++'s poor implementation of class encapsulation.
|
||||
In C++, your implementation details come out leaking into the header file.
|
||||
|
||||
```cpp
|
||||
// .h file
|
||||
|
||||
class World
|
||||
{
|
||||
public:
|
||||
Entity& entity(Entity_Id id);
|
||||
|
||||
void update(const Update_Context& cx);
|
||||
void draw(const Draw_Context& cx);
|
||||
|
||||
private:
|
||||
void draw_entities(const Draw_Context& cx);
|
||||
|
||||
Entity entities[256];
|
||||
// etc
|
||||
};
|
||||
|
||||
// .cpp file
|
||||
|
||||
Entity& World::entity(Entity_Id id) { /* ... */ }
|
||||
|
||||
void World::update(const Update_Context& cx) { /* ... */ }
|
||||
|
||||
void World::draw(const Draw_Context& cx) { /* ... */ }
|
||||
|
||||
void World::draw_entities(const Draw_Context& cx) { /* ... */ }
|
||||
```
|
||||
|
||||
Closing yourself in the bubble of C++ might make you think header files are just that.
|
||||
Annoying nuisances, artifacts of language design of the past.
|
||||
DRY killers.
|
||||
|
||||
Hacks of the dated C compilation model.
|
||||
|
||||
However, if you observe what header files (or their equivalents) look like in _other_ languages---you start seeing something different.\
|
||||
Consider the same example, but written in C.
|
||||
|
||||
```c
|
||||
typedef struct
|
||||
{
|
||||
Entity entities[256];
|
||||
// etc
|
||||
} World;
|
||||
|
||||
Entity* world_entity(World* w, Entity_Id id);
|
||||
|
||||
void world_update(World* w, const Update_Context* cx);
|
||||
void world_draw(const World* w, const Update_Context* cx);
|
||||
```
|
||||
|
||||
See the difference? `draw_entities` is nowhere to be seen.
|
||||
Meanwhile, in the corresponding implementation file:
|
||||
|
||||
```c
|
||||
Entity* world_entity(World* w, Entity_Id id) { /* ... */ }
|
||||
|
||||
void world_update(World* w, const Update_Context* cx) { /* ... */ }
|
||||
|
||||
static void draw_entities(const World* w, const Update_Context* cx) { /* ... */ }
|
||||
|
||||
void world_draw(const World* w, const Update_Context* cx) { /* ... */ }
|
||||
```
|
||||
|
||||
The `draw_entities` implementation detail---a _private function_---is still very much there!
|
||||
|
||||
In OCaml, you would do something similar.
|
||||
Here's `world.mli`:
|
||||
|
||||
```ocaml
|
||||
type t
|
||||
|
||||
val entity : t -> Entity.id -> Entity.t
|
||||
val update : t -> Game_loop.update_context -> unit
|
||||
val draw : t -> Game_loop.draw_context -> unit
|
||||
```
|
||||
|
||||
And here's `world.ml`:
|
||||
|
||||
```ocaml
|
||||
type data = {
|
||||
entities: Entity.t array;
|
||||
}
|
||||
type t = data ref
|
||||
|
||||
let entity w id = (* ... *)
|
||||
|
||||
let update w cx = (* ... *)
|
||||
|
||||
let draw_entities w cx = (* ... *)
|
||||
|
||||
let draw w cx = (* ... *)
|
||||
```
|
||||
|
||||
(Disclaimer: my knowledge of OCaml is pretty surface level. This is probably not valid syntax.)
|
||||
|
||||
Our implementation detail `draw_entities` appears again, but only in the module's interface file.
|
||||
|
||||
And that's how I view header files: as _public interface declarations_.
|
||||
They say, "this is the set of public procedures you can run on these types."
|
||||
|
||||
When viewed this way, header files become extremely useful for tracking changes in a module's public API, semantic versioning-wise.
|
||||
|
||||
But perhaps more importantly, because headers written with the "public interface" philosophy in mind are so laser-focused on exposing the... well, _public interface_, they become your module's _reference documentation._
|
||||
|
||||
My favourite example of this is [Dear ImGui](https://github.com/ocornut/imgui/blob/v1.92.1/imgui.h), which takes this idea to the extreme.
|
||||
Dear ImGui needs no documentation generator like Doxygen.
|
||||
It's all right there in the header file, and all you have to do is open it in your text editor and grep for whatever you need at the moment.
|
||||
|
||||
Here's an excerpt of `imgui.h` (reformatted to fit my blog's column limit better):
|
||||
|
||||
```c
|
||||
// Windows
|
||||
// - Begin() = push window to the stack and start appending to it.
|
||||
// End() = pop window from the stack.
|
||||
// - Passing 'bool* p_open != NULL' shows a window-closing widget in
|
||||
// the upper-right corner of the window, which clicking will set
|
||||
// the boolean to false when clicked.
|
||||
// - You may append multiple times to the same window during
|
||||
// the same frame by calling Begin()/End() pairs multiple times.
|
||||
// Some information such as 'flags' or 'p_open' will only be
|
||||
// considered by the first call to Begin().
|
||||
// - Begin() return false to indicate the window is collapsed or fully
|
||||
// clipped, so you may early out and omit submitting anything to
|
||||
// the window. Always call a matching End() for each Begin() call,
|
||||
// regardless of its return value!
|
||||
// [Important: due to legacy reason, Begin/End and BeginChild/EndChild
|
||||
// are inconsistent with all other functions such as BeginMenu/EndMenu,
|
||||
// BeginPopup/EndPopup, etc. where the EndXXX call should only be
|
||||
// called if the corresponding BeginXXX function returned true.
|
||||
// Begin and BeginChild are the only odd ones out.
|
||||
// Will be fixed in a future update.]
|
||||
// - Note that the bottom of window stack always contains a window
|
||||
// called "Debug".
|
||||
IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0);
|
||||
IMGUI_API void End();
|
||||
```
|
||||
|
||||
Compare this to grepping through an `.rs` file, which usually yields lots of unrelated results.
|
||||
I pretty much never grep through Rust files to look at APIs, while I do it all the time in C++.
|
||||
|
||||
Perhaps it's a sign that the complexity of JavaScript-heavy online HTML documentation viewers with mediocre search functionality is entirely self-inflicted.\
|
||||
(I'll give them though that rich text is pretty cool, and Hackage's global type signature search is _wicked_ cool. Nothing that a sufficiently advanced LSP server shouldn't be capable of doing, though.)
|
||||
|
||||
(...and also, fuck Doxygen. I hate using Doxygen so much. But I'll leave that for another time.)
|
||||
|
||||
---
|
||||
|
||||
Either way, that does it for The Ramblings I Will Link Whenever Someone Mentions Header Files Are The Bane Of Their Existence.
|
||||
|
||||
Thank you for reading.
|
|
@ -26,6 +26,13 @@ if you've been wondering what I've been up to, you've come to the right place.
|
|||
if you want to read any of the posts, follow the links.
|
||||
it's like that by design.
|
||||
|
||||
% tags = ["programming", "cxx"]
|
||||
id = "01K1Y3G5N1KGCN1E9B36QTYMSZ"
|
||||
- ### [header files are cool, actually][page:h.dj]
|
||||
|
||||
% id = "01K1Y3G5N1WTEYF5X4JJHGR5XN"
|
||||
- in which I ramble about how to write good header files.
|
||||
|
||||
% tags = ["meow", "shower"]
|
||||
id = "01K05F3E3DN1PY9ZWN98ZE5HVV"
|
||||
- ### [furry! ---w--- (version 2)][page:furry.dj]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue