1

So I am currently writing an abstraction layer for some embedded systems. I normally do this in C, but i want to see if I can't work in some C++ to learn a little.

I've run into an issue because I'm trying to abstract out a Usart class. The hw init needs to have hardware specific arguments, so I'm struggling to do this in cpp. In C, I could just pass a void pointer and cast the void argument to the type of my choice, but I can't do this in C++ for some reason. I also imagine there's a better way to do this in C++ that is safer so I'd like to go that route if possible. It'd be really cool to have this pass or fail at compile time and not at runtime. This is my idea so far:

#include <iostream>
#include <cstdint>

class Usart
{
public:
    Usart(){}
    ~Usart(){}

    virtual int configure(void* hw_args)
    {
        if(hw_args == NULL)
        {
            return -1;
        }
    }
private:
};

class mcu1_Usart : public Usart
{
public:
    mcu1_Usart(){}
    ~mcu1_Usart(){}
    struct config
    {
        uint32_t pin1;
        uint32_t pin2;
        uint32_t pin3;
        uint32_t pin4;
    };
    virtual int configure(void* hw_args)
    {
        config* conf = (config*)hw_args;
        // do hardware stuff with config

        return 0;
    }
};

class mcu2_Usart : public Usart
{
public:
    mcu2_Usart(){}
    ~ mcu2_Usart(){}
    struct config
    {
        uint32_t pin1;
        uint32_t pin2;
        uint32_t pin3;
        uint32_t pin4;
        uint32_t pin5;
    };
    virtual int configure(void* hw_args)
    {
        config* conf = (config*)&hw_args;
        // do hardware stuff with config
        

        return 0;
    }
};


int main()
{
    Usart _usart;
    mcu1_Usart::config conf;
    _usart.configure((void*)conf);

}

This won't compile. It says it can't convert void* to mcu1_Usart::config. Is there a better way to do this? I really want this to pass/fail at compile time, so using a function pointer as the config init is less than optimal here, but if there's no way to do this in C++ I understand.

Edit: I forgot to say &conf instead of conf. So this does compile. HOWEVER, I would like to know if there is a better way of doing this in C++ because this way is rather unsafe.

Penguin
  • 21
  • 3
  • 1
    you can convert the void pointer to another *pointer* type. so try mcu1_Usart::config* as the type. you have to pass a pointer to the struct rather than the value. it's the same as you'd do it in C, isn't it? i guess you can just pass `&conf` actually, and there shouldn't be any need to explicitly typecast to void* – Chris Rollins Jun 29 '20 at 18:43
  • Oh shit I forgot to pass conf as &conf... I'm an idiot. I am still curious to know if there is a safer way of doing this because this could lead me to passing mcu1_Usart::config as mcu2_Usart::config accidentally. – Penguin Jun 29 '20 at 18:51
  • In mcu2_Usart::Configure(void*), should it not be config* conf = (config*)hw_args;? What is the & doing there? – stackoverblown Jun 29 '20 at 19:08
  • Note that in your example `Usart::configure()` will be called, not `mcu1_Usart::configure()`. Why do you need inheritance at all? Pls explain your design needs. – rustyx Jun 29 '20 at 19:10
  • I am attempting to abstract a usart module from different mcus. So if MCU1 wants to print, I can just write print from a common usart object. the idea is I want the parent class to use a child's function. So I could simply use abstracted functions for most of these. The issue arises when initializing the hardware. They will take different kinds of arguments. So I can't just make an abstract functions because the abstract function must be the same args to compile. – Penguin Jun 29 '20 at 22:30

1 Answers1

1

I think what you're trying to do is to have an interface like "there must be a configure method that returns int and accepts an appropriate configuration structure", which the class implementations must implement in order for the code to compile.

If it's true, then you can use the curiously recurring template pattern and type comparisons to ensure that the configure method exists and has the correct signature. This will still use inheritance, but we'll get rid of virtuality of the configure method, thus avoiding useless vtable entries (and even vtable itself).

The following example uses C++17, so be sure to turn on this language version in your compiler.

#include <type_traits>
#include <iostream>
#include <cstdint>

template<typename Derived>
class Usart
{
public:
    Usart()
    {
        // This will check that there exists a method "configure" that matches
        // the signature we want.
        static_assert(std::is_same_v<decltype(&Derived::configure),
                                     int (Derived::*)(typename Derived::config const&)>);
    }
};

class mcu1_Usart : public Usart<mcu1_Usart>
{
public:
    mcu1_Usart(){}
    ~mcu1_Usart(){}
    struct config
    {
        uint32_t pin1;
        uint32_t pin2;
        uint32_t pin3;
        uint32_t pin4;
    };
    int configure(config const& conf)
    {
        // do hardware stuff with config

        return 0;
    }
};

class mcu2_Usart : public Usart<mcu2_Usart>
{
public:
    mcu2_Usart(){}
    ~mcu2_Usart(){}
    struct config
    {
        uint32_t pin1;
        uint32_t pin2;
        uint32_t pin3;
        uint32_t pin4;
        uint32_t pin5;
    };
    int configure(config const& conf)
    {
        // do hardware stuff with config

        return 0;
    }
};

int main()
{
    mcu1_Usart _usart;
    mcu1_Usart::config conf;
    _usart.configure(conf);
}

To test the compile-time check, try adding an extraneous parameter to e.g. mcu2_Usart::configure, or changing its return value, or even removing the method completely. You'll get a compilation failure, while the unchanged code above compiles successfully (try it online).

Ruslan
  • 18,162
  • 8
  • 67
  • 136