Yes, the null object pattern can help.
But instead of creating an additional dummy device type, use a dummy pointer type.
Somewhere in the config file:
using DEVICE_PTR = dummy_ptr<Device>;
using Device_PTR = Device*;
For implementation, see std::experimental::observer_ptr, aka the world's dumbest smart-pointer, and lobotomize it:
If there is much code whose execution shall be dependent on some compile-time constant, consider using lambdas, and isolating the choice in a single member using if constexpr:
template <class F>
inline constexpr auto exec_on_device(F f) noexcept(!MY_DEVICE || noexcept(f()))
if constexpr (MY_DEVICE)
The Null Object Pattern would fit perfectly here (https://en.m.wikipedia.org/wiki/Null_object_pattern).
You want an object with all the methods that doesn't do anything. That way you can call methods in this object without testing to see if the device exists. Create a sub class called NullDevice or NonExistentDevice that defines all the same methods as the ...
How function calls work depends on the calling convention. Most of the time, there will be a call stack in memory which can be used to pass function arguments etc. around. A calling convention will typically work like this:
save dirty registers to the stack
push function arguments to the stack
call the function
push the return address (next program counter)...