The principles first:
You don't have to use virtual functions to mock.
In places where you can't use runtime-polymorphic types you can use static polymorphism.
But in this case it seems better yet, decouple the configuration interface from the implementation altogether.
Implement your interface not deriving from a shared memory container ("configuration source Is-A shared memory object"). Instead say "configuration source Has-A shared memory object".
Other critical questions:
What makes YAML::Node safe for shared memory? Likely it isn't, because I don't see an allocator specified, and it most certainly involves dynamically allocated memory, as well as internal pointers.
I think the approach could easily be dead in the water just for that.
If the actual source is YAML, why not just share the file, instead of highly complicated shared memory? (We're only grazing the surface here. We did't even mention synchronization).
The filesystem is the de-facto "shared memory" of processes on a computer, since the beginning of time.
Example Decoupling Interface And Implementation
Interfaces make it so that implementations can be decoupled, but as you noticed, inheritance often makes it so that they aren't.
Why not write something like:
struct ConfigData {
struct System {
float num1;
float num2;
struct Logs {
float num3;
float num4;
} logs;
} system;
};
Now make a shared interface (I'll simplify it for the demo):
struct IConfiguration {
virtual ConfigData const& getData() const = 0;
};
So you can have either your YAML backend:
class YAMLConfiguration : public IConfiguration {
public:
YAMLConfiguration(std::istream& is) : _node(YAML::Load(is)) {
parse(_node, _data);
}
virtual ConfigData const& getData() const override {
return _data;
}
private:
YAML::Node _node;
ConfigData _data;
};
Or a shared-memory implementation:
#include <boost/interprocess/managed_shared_memory.hpp>
namespace bip = boost::interprocess;
class SharedConfiguration : public IConfiguration {
public:
SharedConfiguration(std::string name)
: _shm(bip::open_or_create, name.c_str(), 10ul << 10),
_data(*_shm.find_or_construct<ConfigData>("ConfigData")())
{ }
virtual ConfigData const& getData() const override {
return _data;
}
private:
bip::managed_shared_memory _shm;
ConfigData& _data;
};
Full Demo
Live On Coliru¹
struct ConfigData {
struct System {
float num1 = 77;
float num2 = 88;
struct Logs {
float num3 = 99;
float num4 = 1010;
} logs;
} system;
};
struct IConfiguration {
virtual ConfigData const& getData() const = 0;
};
///////// YAML Backend
#include <yaml-cpp/yaml.h>
static bool parse(YAML::Node const& node, ConfigData::System::Logs& data) {
data.num3 = node["num3"].as<float>();
data.num4 = node["num4"].as<float>();
return true;
}
static bool parse(YAML::Node const& node, ConfigData::System& data) {
data.num1 = node["num1"].as<float>();
data.num2 = node["num2"].as<float>();
parse(node["Logs"], data.logs);
return true;
}
static bool parse(YAML::Node const& node, ConfigData& data) {
parse(node["System"], data.system);
return true;
}
class YAMLConfiguration : public IConfiguration {
public:
YAMLConfiguration(std::istream& is) : _node(YAML::Load(is)) {
parse(_node, _data);
}
virtual ConfigData const& getData() const override {
return _data;
}
private:
YAML::Node _node;
ConfigData _data;
};
///////// Shared Memory Backend
#include <boost/interprocess/managed_shared_memory.hpp>
namespace bip = boost::interprocess;
class SharedConfiguration : public IConfiguration {
public:
SharedConfiguration(std::string name)
: _shm(bip::open_or_create, name.c_str(), 10ul << 10),
_data(*_shm.find_or_construct<ConfigData>("ConfigData")())
{ }
virtual ConfigData const& getData() const override {
return _data;
}
private:
bip::managed_shared_memory _shm;
ConfigData& _data;
};
#include <iostream>
void FooFunction(IConfiguration const& cfg) {
std::cout << "Logs.num3:" << cfg.getData().system.logs.num3 << "\n";
}
void FakeApplication() {
std::cout << "Hello from FakeApplication\n";
std::istringstream iss(R"(
System:
num1: 0.1
num2: 0.22
Logs:
num3: 0.333
num4: 0.4444
)");
YAMLConfiguration config(iss);
FooFunction(config);
}
void FakeTests() {
std::cout << "Hello from FakeTests\n";
SharedConfiguration config("shared_memory_name");
FooFunction(config);
}
int main() {
FakeApplication();
FakeTests();
}
Prints
Hello from FakeApplication
Logs.num3:0.333
Hello from FakeTests
Logs.num3:99
Summary And Caution
In short, think thrice before using the shared memory. It's not as simple as you think.
In all likelihood, some of your config values will be something else than POD data types (you know, maybe a string) and suddenly you'll have to care about allocators:
Also, don't forget about synchronization between processes that access shared memory.
¹ Coliru doesn't have yaml-cpp, but you can show the shared implementation with managed_mapped_file: Live On Coliru