5
\$\begingroup\$

Forward

This is a continuation of my work in progress and the last iteration that I posted can be found here. I have designed a compact class template that uses SFINAE with constructor delegation to reduce the amount of code duplication, and to keep this as generic and portable as possible.

I even added in a few functions that works on these registers.

One is an internal method that will retain the same internal value, but will adjust the bitset from one endian to another: Note: I'm only currently supporting Little Endian and Big Endian representations. Currently I do not have any internal members to act as a flag to tell what the current state of the endian is in but I may add this in later and I may add it by either an enumeration, by a bool flag, or by both(one to indicate which it is and the other to indicate if there was a change).

I also have a function that will reverse the order of all of the bits, for example: 0x00110101 after being reversed will become: 0x10101100 and the value will change in this case. Some cases the bit's are perfect mirror representations so the value will stay the same as there is no effect on the bit pattern as in: 0x01011010. This will be the same in either version, but the endian view may change. My reverse function has a second bool parameter which is false by default and will modify the contents internally, if true is passed it will create a copy and return by that copy.

I am able to construct Any of the Four Register types from Any of the Four basic unsigned integral types, or from any of the other Four Register types. All of the types are assumed to be a multiple of bytes such that:

  • u8 = 8bits = 1 byte
  • u16 = 16bits = 2 bytes
  • u32 = 32bits = 4bytes
  • u64 = 64bits = 8bytes

Design

There are three cases or ways my constructors work and they follow this set of design rules:

Overall structure of class:

  • In the first case it is constructing a smaller size from a larger size and can extract a byte, word, or dword from a word, dword or qword by the index value. If the index value is out of range, then assert.

  • In the second case it is constructing a larger size from a smaller size and can set a byte word or dword into word, dword or qword at that index location. If the index value is out of range, then assert.

  • In the third case (default) case it is a 1 to 1 mapping so no calculations nor assertions need to be performed, just save the contents, and the index parameter if passed will have no effect.


Implementation

Here is my class declaration:

#pragma once

#include <algorithm>
#include <assert.h>
#include <bitset>
#include <cstdint>
#include <iostream>
#include <iomanip>
#include <limits>
#include <string>
#include <type_traits>

namespace vpc {
    using u8  = std::uint8_t;
    using u16 = std::uint16_t;
    using u32 = std::uint32_t;
    using u64 = std::uint64_t;

    template<typename T>
    struct Register {
        T data;
        T value;
        std::bitset<sizeof(T)* CHAR_BIT> bits;

        Register() : data{ 0 }, value{ 0 }, bits{ 0 } {}

        template<typename P, std::enable_if_t<(sizeof(P) > sizeof(T))>* = nullptr>
        explicit Register(const P val, const u8 idx = 0) :
            data{ static_cast<T>((val >> std::size(bits) * idx) &
                  std::numeric_limits<std::make_unsigned_t<T>>::max()) },
            value{ data },
            bits{ data }
        {

            constexpr u16 sizeT = sizeof(T);
            constexpr u16 sizeP = sizeof(P);
            assert((idx >= 0) && (idx <= ((sizeP / sizeT) - 1)) );
        }


        template<typename P, std::enable_if_t<(sizeof(P) < sizeof(T))>* = nullptr>
        explicit Register(const P val, const u8 idx = 0) :
            data{ static_cast<T>((static_cast<T>(val) << sizeof(P)*CHAR_BIT*idx) &
                  std::numeric_limits<std::make_unsigned_t<T>>::max()) },
            value{ data },
            bits{ data }
        {
            constexpr u16 sizeT = sizeof(T);
            constexpr u16 sizeP = sizeof(P);
            assert((idx >= 0) && (idx <= ((sizeT / sizeP) - 1)) );
        }


        template<typename P, std::enable_if_t<(sizeof(P) == sizeof(T))>* = nullptr>
        explicit Register(const P val, const u8 idx = 0) :
            data{ static_cast<T>( val ) }, value{ data }, bits{ data }
        {}

        template<typename P>
        explicit Register(const Register<P>& reg, const u8 idx = 0) : Register(reg.data, idx) {}

