A Proper Property

A continuation of Effective Implementation of C++ Properties

by Viktor Korsun

Gašper Ažman

Recap:

What's a property?

It pretends it's a field

x.foo = 5;

Assignment

int bar = x.foo;

Reading

If it quacks like a field, it's gotta be...

struct Airplane {
  int cargo;
} airplane;

... a field, right?

Except it's more like a getter and a setter.

Storage is optional

struct Airplane {
  struct Cargo {
    int operator=(int y) { 
      /* haha, ignore */
      return 0;
    }

    operator int() const {
      return 0;
    }
  };
  Cargo cargo;
} airplane;

Perfectly valid opinions

Real programmers don't use properties

It is bad design; methods do exist.

That's that Java thing, right?

I can fix my codebase with sed!

But what if I need to change a widely used bad interface?*

* or another of a million reasons they are handy, sometimes

You need properties when you want to interact with the host object

Otherwise, just use a type.

Try 1: KISS

struct Airplane {
  bool has_cargo;
  struct Cargo {
    int operator=(int y) {
      has_cargo = rand()%2;
      return 0;
    }

    operator int() const {
      return has_cargo ? rand()%5 : 0;
    }
  };
  Cargo cargo;
} airplane;

Problem: The above does not work.

Methods of Cargo need

(Airplane*) this

 

not (Cargo*) this

Try 2: KISS

Save the this pointer

struct Airplane {
  bool has_cargo;
  struct Cargo {
    int operator=(int y) {
      host->has_cargo = rand()%2;
      return 0;
    }

    operator int() const {
      return host->has_cargo ? rand()%5 : 0;
    }

    Airplane* host;

  };
  Cargo cargo;

  // constructor
  Airplane() : cargo{this} {}

} airplane;

OK, THIS WORKS

But sizeof(Airplane) is now 16 bytes!

 

Way bigger than just that bool!

Try 3: Three sheets to the wind

We could put the offset in the type of the property!

Object layout

Airplane
bool is_cargo;
Cargo cargo;

Fixed offset

(Airplane*) = (Cargo*) this - offset

... with a sprinkle of reinterpret casts to char*

Getting the offset

struct Airplane {
  bool has_cargo;
  struct Cargo {
    // ...
  };
  Cargo cargo;
  
} airplane;

static auto const cargo_offset = offsetof(Airplane, cargo);

Using the offset

Airplane* cargo_to_airplane(Cargo* cargo) {
  auto const raw_cargo = reinterpret_cast<char*>(cargo);
  auto const raw_airplane = raw_cargo - cargo_offset;
  return reinterpret_cast<Airplane*>(raw_airplane);
}
struct Airplane {
  bool has_cargo;
  struct Cargo {
    // ...
  };
  Cargo cargo;
  
} airplane;

static auto const cargo_offset = offsetof(Airplane, cargo);

... And, that's it. We can do this:

static auto const cargo_offset;

struct Airplane {
  bool has_cargo;
  struct Cargo {
    operator=(int x) {
      cargo_to_airplane(this)->has_cargo = rand()%x;
    }
  };
  Cargo cargo;
  
} airplane;

static auto const cargo_offset = offsetof(Airplane, cargo);

Damn that's ugly.

All sorts of things are wrong with it. We want this:

struct Airplane {
  bool has_cargo;
  auto set_cargo(int x) {
    return has_cargo = rand()%x;
  }
  auto get_cargo() const {
    return has_cargo;
  }
  PROPERTY(cargo, get_cargo, set_cargo);
};

To do that, we need contiguous code.

 

But offsetof() does *not* work inside the class. It needs a complete type.

Dealing with it:

struct Airplane {
  bool has_cargo;
  struct Cargo {
    operator=(int x) {
      cargo_to_airplane(this)->has_cargo = rand()%x;
    }
  };
  Cargo cargo;

  // We can't call offsetof, but we *can* do this.
  static auto constexpr cargo_address() const {
    return &Airplane::cargo;
  }
  
} airplane;

Dealing with it: member pointer to offset

template <typename Host, typename Member>
static auto constexpr offset_of(Member* cargo) {
  return reinterpret_cast<size_t>(&(Host*)0->*cargo));
}
Airplane* cargo_to_airplane(Cargo* cargo) {
  auto const raw_cargo = reinterpret_cast<char*>(cargo);
  auto const cargo_offset =
      offset_of<Airplane>(Airplane::cargo_offset());
  auto const raw_airplane = raw_cargo - cargo_offset;
  return reinterpret_cast<Airplane*>(raw_airplane);
}

These are free functions, using a method in a type-dependent context

Ok, that's cool.

 

Contiguous code, not too much badness...

Let's do MORE CARGO

struct Airplane {
  bool has_cargo;
  struct Cargo {
    operator=(int x) {
      cargo_to_airplane(this)->has_cargo = rand()%x;
    }
  };
  Cargo cargo;
  Cargo more_cargo;

  static auto constexpr cargo_address() const {
    return &Airplane::cargo;
  }
  // ... THIS IS HORRIBLE.
  static auto constexpr more_cargo_address() const {
    return &Airplane::cargo;
  }
  
} airplane;

Does not work.

There's a copy-paste bug on this slide. ->macro

Type tags to the rescue

template <typename T> type_ { using type = T; };

struct Airplane {
  bool has_cargo;

  template <typename Tag>
  struct Cargo {
    using tag = Tag;
    operator=(int x) {
      cargo_to_airplane<tag>(this)->has_cargo = rand()%x;
    }
  };
  struct cargo_tag;
  Cargo<cargo_tag> cargo;
  static auto constexpr member_address(type_<cargo_tag>) const {
    return &Airplane::cargo;
  }

  struct more_cargo_tag
  Cargo<more_cargo_tag> more_cargo;
  static auto constexpr member_address(type_<more_cargo_tag>) const {
    return &Airplane::more_cargo;
  }
} airplane;

The other side

template <typename Cargo>
Airplane* cargo_to_airplane(Cargo* cargo) {
  auto const raw_cargo = reinterpret_cast<char*>(cargo);
  auto const tag = type_<typename Cargo::tag>{};
  auto const cargo_offset =
      offset_of<Airplane>(Airplane::member_offset(tag)));
  auto const raw_airplane = raw_cargo - cargo_offset;
  return reinterpret_cast<Airplane*>(raw_airplane);
}
template <typename Host, typename Member>
static auto constexpr offset_of(Member* cargo) {
  return reinterpret_cast<size_t>(&(Host*)0->*cargo));
}

And that's it!

 

... well, for the talk.

 

 

 

Talk to me later about how to

* make copying safe

* get rid of the one byte of overhead,

* make this a single-line macro.

 

Library's here:

https://github.com/bitekas/properties

A proper property

By Gašper Ažman

A proper property

Slide deck describing how to build a no-overhead class that can serve as an emulation of object properties (fields with getters and setters) in other languages.

  • 524
Loading comments...

More from Gašper Ažman