
It’s usually a good idea to maintain one place for common code that may be used across multiple applications or libraries. This code is often compiled into those items (i.e. not a library itself). Frequently, you’ll want to be able to define the device you’re compile the code for so-as not to have to include any more code than required. Below are some ways to accomplish this. They’re separated by compile time and run time options. The compile time options are less flexible, but offer no runtime impact and smaller compiled sizes. The runtime options are more flexible, but more computationally expensive and larger in compiled size. It’s best to start with compile time solutions until you actually need a runtime one.
Compile Time Options
Using a #define
This would require your build system to pass a define into the sources, either by way of a command
line option (e.g. -DUSE_DEVICE_1
) or via a configuration header that’s generated by the build
system that contains all the defines you’d like to declare when compiling that instance. In the
latter case, you’d #include
the config header in all the sources that require using the
configuration defines.
Here’s an example of how you’d use the define in your sources to select the correct code.
#include "config.h" // This would only be required if USE_DEVICE_X was
//defined in a config.h instead of passed via command line
void doSomething() {
... any common code to run before device specific region ...
#if defined USE_DEVICE_1
... code for device 1 ...
#elsif defined USE_DEVICE_2
... code for device 2 ...
#else
#warn "No Device Defined!"
#endif
... any common code to run before device specific region ...
}
Different source files, common interface. (a.k.a. Impl)
Create a common interface in a shared header:
// abstractdevice.h
#pragma once
class AbstractDevice {
public:
AbstractDevice();
void commonFunction();
}
Then create a source file for each separate device. In your build system, only compile the one for the device you’re selecting. If there is common code, place it in a separate common file that’ll always get compiled.
// abstractdevice_device_1.cpp
#include "abstractdevice.h"
AbstractDevice::AbstractDevice() {
... code for device 1 constructor ...
}
// abstractdevice_device_2.cpp
#include "abstractdevice.h"
AbstractDevice::AbstractDevice() {
... code for device 2 constructor ...
}
// abstractdevice_common.cpp
#include "abstractdevice.cpp"
void AbstractDevice::commonFunction() {
... code that'll be common to all devices ...
}
Run Time Options
(Private) Pointer to Implementation (pImpl)
This works similar to the Impl compile time solution above, but compiles all the implementations and selects the correct one at runtime. Here’s an example of how that’d work:
// This probably should exist in a common header somewhere.
enum DeviceType {
DeviceType1,
DeviceType2,
DeviceType3
}
// abstractdevice.h
#pragma once
#include <memory>
class AbstractDevice {
public:
class Impl {
public:
Impl();
virtual ~Impl();
virtual int foo();
}
AbstractDevice(DeviceType dt = DeviceType1) {
switch(dt) {
case DeviceType1: p = new AbstractDevice_Device1(); break;
case DeviceType2: p = new AbstractDevice_Device2(); break;
case DeviceType3: p = new AbstractDevice_Device3(); break;
default: ERROR("Unsupported device %d", dt); break;
}
}
AbstractDevice::~AbstractDevice() = default;
int foo() { return p->foo(); }
private:
std::unique_ptr<Impl> p;
}
You’d then have separate implementations of the Impl objects for each device:
// abstractdevice_device1.h
#include "abstractdevice.h"
class AbstractDevice_Device1 : public AbstractDevice::Impl {
public:
AbstractDevice_Device1();
~AbstractDevice_Device1() = default;
int foo();
}
// abstractdevice_device1.cpp
#include "abstractdevice_device1.h"
AbstractDevice_Device1::AbstractDevice_Device1() : AbstractDevice::Impl() {
// Construct a device 1
}
int AbstractDevice_Device1::foo() {
// Implement device 1 version of foo()
return something;
}
Notes on Runtime Device Selection
If it’s possible, it’s always better to work with devices as an enum (which is an integer) over a string. If you keep the device name as a string, this will require a string compare, which is relatively computationally expensive over a simple integer compare. If you must use a string to define your device, it’s best to convert the string to an enum as soon as possible and then use the enum for the duration. This will reduce the number of string compares to one, while the rest can use simple integer comparisons. This is a good opportunity to use the String Value Object Pattern .