        void changeEndian() {
            T tmp = data;
            char* const p = reinterpret_cast<char*>(&tmp);
            for (size_t i = 0; i < sizeof(T) / 2; ++i)
                std::swap(p[i], p[sizeof(T) - i - 1]);
            bits = tmp;
        }
    };

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

    template<typename T>
    std::ostream& operator<<(std::ostream& os, const Register<T>& r) {
        return os << "Reg" << std::size(r.bits) << '(' << +r.data << ")\nhex: 0x"
            << std::uppercase << std::setfill('0') << std::setw(sizeof(T) * 2) << std::hex
            << +r.bits.to_ullong() << std::dec << "\nbin: "
            << r.bits << "\n\n";
    }

    // this is a universal template class to change the endian on any value
    template<typename T>
    T changeEndian(T in) {
        char* const p = reinterpret_cast<char*>(&in);
        for (size_t i = 0; i < sizeof(T) / 2; ++i)
            std::swap(p[i], p[sizeof(T) - i - 1]);
        return in;
    }

    template<typename T>
    Register<T> reverseBitOrder(Register<T>& reg, bool copy = false) {
        static constexpr u16 BitCount = sizeof(T) * CHAR_BIT;

        auto str = reg.bits.to_string();
        std::reverse(str.begin(), str.end());

        if (copy) { // return a copy
            Register<T> cpy;
            cpy.bits = std::bitset<BitCount>(str);
            cpy.data = static_cast<T>(cpy.bits.to_ullong());
            return cpy;
        }
        else {
            reg.bits = std::bitset<BitCount>(str);
            reg.data = static_cast<T>(reg.bits.to_ullong());
            return {};
        }
    }

} // namespace vpc

Example

And here is an example of it in use:

#include "Register.h"

int main() {
    using namespace vpc;

    Reg8  r8{ 0xEF };
    Reg16 r16{ 0xABCD };
    Reg32 r32{ 0x23456789 };
    Reg64 r64{ 0x0123456789ABCDEF };

    std::cout << "Default Constructors by value\n";
    std::cout << r8 << r16 << r32 << r64 << '\n';

    std::cout << "Showing opposite endian of original values\n";
    r8.changeEndian();
    r16.changeEndian();
    r32.changeEndian();
    r64.changeEndian();
    std::cout << r8 << r16 << r32 << r64;

    std::cout << "Reversing the bit representation\n";
    reverseBitOrder( r8 );
    reverseBitOrder( r16 );
    reverseBitOrder( r32 );
    reverseBitOrder( r64 );
    std::cout << r8 << r16 << r32 << r64 << '\n';

    std::cout << "Showing opposite endian of the reversed bit order\n";
    r8.changeEndian();
    r16.changeEndian();
    r32.changeEndian();
    r64.changeEndian();
    std::cout << r8 << r16 << r32 << r64;

    // I'm only going to show a couple for demonstration instead of
    // showing every possible combination

    std::cout << "Constructing from larger types:\n";
    Reg8 r8a0{ r32, 0 }; // sets r8 to what is in r32 at 0
    Reg8 r8a1{ r32, 1 }; // sets r8 to what is in r32 at 1
    Reg8 r8a2{ r32, 2 }; // sets r8 to what is in r32 at 2 
    Reg8 r8a3{ r32, 3 }; // sets r8 to what is in r32 at 3
    // Reg8 r8a4{ r32, 4 }; // uncomment -> assertion failure index out of range
    std::cout << r8a0 << r8a1 << r8a1 << r8a2 << '\n';

    // This also works not just by Register<T> but also from types:
    Reg8 r8b0{ u32(0x01234567), 0 }; // r8a0 = 0x67
    Reg8 r8b1{ u32(0x01234567), 1 }; // r8a1 = 0x45
    std::cout << r8b0 << r8b1 << '\n';

    Reg64 r64a0{ r32, 0 };
    Reg64 r64a1{ r32, 1 }; 
    // Reg64 r64a2{ r32, 2 }; // uncomment -> assertion failure index out of range
    std::cout << r64a0 << r64a1 << '\n';

    // Just for fun:
    r64a0.changeEndian();
    r64a1.changeEndian();
    std::cout << r64a0 << r64a1 << '\n';
    reverseBitOrder( r64a0 );
    reverseBitOrder( r64a1 );
    std::cout << r64a0 << r64a1 << '\n';
    r64a0.changeEndian();
    r64a1.changeEndian();
    std::cout << r64a0 << r64a1 << '\n';

    std::cout << "Constructing from smaller types\n";
    Reg32 r32a { r8, 0 };
    Reg32 r32b { r8, 1 };
    Reg32 r32c { r8, 2 };
    Reg32 r32d { r8, 3 };
    // Reg32 r32e{ r8, 4 }; // uncomment -> assertion failure index out of range
    std::cout << r32a << r32b << r32c << r32d << '\n'; 

    // This also works not just by Register<T> but also from types:
    Reg32 r32b0{ u16(0x4567), 0 }; // r32a0 = 0x00004567
    Reg32 r32b1{ u16(0x4567), 1 }; // r32a1 = 0x45670000
    std::cout << r32b0 << r32b1 << '\n';

    // Third case constructor 
    Reg8 r8x(r8);
    Reg8 r8x2(r8,2); // 2 has no effect due to same size
    std::cout << r8x << r8x2 << '\n';        

    Reg16 r16x(r16);
    Reg16 r16x3( u16(0xABCD), 3 ); // 3 has no effect due to same size
    std::cout << r16x << r16x3 << '\n';

    return EXIT_SUCCESS;
}

