ysaie

31 / ⚧ / code, music, art, games

──────────────────────────────
🌸 many-shaped creature
✨ too many projects
🚀 cannot be stopped
🌙 stayed up too late
:eggbug: eggbug enjoyer
──────────────────────────────
header image: chapter 8 complete from celeste
avatar: made using this character builder


📩 email
contact@echowritescode.dev

so, i used C++ namespaces in Crucible pretty heavily...


a typical way to address a very common symbol looks like this: crucible::core::memory::HeapBuffer. logically this makes sense to me:

  • crucible is the overarching project
  • core is the name of the linkage unit (DLL, static library, or executable)
  • memory is the specific header file it comes from
  • HeapBuffer is the symbol

but then i end up with ridiculous function declarations like this (contrived, but very representative) example:

template<typename T> requires crucible::core::convert::Convertible<crucible::core::string::String, T>
[[nodiscard]] constexpr auto join(crucible::core::memory::HeapBuffer<T> const &objects, crucible::core::string::String const &delimiter) noexcept -> crucible::core::string::String;

i also think [[nodiscard]], constexpr where possible, and noexcept where possible are all sensible defaults and should be added to every function declaration unless there's some reason they can't be. i think there's nothing to be done about these, but they contribute to the unnecessary line length and make it even more important to slim down the symbol names.

this problem is really getting to me, and i can only think of a few solutions, none of which really appeal to me:

  • reduce the number of namespaces: practical, but it discards information that i think is valuable
  • start using namespace short = some::nested::long::ns;, using namespace some::nested::long::ns;, or using some::nested::long::ns::ThingIWant; which feels brittle (especially in header files)
  • same as the last one, but put it into a namespace internal { ... } or similar. i think this is my least-disliked solution, but it's still kind of wonky, and you still end up with internal::Thing instead of just the ideal Thing.

in other languages i think the namespace issue would be resolved by the import system, where you could both declare the requirement of some::nested::namespace::symbol and alias it to symbol in the same declaration, only for the current file (since in basically every other language the source for a single module is inside exactly one file), without leaking it to unexpected places.

what do you folks do to de-clutter your symbol references?


You must log in to comment.

in reply to @ysaie's post:

You could definitely, at the very least, keep code generally in the crucible namespace to declutter things a lil bit. But a lot of that is just language quirks. Part of why I like Rust is that you can import a namespace, or a specific object from the namespace, so you can choose your specificity very situationally.

I'm currently working on an engine for a client where they do not give one singular fuck about namespaces, and it's actually kind of freeing. Basically everything is in the global namespace, and they weirdly don't have too many naming conflicts because of it. Don't overcomplicate it; try reducing your namespaces to just the essential crucible one.

i suspect that C++ is like that as an over correction for the nightmare of C's global namespace woes

in other languages, i might be tempted to do something like a typedef or alias here, i think that is related to your second option, so i am curious why it might be fragile in C++?

C++ has very weird name lookup rules - for example, if i brought the type Foo and the function do_something(Foo const &foo) into scope, then called a generic function that happened to call do_something(T const &), meaning to call a totally different function that just happens to have the same name, it's not entirely clear which version of do_something() would actually be called.

it is a defined behavior, but i would have to test it out on godbolt every time before being able to answer confidently.

@bruxisma had a good reply to my other post about this, and i briefly mentioned it in the OP here - you can always do namespace aliases, i.e. namespace short = some::really::long::namespace and then do short::Foo instead of some::really::long::namespace::X. i think this is as close to a middle ground as you can get. between that and just lowering the number of namespaces, i feel like it's kind of a style choice.

ahh, i misunderstood because of the way i had grouped them in my initial post - aliases are definitely better than using namespace!

so, in principle there's nothing wrong with specifically namespace aliases - they're ADL friendly, they don't leak (as long as you put the alias inside the current namespace and not at global scope), and you only need to write 1 extra line to establish the alias.

i guess i don't have solid reasoning for not using them besides them just... feeling weird? idk, this is not super rigorous of me, it just feels strange to tell clients to write namespace ns = mylib::some::nested::ns; in all of their source files. the most natural-feeling (to me) kind of API is exactly one level of nesting for a library scope (e.g. mylib::Foo foo;), and nothing more.