2.1. Multiple Implementations¶
In C++, you can accomplish multiple coexisting implementations for the same object in two ways – the delegation pattern, which uses templates, and the virtual extension pattern.
2.1.1. Delegation¶
The delegation pattern populates a high-level class with object-specific properties on creation. That class then functions as an interface, delegating all its operations to those functions:
// main.cc
#include "vector.hh"
#include <iostream>
int main(int argc, char *argv[]) {
Vector x3 = mkHostVec();
x3.print();
}
The implementations for each instance of the object are created through a series of functions that get passed into the object:
// vector_host.cc and vector_cuda.cu
#include <iostream>
#include "vector.hh"
void *initCUDA() {
std::cout << "Device Ctor" << std::endl;
return nullptr;
}
void dtorCUDA(void *data) {
std::cout << "Device Dtor" << std::endl;
}
void printCUDA(void *data) {
std::cout << "Device Vector" << std::endl;
}
Vector mkCUDAVec() {
return Vector(initCUDA, dtorCUDA, printCUDA, Device::CUDA);
}
void *initHost() {
std::cout << "Host Ctor" << std::endl;
return nullptr;
}
void dtorHost(void *data) {
std::cout << "Host Dtor" << std::endl;
}
void printHost(void *data) {
std::cout << "Host Vector" << std::endl;
}
Vector mkHostVec() {
return Vector(initHost, dtorHost, printHost, Device::Host);
}
The high-level object that ties these together just stores each function to use later:
// vector.hh
#include <functional>
enum class Device { Host, CUDA };
class Vector {
private:
std::function<void *()> _ctor;
std::function<void(void *)> _dtor;
std::function<void(void *)> _print;
public:
Device dev;
Vector(std::function<void *()> &&ctor,
std::function<void(void *)> &&dtor,
std::function<void(void *)> &&print, Device _dev)
: _ctor(ctor), _dtor(dtor), _print(print), dev(_dev) {
data = _ctor();
}
~Vector() {
_dtor(data);
}
void print() {
_print(data);
}
void *data;
};
Vector mkCUDAVec();
Vector mkHostVec();
2.1.2. Virtual Extension¶
High-level code can work entirely in terms of a base object:
// main.cc
#include "vector.hh"
int main(int argc, char *argv[]) {
Vector *x3 = mkHostVec();
x3->print();
delete x3;
}
The disadvantage of this apprach is that it requires manual memory management with pointers.
The implementation codes can be separately named classes, or template specializations – either one:
// vector_host.cc and vector_cuda.cu
#include "vector.hh"
#include <iostream>
template<>
void DevVector<Device::Host>::print() {
std::cout << "Host Vec" << std::endl;
}
template<>
DevVector<Device::Host>::DevVector() {
std::cout << "Ctor Host" << std::endl;
}
template<>
DevVector<Device::Host>::~DevVector() {
std::cout << "Dtor Host" << std::endl;
}
Vector *mkHostVec() {
return new DevVector<Device::Host>();
}
template<>
void DevVector<Device::CUDA>::print() {
std::cout << "Device Vector" << std::endl;
}
template<>
DevVector<Device::CUDA>::DevVector() {
std::cout << "Ctor Device" << std::endl;
}
template<>
DevVector<Device::CUDA>::~DevVector() {
std::cout << "Dtor Device" << std::endl;
}
Vector *mkCUDAVec() {
return new DevVector<Device::CUDA>();
}
The vector.hh
header file has all the interesting parts:
// Declare tags for host and CUDA spaces
enum class Device { Host, CUDA };
// The base vector class defines only prototypes
struct Vector {
virtual void print() = 0;
virtual ~Vector() {};
};
// These classes implement the Vector interface.
template <Device dev>
struct DevVector : public Vector {
void print();
DevVector();
~DevVector();
void *data;
};
Vector *mkHostVec();
Vector *mkCUDAVec();
Contributed by
David M. Rogers