Output

And here is the output: it matches expected values!

Default Constructors by value
Reg8(239)
hex: 0xEF
bin: 11101111

Reg16(43981)
hex: 0xABCD
bin: 1010101111001101

Reg32(591751049)
hex: 0x23456789
bin: 00100011010001010110011110001001

Reg64(81985529216486895)
hex: 0x0123456789ABCDEF
bin: 0000000100100011010001010110011110001001101010111100110111101111


Showing opposite endian of original values
Reg8(239)
hex: 0xEF
bin: 11101111

Reg16(43981)
hex: 0xCDAB
bin: 1100110110101011

Reg32(591751049)
hex: 0x89674523
bin: 10001001011001110100010100100011

Reg64(81985529216486895)
hex: 0xEFCDAB8967452301
bin: 1110111111001101101010111000100101100111010001010010001100000001

Reversing the bit representation
Reg8(247)
hex: 0xF7
bin: 11110111

Reg16(54707)
hex: 0xD5B3
bin: 1101010110110011

Reg32(3299010193)
hex: 0xC4A2E691
bin: 11000100101000101110011010010001

Reg64(9278720243462943735)
hex: 0x80C4A2E691D5B3F7
bin: 1000000011000100101000101110011010010001110101011011001111110111


Showing opposite endian of the reversed bit order
Reg8(247)
hex: 0xF7
bin: 11110111

Reg16(54707)
hex: 0xB3D5
bin: 1011001111010101

Reg32(3299010193)
hex: 0x91E6A2C4
bin: 10010001111001101010001011000100

Reg64(9278720243462943735)
hex: 0xF7B3D591E6A2C480
bin: 1111011110110011110101011001000111100110101000101100010010000000

Constructing from larger types:
Reg8(145)
hex: 0x91
bin: 10010001

Reg8(230)
hex: 0xE6
bin: 11100110

Reg8(230)
hex: 0xE6
bin: 11100110

Reg8(162)
hex: 0xA2
bin: 10100010


Reg8(103)
hex: 0x67
bin: 01100111

Reg8(69)
hex: 0x45
bin: 01000101


Reg64(3299010193)
hex: 0x00000000C4A2E691
bin: 0000000000000000000000000000000011000100101000101110011010010001

Reg64(14169140888105648128)
hex: 0xC4A2E69100000000
bin: 1100010010100010111001101001000100000000000000000000000000000000


Reg64(3299010193)
hex: 0x91E6A2C400000000
bin: 1001000111100110101000101100010000000000000000000000000000000000

Reg64(14169140888105648128)
hex: 0x0000000091E6A2C4
bin: 0000000000000000000000000000000010010001111001101010001011000100


