c++templatesconstructorc++17disambiguation

Converting a set of classes to class templates and avoiding constructor ambiguity


I will try to make this question as short as I can but there is a good amount of code to show in order for one to understand what I'm trying to achieve and how to resolve my current issue.

Here are my original class declarations with all of their constructors:

Register.h - Original Version

#include <bitset>
#include <cassert>
#include <cstdint>
#include <iostream>

typedef std::uint8_t u8;
typedef std::uint16_t u16;
typedef std::uint32_t u32;
typedef std::uint64_t u64;

const u16 BYTE = 0x08, WORD = 0x10, DWORD = 0x20, QWORD = 0x40;

typedef std::bitset<BYTE> Byte;
typedef std::bitset<WORD> Word;
typedef std::bitset<DWORD> DWord;
typedef std::bitset<QWORD> QWord;

template<typename T>
void getByteFrom(T val, u8 idx, u8& res) {
    res = ((val >> (idx * 8) & 0xff));
}

template<typename T>
void getWordFrom(T val, u8 idx, u16& res) {
    res = ((val >> (idx * 16) & 0xffff));
}

template<typename T>
void getDWordFrom(T val, u8 idx, u32& res) {
    res = ((val >> (idx * 32) & 0xffffffff));
}

template<typename T>
struct Register {
    T data;
    Register() = default;
};

struct Reg8 : public Register<u8> {
    u8 value;  // must be declared before std::bitset<T>
    Byte bits;

    // Default 0 Initialized Constructor
    Reg8() : value{ 0 }, bits{ value } { this->data = 0; }

    // Constructors by Register Sized Values
    explicit Reg8(u8 val)  : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg8(u16 val) : value{ static_cast<u8>( val ) }, bits{ value } {
        this->data = value;
    }
    explicit Reg8(u32 val) : value{ static_cast<u8>( val ) }, bits{ value } {
        //this->data = value;
    }
    explicit Reg8(u64 val) : value{ static_cast<u8>( val ) }, bits{ value } {
        //this->data = value;
    }

    Reg8(u16 val, u8 idx ) {
        assert( idx == 0 || idx == 1 );
        getByteFrom(val, idx, this->value);
        bits = value;
        this->data = value;
    }

    Reg8(u32 val, u8 idx) {
        assert(idx <= 0 && idx >= 3);
        getByteFrom(val, idx, this->value);
        bits = value;
        this->data = value;
    }

    Reg8(u64 val, u8 idx) {
        assert(idx <= 0 && idx >= 7);
        getByteFrom(val, idx, this->value);
        bits = value;
        this->data = value;
    }

    // Constructors by Register Types
    template<typename T>
    explicit Reg8(Register<T>* reg) {
        this->value = static_cast<u8>( reg->data );
        this->bits = value;
    }
};

struct Reg16 : public Register<u16> {
    u16  value;  // must be declared before std::bitset<T>
    Word bits;

    // Default 0 Initialized Constructor
    Reg16() : value{ 0 }, bits{ value } { this->data = 0; }

    // Constructors by Register Sized Values
    explicit Reg16(u16& val) : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg16( u8& val) : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg16(u32& val) : value{ static_cast<u16>(val) }, bits{ value } {
        this->data = value;
    }
    explicit Reg16(u64& val) : value{ static_cast<u16>(val) }, bits{ value } {
        this->data = value;
    }

    Reg16( u32 val, u8  idx) {
        assert(idx == 0 || idx == 1);
        getWordFrom(val, idx, this->value);
        bits = value;
        this->data = value;
    }

    Reg16(u64 val, u8 idx) {
        assert(idx <= 0 || idx <= 3);
        getWordFrom(val, idx, this->value);
        bits = value;
        this->data = value;
    }

    // Constructors by Register Types
    template<typename T>
    explicit Reg16(Register<T>* reg) {
        this->value = static_cast<u16>(reg->data);
        this->bits = value;
    }

};

struct Reg32 : public Register<u32> {
    u32 value;  // must be declared before std::bitset<T>
    DWord bits;

    // Default 0 Initialized Constructor
    Reg32() : value{ 0 }, bits{ value } { this->data = 0; }

    // Constructors by Register Sized Values
    explicit Reg32(u32& val) : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg32( u8& val) : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg32(u16& val) : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg32(u64& val) : value{ static_cast<u32>(val) }, bits{ value } {
        this->data = value;
    }   

    Reg32(u64 val, u8 idx) {
        assert(idx == 0 || idx == 1);
        getDWordFrom(val, idx, this->value);
        bits = value;
        this->data = value;
    }

    // Constructors by Register Types
    template<typename T>
    explicit Reg32(Register<T>* reg) {
        this->value = static_cast<u32>(reg->data);
        this->bits = value;
    }
};

struct Reg64 : public Register<u64> {
    u64 value;  // must be declared before std::bitset<T>
    QWord bits;

    // Default 0 Initialized Constructor
    Reg64() : value{ 0 }, bits{ value } { this->data = 0; }

    // Constructors by Register Sized Values
    explicit Reg64(u64& val) : value{ val }, bits{ value }{
        this->data = value;
    }
    explicit Reg64( u8& val) : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg64(u16& val) : value{ val }, bits{ value } {
        this->data = value;
    }
    explicit Reg64(u32& val) : value{ val }, bits{ value } {
        this->data = value;
    }


