Search code examples
c++templatesc++17partial-specialization

C++ partial class template specialization over multiple parameters


I'm trying to essentially define a template class that represents a hardware peripheral, which has some remappable pins. Since the mapping is defined at compile (or actually hardware schematic drawing) -time, I'd like to bring these defines in via template parameters. However, since each of the pins can be mapped independently of the others, the set of possible types is basically a cartesian product of the individual mappings, and I'm not sure if this can be made to work. What I have now is:

     enum class SPI1_NSS {
        PA4,
        PA15
     };

     enum class SPI1_SCK {
        PA5,
        PB3
     };

     template<SPI1_NSS nss_enum, SPI1_SCK sck_enum>
     struct SPI_1 {
        //...other stuff

        struct nss;
        struct sck;

     };

     template<SPI1_SCK sck>
     struct SPI_1<SPI1_NSS::PA4, sck>::nss {
        using pin = GPIOs::A::pin<4>;
     };

     template<SPI1_SCK sck>
     struct SPI_1<SPI1_NSS::PA15, sck>::nss {
        using pin = GPIOs::A::pin<15>;
     };

     template<SPI1_NSS nss>
     struct SPI_1<nss, SPI1_SCK::PA5>::sck {
        using pin = GPIOs::A::pin<5>;
     };

     template<SPI1_NSS nss>
     struct SPI_1<nss, SPI1_SCK::PB3>::sck {
        using pin = GPIOs::B::pin<3>;
     };

This fails with error: invalid class name in declaration of 'class HAL::SPI_1<HAL::SPI1_NSS::PA4, sckp>::nss' and similar errors for the others. It works if I remove one of the two template parameters.

What I'd expect to have is that, for example, given

    using spi = SPI_1<SPI1_NSS::PA4, SPI1_SCK::PB3>;

the type spi::nss::pin would be GPIOs::A::pin<4> and spi::sck::pin would be GPIOs::B::pin<3>. Is this kind of "Cartesian specialization" possible somehow?

I do realize that I could just template on the GPIO types directly, and this is a bit overengineered. However, the advantage I'd get from this is that the enum provides and guarantees only the valid choices for the pins, so it makes for a clearer interface.


Solution

  • If your intent is to specialize orthogonality, I'd use distinct meta-functions that aren't nested withing SPI_1

    namespace detail {
      template<SPI1_NSS>
      stuct nss;
    
      template<>
      struct nss<PA4> {
        using pin = GPIOs::A::pin<4>;
      };
    
      template<>
      struct nss<PA15> {
        using pin = GPIOs::A::pin<15>;
      };
    
      // Same for sck
    }
    
    template<SPI1_NSS nss_enum, SPI1_SCK sck_enum>
    struct SPI_1 {
       //...other stuff
    
       using nss = detail::nss<nss_enum>;
       using sck = detail::sck<sck_enum>;
    };