Allocating an instance of a class from a fixed place in ram

I have several dialog classes which I want to pre-allocate in RAM (for several reasons like to avoid ram fragmentation and make sure there is always enough ram to open the dialog). They are not required to be open at the same time, so I would like to save ram and allocate them from the same fixed address in RAM. The fixed location in ram would be the size of the most memory consuming dialog. Do you know if this is possible?

In C you would use a (global) union for this. I don’t know if C++ has a better way.

1 Like

Yes, it is possible. You can use the C way (putting the class in a union), but then you have to manually call the constructors (placement new) and destructors. The alternative is to use std::variant, which takes care of that and ensuring that your types are being accessed safely.

2 Likes

Really quick-and-painful tutorial on stuff sharing memory and going from C-style to C++:

Say you have two things and you want them to occupy the same space in RAM.
We do this with a union:

struct ThingA { int x; };
struct ThingB { double y; };

union Shared {
  ThingA a;
  ThingB b;
} shared;

With this, you can do:

shared.a.x = 1;
shared.b.y = 2;

Both structs will be in the same address, giving you type punning and all that fun Undefined Behavior.
However, when you want to read x or y, you first need to know one thing: does the union have a ThingA or a ThingB? An extra variable becomes necessary to serve as an indicator. Our code now looks like this:

enum class SharedType {
  A,
  B
} type;

union Shared {
  ThingA a;
  ThingB b;
} shared;

type = SharedType::A;
shared.a.x = 1;

type = SharedType::B;
shared.b.y = 2;

Now, if we are really careful to always update the type accordingly, we can access the contents of the union.

It would be a little less painful to use if the enum wasn’t an entirely separate variable from the union, so let’s put them together:

struct Shared {
  enum class Type {
    A,
    B
  } type;

  union {
    ThingA a;
    ThingB b;
  };
} shared;

shared.type = Shared::Type::A;
shared.a.x = 1;

This is called a “tagged union” and with it we can pass shared around to functions much more easily.

We still have the annoying problem of setting and checking the enum manually, and this is extremely error-prone. Let’s add some helper functions:

void setShared(Shared* shared, ThingA a) {
  shared->type = Shared::Type::A;
  shared->a = a;
}

ThingA* getIfThingA(Shared* shared) {
  return shared->type == Shared::Type::A ? &shared->a : nullptr;
}

void setShared(Shared* shared, ThingB b) {
  shared->type = Shared::Type::B;
  shared->b = b;
}

ThingB* getIfThingB(Shared* shared) {
  return shared->type == Shared::Type::B ? &shared->b : nullptr;
}

Shared shared;

setShared(&shared, ThingA{1});

if (auto a = getIfThingA(&shared)) {
  printf("%d", a->x);
} else if (auto b = getIfThingB(&shared)) {
  printf("%f", b->y); 
}

This is handy, but it doesn’t give you any guarantees, since you’re still free to access the properties directly. Let’s rewrite that in a way that forces users of Shared to follow its rules.

class Shared {
  enum class Type {
    A,
    B
  } type = Type::A;
  union {
    ThingA a;
    ThingB b;
  };
public:
  Shared& operator = (const ThingA& a) {
    type = Type::A;
    this->a = a;
    return *this;
  }
  Shared& operator = (const ThingB& b) {
    type = Type::B;
    this->b = b;
    return *this;
  }
  ThingA* getIfThingA() {
    return type == Type::A ? &a : nullptr;
  }
  ThingB* getIfThingB() {
    return type == Type::B ? &b : nullptr;
  }
};

Shared shared;
shared = ThingA{1};
if (auto a = shared.getIfThingA()) {
  printf("%d", a->x);
}

The code is getting wordy and there are still some important guarantees missing. What would happen if ThingA had an std::string or a FILE* or any other resource that needs to be destroyed when Shared changes from ThingA to ThingB? We need to change our operator overloads so that they clean up an initialize things properly:

Shared& operator = (const ThingA& a) {
  if (type == Type::B) {
    b.~ThingB(); // destroy ThingB
    new (&this->a) ThingA(); // use placement new to construct A
  }
  type = Type::A;
  this->a = a; // now we can safely use ThingA.
}

Now it is definitely too wordy, and it is only for two types! What if I need 10 things to be in Shared?
Templates to the rescue! They allow us to make a Shared class that can store whatever we want, without having to write all that boilerplate. Best of all, this is already done for you in C++17 and it is called std::variant.

Let’s replace Shared with an std::variant:

std::variant<ThingA, ThingB> shared; // add all the Things you want
shared = ThingA{1};
if (auto a = std::get_if<ThingA>(&shared)) {
  printf("%d", a->x);
} else if (auto b = std::get_if<ThingB>(&shared)) {
  printf("%f", b->y);
}

It can get annoying typing std::variant<ThingA, ThingB> whenever you want to refer to it, so I really recommend having an alias:

using Shared = std::variant<ThingA, ThingB>;
Shared shared;
shared = ThingA{1};

This is especially useful because later on you might want to add ThingC and an alias allows you to make that change in just one place.
There’s lots of things that can be done with a variant other than calling get_if, but this should serve as a good start.

5 Likes

What a nice answer! Kudos.

2 Likes

That is really well explained! :+1:

1 Like