    // Constructors by Register Types
    template<typename T>
    explicit Reg64(Register<T>* reg) {
        this->value = static_cast<u64>(reg->data);
        this->bits = value;
    }
};

std::ostream& operator<<(std::ostream& os, const Reg8& r);
std::ostream& operator<<(std::ostream& os, const Reg16& r);
std::ostream& operator<<(std::ostream& os, const Reg32& r);
std::ostream& operator<<(std::ostream& os, const Reg64& r);

Now I went and turned these into template classes to reduce a lot of the code duplication. And this is what I have so far:

Register.h - Newer Version

template<typename Ty>
struct Register_t {
    static constexpr u16 BitCount = sizeof(Ty) * CHAR_BIT;

    Ty currentValue;
    Ty previousValue;
    std::bitset<BitCount> bits;

    Register_t() : 
        currentValue{ 0 }, 
        previousValue{ 0 }, 
        bits{ 0 }{}

    template<typename U>
    explicit Register_t(U val) : 
        currentValue{ static_cast<Ty>(val) }, 
        previousValue{ 0 }, 
        bits{ currentValue } {}

    template<typename U>
    explicit Register_t(Register_t<U>& r) {
        this->currentValue = static_cast<Ty>(r->currentValue);
        this->bits = r->bits;
    }        
};

template<typename Ty>
struct Register : public Register_t<Ty> {
    Register() = default;
    explicit Register(Ty val) : Register_t<Ty>( val ) {}    

    // Reg8
    template<typename U>
    Register( u16 val, u8 idx) {
        assert(idx == 0 || idx == 1);
        getByteFrom(val, idx, currentValue);
        this->bits = this->currentValue;
    }

    Register(u32 val, u8 idx) {
        assert(idx <= 0 && idx >= 3);
        getByteFrom(val, idx, this->currentValue);
        this->bits = this->currentValue;
    }

    Register(u64 val, u8 idx) {
        assert(idx <= 0 && idx <= 7);
        getByteFrom(val, idx, this->currentValue);
        this->bits = this->currentValue;
    }

    // Reg16
    Register(u32 val, u8 idx) {
        assert(idx == 0 || idx == 1);
        getWordFrom(val, idx, this->currentValue);
        this->bits = this->currentValue;
    }

    Register(u64 val, u8 idx) {
        assert(idx <= 0 && idx <= 3);
        getWordFrom(val, idx, this->currentValue);
        this->bits = this->currentValue;
    }

    // Reg32
    Register(u64 val, u8 idx) {
        assert(idx == 0 || idx == 1);
        getDWordFrom(val, idx, this->currentValue);
        this->bits = this->currentValue;
    }
};

using Reg8  = Register<u8>;
using Reg16 = Register<u16>;
using Reg32 = Register<u32>;
using Reg64 = Register<u64>;

Now when it comes to the constructors that will take a std::uintx_t type as well as an index value. Some of the constructor declarations match for example:

In the original version Reg8 has Reg8(u32 val, u8 idx) and Reg16 has Reg16(u32 val, u8 idx). And if you look closer Reg8(...) asserts that idx <= 0 && idx >= 3 while Reg16(...) asserts that idx == 0 || idx == 1.

However when I try to template these classes and port over the constructors, these now become ambiguous. I don't know how to determine which assert to use to distinguish between it being an Reg8, Reg16, Reg32 etc...


Solution

  • To me it looks like everything in your class comes down to using sizeof and numeric_limits::max of the unsigned version of your type.

    I've written up for you below a rough draft of how I think the class could look:

    template <typename T>
    struct Register {
        T data;
        T value;
        bitset<sizeof(T) * CHAR_BIT> bits;
    
        Register() : data(), value() {}
    
        template <typename P>
        explicit Register(const P val) : data(static_cast<T>(val)), value(data), bits(data) {}
    
        template <typename P>
        Register(const P val, const unsigned char idx) : data(static_cast<T>((val >> std::size(bits) * idx) & numeric_limits<make_unsigned_t<T>>::max())), value(data), bits(data) {
            assert(idx == '\0' || idx < sizeof(P) / sizeof(T));
        }
    
        template <typename P>
        Register(const Register<P>& reg) : data(static_cast<T>(reg.data)), value(data), bits(data) {}
    };
    
    template <typename T>
    ostream& operator<<(ostream& os, const Register<T>& r) {
      os << "Reg" << size(r.bits) << '(' << r.data << ")\nhex: 0x" << uppercase << setfill('0') << setw(sizeof(T) * 2) << hex << r.data << dec << "\nbin: ";
    
      for(std::size_t i = 0; i < size(r.bits); ++i) {
        cout.put('0' + r.bits[i]);
      }
      return os << endl << endl;
    }
    
    template <>
    ostream& operator<<<unsigned char>(ostream& os, const Register<unsigned char>& r) {
      os << "Reg" << size(r.bits) << '(' << static_cast<int>(r.data) << ")\nhex: 0x" << uppercase << setfill('0') << setw(sizeof(unsigned char) * 2) << hex << static_cast<int>(r.data) << dec << "\nbin: ";
    
      for(std::size_t i = 0; i < size(r.bits); ++i) {
        cout.put('0' + r.bits[i]);
      }
      return os << endl << endl;
    }