Reputation: 311
I want to create an interface class for a communication interface - see source code. However, different interfaces (USB, COM, etc.) have different configuration options, so the Open( ) function must/should reflect this. Thus I've created a template class:
#include <stdio.h>
#include <iostream>
template <typename T>
class Interface
{
public:
virtual int Open(const T & config) = 0;
virtual void PrintConfig(void) = 0;
};
struct config_c1
{
int a;
int b;
};
class C1 : public Interface<struct config_c1>
{
public:
int Open(const struct config_c1 & config) override
{
config_ = config;
return 0;
}
void PrintConfig(void) override
{
std::cout << config_.a << " " << config_.b << std::endl;
}
private:
struct config_c1 config_;
};
struct config_c2
{
int c;
int d;
};
class C2 : public Interface<struct config_c2>
{
public:
int Open(const struct config_c2 & config) override
{
config_ = config;
return 0;
}
void PrintConfig(void) override
{
std::cout << config_.c << " " << config_.d << std::endl;
}
private:
struct config_c2 config_;
};
int main (void)
{
config_c1 cfg1{0, 1};
config_c2 cfg2{2, 3};
C1 c1;
c1.Open(cfg1);
c1.PrintConfig();
C2 c2;
c2.Open(cfg2);
c2.PrintConfig();
/* But I realized this won't work as "I" must be declared either as I<config_c1> or I<config_c2> */
Interface * I = &c1;
I->PrintConfig();
I = &c2;
I->PrintConfig();
}
However, using this construct I can't create a base pointer to access different interface configurations.
What is the way out??
I've been thinking to update the interface following way:
class Interface
{
public:
virtual int Open(const void * config) = 0;
virtual void PrintConfig(void) = 0;
};
In such a case I would need to distinguish between different configurations in some way like:
struct config_c1
{
int interface_id;
}
But that seems awkward in C++ and I would expect more elegant way?? Thx for any hints or directions.
EDIT: My mistake. I've provided an example to print stuff in a generic way, but the main target was to have a common interface for all the operations: Open, Close, Read, Write. So I can do:
Interface * I = <whichever_class_implementing_that_interface>;
I->Open(config);
I->Write(message);
I->Close();
If that's an incorrect approach / design, what are the ways these things are implemented?
Upvotes: 0
Views: 77
Reputation: 158
As correctly pointed out in the comment, you are trying to mix a CRTP-like approach (it's not, actually, crtp, but similar. More info here CRTP) with virtual inheritance, which may work elegantly in some contexts but not here, while you are trying the type erasure with template class. The main goal is to move the Config type outside the Interface. One possible approach is to do it below. It may not cover all your needs, but it will give you some hint.
#include <iostream>
class Interface {
public:
virtual void PrintConfig() const = 0;
virtual ~Interface() = default;
};
struct config_c1 { int a; int b; };
struct config_c2 { int c; int d; };
template<typename> struct ConfigPrinter {};
template<> struct ConfigPrinter<config_c1> {
void operator()(const config_c1& cnf) const noexcept {
std::cout << cnf.a << " " << cnf.b << std::endl;
}
};
template<> struct ConfigPrinter<config_c2> {
void operator()(const config_c2& cnf) const noexcept {
std::cout << cnf.c << " " << cnf.d << std::endl;
}
};
template<typename TConf>
requires requires(const TConf& c) { ConfigPrinter<TConf>{}(c); }
class ConfigHolder {
public:
int Open(const TConf& conf) {
config_ = conf;
return 0;
}
protected:
void Print() const { ConfigPrinter<TConf>{}(config_); }
private:
TConf config_;
};
template<typename TConf>
class SomeC : public Interface, public ConfigHolder<TConf> {
public:
void PrintConfig() const override { ConfigHolder<TConf>::Print(); }
};
int main ()
{
config_c1 cfg1{0, 1};
config_c2 cfg2{2, 3};
SomeC<config_c1> c1;
c1.Open(cfg1);
c1.PrintConfig();
SomeC<config_c2> c2;
c2.Open(cfg2);
c2.PrintConfig();
/* But I realized this won't work as "I" must be declared either as I<config_c1> or I<config_c2> */
Interface* I = &c1;
I->PrintConfig();
I = &c2;
I->PrintConfig();
}
Upvotes: 0
Reputation: 311
Ok, so I mixed up two approaches. Approach 1 (I know in advance which interface I'll use):
class Interface
{
public:
virtual int Write(std::string data) = 0;
virutal int Read(std::string data) = 0;
};
class COM : public Interface
{
public:
int Open(com_config & ) { ...implementation... };
int Close() { ...implementation... };
int Write(std::string data) { ...implementation... };
int Read(std::string data) { ...implementation... };
}
class USB : public Interface
{
public:
int Open(usb_config & ) { ...implementation... };
int Close() { ...implementation... };
int Write(std::string data) { ...implementation... };
int Read(std::string data) { ...implementation... };
}
class ControlSomething
{
public:
ControlSomething(Interface * I);
int RestartDevice() { I->Write( <message_to_restart_device>); };
int DisableFeature() { I->Write( <message_to_disable_feature>); };
private:
Interface * I;
}
int main (void)
{
/* I know in advance, which interface I'm going to use - USB */
USB usb_connection;
usb_config usb_cfg = {...};
usb_connection.Open(usb_config);
ControlSomething cs{usb_connection};
cs.RestartDevice();
cs.DisableFeature();
usb_connection.Close();
}
Approach 2 (I don't know in advance which interface I'll use); the user provides the input from command line about the used interface:
/* Extend Interface class with Open/Close and use a generic type for config */
class Interface
{
public:
virtual int Open(std::string & config) = 0;
/* Or use YAML / JSON?? */
// virtual int Open(std::string & json_file_path) = 0;
/* Or use void pointer?? not really safe, but doable */
// virtual int Open(const void * config) = 0;
virutal int Close() = 0;
virtual int Write(std::string data) = 0;
virutal int Read(std::string data) = 0;
};
/* COM & USB interface updates the Open */
...
int Open(std::string & config);
or
int Open(std::string & json_file_path);
or
int Open(const void * config);
class ControlSomething
{
public:
ControlSomething(Interface * I);
int Connect(std::string & config) { I->Open(config);};
or
int Connect(std::string & json_file_path) { I->Open(json_file_path); };
...
int Disconnect(void) { I->Close(); };
int RestartDevice() { I->Write( <message_to_restart_device>); };
int DisableFeature() { I->Write( <message_to_disable_feature>); };
private:
Interface * I;
}
int main (char argc, char ** argv)
{
/* pseudo code */
args = parse_argv();
/* args content - type of connection, configuration string / json etc. */
Interface * I = CreateInterface( args.type );
ControlSomething cs{I};
cs.Open(args.config);
cs.RestartDevice();
cs.DisableFeature();
cs.Close();
}
Does the second solution makes sense? Or this approach is not used at all? (code not tested, not sure it will work)
Upvotes: 0