Search code examples
c++structvariadic-templatesellipsisfold-expression

template function specialization using variadic arguments


I have a class that takes a variable number of arguments (including no arguments) but when I try to pass a struct as argument for its constructor, I get a compile time error:

error: converting to 'const ioPin' from initializer list would use explicit constructor 'ioPin::ioPin(Args ...) [with Args = {gpioPort, gpioMode, pinState}]'
11 | t::A, .mode = gpioMode::output, .state = pinState::high });

This is my class:

/*ioPin.hh*/

struct ioPinParams
{
    gpioPort port{gpioPort::null};
    gpioPin pin{gpioPin::null};
    gpioMode mode{gpioMode::input};
    gpioPUPD pupd{gpioPUPD::disabled};
    gpioOutputType oType{gpioOutputType::pushPull};
    gpioOutputSpeed oSpeed{gpioOutputSpeed::low};
    pinState state{pinState::low};
};

class ioPin
{
    private:

    bool init();
    bool init(const GPIO_TypeDef *);
    bool init(const gpioPort &);
    bool init(const ioPinParams &);
    bool init(const gpioPin &);
    bool init(const gpioMode &);
    bool init(const gpioPUPD &);
    bool init(const gpioOutputType&);
    bool init(const gpioOutputSpeed&);
    bool init(const pinState &);

    public:
    explicit ioPin();

    template<class ...Args>
    explicit ioPin(Args ...args);
    ~ioPin();

};

template<class ...Args>
ioPin::ioPin(Args ...args)
{
    init();
    (init(args),...);
}

This is the implementation file:

/*ioPin.cpp*/

#include "ioPin.hh"
ioPin::ioPin()
{
    init();
}

This is the main:

ioPin a; /* <--- This instantation works well */ 
ioPin b(gpioMode::output, gpioPin::_5, pinState::high, gpioPUPD::disabled, GPIOA); /* <--- This instantation works well */ 
ioPin c(GPIOA, gpioPin::_5, gpioMode::output, pinState::high); /* <--- This instantation works well */ 
ioPin d(gpioMode::output, gpioPin::_5, pinState::high, gpioPort::A); /* <--- This instantation works well */ 
ioPin e( {.port = gpioPort::A, .mode = gpioMode::output, .state = pinState::high }); /* <--- Here is where the error arises */ 

int main(void)
{
    while (1)
    {
        /* code */
    }
    return 0;
}

I tried adding a template specialization to the ioPin.hh file:

template<>
ioPin::ioPin<ioPinParams>(ioPinParams params)
{

}

But the error remains exactly the same.

If I remove the explicit specifier from the constructor, the program compiles but the method

        bool init(const ioPinParams &):

Never gets called.

As the last resort, I though of the dumb idea of overloading the constructor like:

explicit ioPin(const ioPinParams &);

But then I get what is obvious: ambiguous error:

error: call of overloaded 'ioPin(<brace-enclosed initializer list>)' is ambiguous
11 | t::A, .mode = gpioMode::output, .state = pinState::high });
                                                              ^

I'd really appreciate help on this. I don't know what am I missing.


Solution

  • {.port = gpioPort::A, .mode = gpioMode::output, .state = pinState::high }
    

    This is a braced initialization list, of some unspecified type. It needs to be bound to a specified type. This needs to specify, in some way: "Hello, I'm class X, or class Y".

    If this gets passed in as a parameter to some function that's declared as X or Y, then everything gets figured out.

    Unfortunately, this is the only thing that your suffering C++ compiler has to work with:

    template<class ...Args>
    explicit ioPin(Args ...args);
    

    That's what the parameter is. Args, here, is also some mysterious, unspecified type, and it gets deduced to whatever the type is of the actual object that gets passed in.

    But what gets passed in is also something that's looking to figure out what it's type is. The TLDR version: a braced initialization list cannot be used to deduce a template parameter.

    You're going to have explicitly specify what this mysterious type is:

    ioPin e( ioPinParams{.port = gpioPort::A, .mode = gpioMode::output, .state = pinState::high });
    

    This solves this Scooby-Doo mystery. An ioPinParams parameter gets passed into the constructor, that's what gets deduced for the template parameter and then get forwarded to the correct initialization function via overload resolution.