That's good if you need just one implementation in a given binary
In particular for differences based on the target device, since one expects a given binary to be running on a particular platform.
I have found it helpful to further organize the differences into subdirectories, so that most of the project can refer to the files unqualified, with the build system setting the search paths appropriately for each build configuration. That also makes it easier to keep track of which files are generic and which are tied to a particular implementation.
For example, if your cortex-m4 uses the lwip BSD socket api, and you want to debug the code using the sockets on desktop linux or windows, you might have a platform/lwip, platform/bsd, and/or platform/winsock subdirectories. Then files for a "socket_interface" that provide a common API to the rest of your code, adapting to whichever actual socket implementation they include, go in those directories. Your build configuration can set the search directories (and libraries to link) appropriately for each target. I once ported an existing library from Windows (winsock, winthread) to a cortex-M4 (lwip, CMSIS/KEIL), and handled the different implementations that way, so I could still debug the common code--pretty much everything besides the socket and thread code--on Windows with wireshark to record packets.
Since a given binary would only ever target one platform, and thus one socket implementation, anyway, there isn't much drawback to letting the build configuration switch such implementations. I don't know exactly what your real-world situation is, so I'll note that this kind of method can get unwieldy once you start needing more than one implementation in the same binary.
There is an alternative if you need more than one implementation
If you need multiple implementations within a given build, but still want to avoid a vtable lookup, one alternative is to consider making your class a template instead, for example using CRTP to get compile-time polymorphism, rather than run-time polymorphism. Do be aware that in this method, base pointers to different derived classes are not convertible as they would be with run-time polymorphism. For derived classes "A
" and "B
", and base template "base<typename>
", the base classes are "base<A>
" and "base<B>
", which are, to the language, completely different types, even if they look basically the same to us.
If you would benefit from run-time polymorphism, use it anyway
It sounds like you don't actually need run-time polymorphism in this particular situation, in which case, absolutely don't use it. The general C/C++ rule of "don't pay for it if you don't need it" especially applies to embedded devices; saving resources by eliminating things you didn't need can mean more capacity for other features, or downgrading to cheaper hardware, which can in turn mean competitive advantage.
That said, the Cortex-M4 may be more powerful than you might think. If you find yourself in a situation where inheritance provides a suitable benefit, I'd use inheritance anyway.
As an anecdotal data point from my own personal experience, the right model M4 with the right supporting hardware and appropriate configuration, can run a preemptive RTOS, filter digital and analog user control inputs, handle Ethernet over EtherCAT (including rerouting packets) fast enough for remote display (800x480 pixel) for an industrial HMI. That project was in C, not C++, so there were no virtual functions, but my point is rather that this isn't a chip with a data bus width too small for pointers, or a chip that can't handle the overhead of a preemptive RTOS, or, depending on model and use case, a chip that would even need slower off-chip RAM.
Do make sure you review gcc's options to tune for the cortex-M4, but for judicious use of single, non-virtual inheritance, I wouldn't be especially worried unless and until I saw the code running too slowly.