As good practice from my Turbo Pascal days, I always consider a translation unit as being a module/unit/package/..., hence
module.
So lets say you have usb module to talk with USB devices.
It would be composed of the following files:
- usb.h and usb.cpp for the public interface and os agnostic code, including function declarations for the OS specific parts.
- usb_windows.cpp, usb_linux.cpp, ... for OS specific code
- usb_windows_x86.cpp, usb_windows_arm.cpp,... for inline Assembly, intrisics or hardware specific data structures.
Then you get Make, CMake or whatever is your favourite build tool to combine those files depending on the build target.
Pre-processor usage for OS specific code always starts easy on the beginning, but when you look at a bunch of files with 20 years of history and continuous change by consulting companies you need several days just to understand what is actually being generated when dropped into a random code base.
This way is it much easier to navigate across the code.
It is also the approach adopted by Go, and a couple of other languages without direct support for pre-processors.