Reg64(591751049)
hex: 0x0000000023456789
bin: 0000000000000000000000000000000000100011010001010110011110001001

Reg64(2541551402828693504)
hex: 0x2345678900000000
bin: 0010001101000101011001111000100100000000000000000000000000000000


Reg64(591751049)
hex: 0x8967452300000000
bin: 1000100101100111010001010010001100000000000000000000000000000000

Reg64(2541551402828693504)
hex: 0x0000000089674523
bin: 0000000000000000000000000000000010001001011001110100010100100011


Constructing from smaller types
Reg32(247)
hex: 0x000000F7
bin: 00000000000000000000000011110111

Reg32(63232)
hex: 0x0000F700
bin: 00000000000000001111011100000000

Reg32(16187392)
hex: 0x00F70000
bin: 00000000111101110000000000000000

Reg32(4143972352)
hex: 0xF7000000
bin: 11110111000000000000000000000000


Reg32(17767)
hex: 0x00004567
bin: 00000000000000000100010101100111

Reg32(1164378112)
hex: 0x45670000
bin: 01000101011001110000000000000000


Reg8(247)
hex: 0xF7
bin: 11110111

Reg8(247)
hex: 0xF7
bin: 11110111


Reg16(54707)
hex: 0xB3D5
bin: 1011001111010101

Reg16(43981)
hex: 0xABCD
bin: 1010101111001101

Objectives

My class above is not complete yet as this is just a public interface. Eventually I'm thinking about turning it into a class and encapsulating the internal members. There are a few more constructors that I would like to add and that is constructing a larger Register<T> from several smaller Register<T> objects.

Pseudo Example:

 Reg8 r8a{ 0xAB };
 Reg8 r8b{ 0xFE };    
 Reg16 r16{ r8a, r8b }; // would yield 0xFEAB
                        // remember that first index 0 is to the right
                        // so our first passed in parameter would be
                        // set to index 0 and the next would be index 1

Possible future additions:

  • If I do plan on encapsulating the members by making then private and adding accessing and modifying functions, then I'll need to add them. Other than that this is the bulk of my class design.

  • I may even add operator<<=() and operator>>=() that would push and pop information from one register to another, streaming the bits. The same thing as you would see in assembly languages when you shift the registers.

  • Eventually I'll add in the arithmetic and comparison operators that you would commonly see done in assembly language on registers. Addition, Subtraction, Negation, Comparison etc. I'll even include the operator[] to get the individual bit at a specific index through the use of bitset.

  • I'll even have a function to get a byte, word, or dword from a word, dword or qword by index similar to the constructors, and one to extract them as well.

  • Maybe a few house keeping functions

  • As of now I have a data and value variables and the value isn't being used. I plan on changing data to value and rename value to previousValue, then if the value of the internal register changes, both its value and its bitset bit pattern changes and it will retain a history of the last value. It'll do this by saving value into previousValue before applying the changes.

    • I think this would be a nice feature so that when working with the CPU class and you are moving in and out of registers or adding in place by immediate values, etc. you may not have to construct another temp Register<T> object since you can do the operation on its history, then the calculation on that register can be done in place.

Objectives I'd like to fulfill:

  • Keep the bulk of the work done during compile time and let the compiler optimize away all it needs to.
  • Allowing it to be fast and efficient.
  • Making it to be generic, portable and reusable.
  • Making it readable and expressive enough.
  • Not sure if this is thread safe or not: it is something I'd like to keep in mind.
    • Let's say one builds a virtual CPU that is a quad core. They may want to have each core in its own thread. Then all of the registers belong to each of the individual cores of the CPU would have to be thread safe.
    • Even in a single core environment, one might want to construct their CPU that has multithreading capabilities and the registers again should be thread safe.
  • Finally, the one thing I like about it the most is a combination of its ease of use, and its dynamic ability to be created from the four common basic unsigned types as well as the four templates of Register types. I also like the fact that you can construct a smaller register from a larger and vise versa and that you can index into which byte, word or dword you want to save or write to.

Summary

I like to consider this a very versatile multipurpose register class with a good amount of flexibility, code reuse, easy to use, and being generic and portable with great readability while keeping thread safety in mind.


