I'm working on 6502 emulator in C++ as a part of my thesis. It has 6 registers, most of them just hold values but there's one special - Processor Status. It's 8 bit wide and each bit means a different flag. The best choice for me seemed to make it std::bitset<8> and create a corresponding enum class to map its values to the real bits as follow:
enum class PSFlags : uint8_t
{
Carry = 0,
Zero = 1,
InterruptDisable = 2,
Decimal = 3,
Break = 4,
Unknown = 5,
Overflow = 6,
Negative = 7
};
struct Registers
{
int8_t A;
int8_t X;
int8_t Y;
uint8_t SP;
uint16_t PC;
static constexpr uint8_t PSSize = 8;
std::bitset<PSSize> PS;
constexpr Registers() noexcept :
A(0),
X(0),
Y(0),
SP(0xFF),
PS(0b00100100),
PC(0)
{
}
};
And now, if I want to refer to one of three: size of PS, the flag number or the bitset itself I have:
Registers::PSSize; // size
PSFlags::Carry; // flag number
Registers r; r.PS; // bitset itself
Where every call accesses the value in a very different way. I'd like to have it more consistent, e.g.
Registers::PS::value; // for the bitset itself
Registers::PS::size; // for the size
Registers::PS::flags::Carry; // for the name of flag
Do you have any good ideas on how to achieve such (or similar) consistency without creating some crazy or ugly constructs in the code?
What OP wants (or something acceptable similar) can be achieved using nested struct
s.
Just for fun, I tried to model what OP intended:
#include <bitset>
struct Registers
{
int8_t A;
int8_t X;
int8_t Y;
uint8_t SP;
static constexpr uint8_t PSSize = 8;
struct PS: std::bitset<PSSize> {
enum Flags {
Carry = 0,
Zero = 1,
InterruptDisable = 2,
Decimal = 3,
Break = 4,
Unknown = 5,
Overflow = 6,
Negative = 7
};
static constexpr unsigned Size = PSSize;
constexpr PS(std::uint8_t value):
std::bitset<PSSize>((unsigned long long)value)
{ }
std::uint8_t value() const { return (std::uint8_t)to_ulong(); }
} PS;
uint16_t PC;
constexpr Registers() noexcept :
A(0),
X(0),
Y(0),
SP(0xFF),
PS(0x24),//PS(0b00100100),
PC(0)
{
}
} r;
A small test to show this in action:
#include <iomanip>
#include <iostream>
#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
int main()
{
std::cout << std::hex << std::setfill('0');
DEBUG(std::cout << Registers::PS::Flags::Carry << '\n');
DEBUG(std::cout << r.PS[Registers::PS::Flags::Carry] << '\n');
DEBUG(std::cout << Registers::PS::Flags::InterruptDisable << '\n');
DEBUG(std::cout << r.PS[Registers::PS::Flags::InterruptDisable] << '\n');
DEBUG(std::cout << Registers::PS::Flags::Break << '\n');
DEBUG(std::cout << r.PS[Registers::PS::Flags::Break] << '\n');
DEBUG(std::cout << Registers::PS::Size << '\n');
DEBUG(std::cout << "0x" << std::setw(2) << (unsigned)r.PS.value() << '\n');
// done
return 0;
}
Output:
std::cout << Registers::PS::Flags::Carry << '\n';
0
std::cout << r.PS[Registers::PS::Flags::Carry] << '\n';
0
std::cout << Registers::PS::Flags::InterruptDisable << '\n';
2
std::cout << r.PS[Registers::PS::Flags::InterruptDisable] << '\n';
1
std::cout << Registers::PS::Flags::Break << '\n';
4
std::cout << r.PS[Registers::PS::Flags::Break] << '\n';
0
std::cout << Registers::PS::Size << '\n';
8
std::cout << "0x" << std::setw(2) << (unsigned)r.PS.value() << '\n';
0x24
Note:
About naming the nested struct Registers::PS
and the member Registers::PS
with same name is I was expecting to work. Though, usually I use uppercase start character for type identifiers and lowercase start characters for variables. Hence, I don't have this issue usually.
As being in doubt about this, I tested the struct Registers
against various compilers (though I wouldn't count this as proof against the standard): Compiler Explorer
Deriving from std::
containers should be done with care (i.e. better not). Probably for performance reasons, none of the std::
containers provides a virtual
destructor with the respective consequences. In the above code, this shouldn't be a problem.
6502 reminded me to the Commodore 64 where I made my first attempts on (although the C64 had the even more modern 6510 CPU). However, that's looong ago... ;-)