I am wondering if you can do the same with a C++ object,
Yes, the same can be done. I have used C++ for memory mapped io in telecomm system development for more than a decade. I seldom dealt with structs in embedded software, a struct has no advantages over a class. Also, I prefer to mask and shift to handle fields, and I have developed a bit of distaste for bit fields.
For struct and class instances, the data layout and packing are possible to determine and confirm, and thus an assert() can inform at runtime when the data has been built incorrectly.
a) neither C nor C++ provide memory layout semantics, and
b) compiler options can change the results (packing, alignment, field rearrangement)
These two items required a runtime check of some sort. We typically asserted on data attribute total length, and one or more distances between starts of field offsets, all in a named-ctor.
Many compilers provide pragmas to support memory-mapped-io packing and alignment issues. Unfortunately, often the pragma's of different compilers have different names. This is usually not important, as hardware specific software is almost never portable anyway. All pragmas I encountered seem to work. Ultimately we chose to not use them.
Note that endianness also has impacts, but the hardware you work with will be (or perhaps must be) designed with the correct endianness.
I have most of my embedded system experience with the tool vxWorks, and some with OSE. Both using GCC prior to 2011.
and how to make this object a singleton and
I find no value in singletons. But I believe a singleton can be built in the conventional way as the code of an instance is not stored in a data field. They are separate. And when you invoke your singletonGet() method, it will need to do the mapping of the data 'out' to the hw address.
The most typical mechanism of 'how to address a hw at address 0xAAAA0001' is to cast a uint64_t enumerated address to a pointer
class FooHW; // hardware class
FooHW* foo = reinterpret_cast<FooHW*>(AAAA0001_uint64_t); // enum addr
This skips all the ctor / dtor stuff. And this is desirable. How you access a memory mapped equipment generally must not disturb the operating hw during a sofware restart. A warm start (where hw is already running, but software has been restarted) is far more common during both development and operation than a cold start (aka power bounce).
It is possible to use placement 'new' (and 'delete') to create an instance at the correct memory address. But this has no advantage. You should research cold-start, warm-start, protection-switch, etc. The Foo* pointer residing with the rest of the code is typical.
how would inheritance affect this kind of mapping.
I suppose all these questions are tool specific. What I have seen is that the base class data attributes is pre-pended to the derived class data attributes. It is possible some tools do it the other way around.
In my work, I can not remember a memory mapped instance to be derived. However, I think it can work, just more effort which I would choose to avoid.