Conclusion

Other things I'd like to commit to this project in the future after finishing with the operators and methods, would be to extend this to signed types as well as floating types, but that's tomorrows challenge. Then I plan to use it in my emulator projects where I plan on emulating an 8080 and a 6502 CPUs. I plan on using the 8080 to emulate arcade games that ran on the 8080 such as Space Invaders, and the 6502 such as an NES emulator.


Feedback: Questions & Concerns

Remember these will be used to create a virtual CPU or virtual machine so think of these as you would an actual register within a CPU. Let me know what you think of my universal multipurpose virtual Register class. I want to hear all different kinds of feedback; pros and cons, where improvements can be made, any corner cases I may have missed, etc.

\$\endgroup\$
4
\$\begingroup\$

Here are some thoughts on how to further improve your program.

Use all required #includes

The program refers to CHAR_BIT which is defined in <climits> but only <limits> is currently included. Similarly, std::size is defined in <iterator>.

Eliminate unused variables

The idx is sometimes used and sometimes ignored. My thought is that allowing a parameter that is silently ignored is not as good as simply throwing a compile-time error. For that reason, I'd remove idx from those calls.

Use the C++ version of #include files

Instead of <assert.h> a C++ program should include <cassert> to avoid polluting the global namespace.

Reconsider the use of templates

The current code allows me to do this:

Register<std::string> stringreg{};
Register<std::ostream> streamreg{};
Register<double> doublereg{3.141};
auto madness{stringreg};

The only complaint from the compiler is about a narrowing conversion with doublereg. It's hard to imagine that these "Register" types would be useful, with the possible exception of a double register. For that reason, I'd suggest either using the four concrete sizes without templates or adding further restrictions via std::enable_if.

Consider a more efficient data structure

Processors, either real or simulated, don't typically do that much bit reversal. Also endianness, on processors where it can be changed, is typically a global value and not a per-register value. For all of those reasons, I would suggest that using native types such as uint_fast8_t or uint_least8_t (depending on whether speed or size is important to your program) might be a better choice. Reversals and bit manipulation on integral types is not that hard. Operations such as mask-and-shift are likely to be much more important than bit reversals.

My typical approach with things like this is to actually write sample code first, as though I had already created the Register class and then let the proposed uses guide the design.

Consider automating tests

There are a number of ways to automate testing. The code you've presented has a good start at exercising the options, but what it doesn't have is a way to automatically verify the results. I often use cppunit for unit tests like this, but there are other unit test frameworks as well.

\$\endgroup\$
  • \$\begingroup\$ About idx sometimes not being used. This is not true, in fact it is always used. If you pass in different times and omit it, then it is the same as passing in 0. It is also a crucial part of the structure and for doing the calculations in extracting or setting the words at that idx upon construction. The only time that the idx has no effect or acts as a no op is when both T and P have the same size, so in this case it doesn't matter if you pass in 0, 1, 4 etc. because there is no calculation for it as it is a 1-1 mapping of bits. \$\endgroup\$ – Francis Cugler May 18 at 21:08
  • \$\begingroup\$ You also mentioned about not having templates and just going with the 4 basic types non-template. Well I've tried this already, and when I started to put my registers into containers via smart pointers I was having issues being able to call the class's member functions. So I decided to use a single template structure. My main goal is the 4 basic types, as for floats; that can always be done in software. But I do appreciate your feed back as this is a great learning experience for me. This is my first time trying to write an emulator. \$\endgroup\$ – Francis Cugler May 18 at 21:08
  • \$\begingroup\$ If the passed idx variable has no effect, and is not referenced in the code for some functions that is the definition of unused, right? \$\endgroup\$ – Edward May 18 at 21:11
  • 1
    \$\begingroup\$ For an alternative approach to simulation, see codereview.stackexchange.com/questions/115118/… \$\endgroup\$ – Edward May 18 at 21:21
  • 1
    \$\begingroup\$ "std::size is defined in <iterator>." In fact, per [[iterator.range]/1](timsong-cpp.github.io/cppwp/n4659/iterator.range#1), #include <string> suffices. \$\endgroup\$ – L. F. May 19 at 12:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.