3

In some data structures, it would be useful to have members whose values are computed from the other data members upon access instead of stored.

For example, a typical rect class might store it's left, top, right and bottom coordinates in member data fields, and provide getter methods that return the computed width and height based on those values, for clients which require the relative dimensions instead of the absolute positions.

struct rect
{
   int left, top, right, bottom;

   // ...

   int get_width() const { return right - left; }
   int get_height() const { return bottom - top; }
};

This implementation allows us to get and set the absolute coordinates of the rectangles sides,

float center_y = (float)(box.top + box.bottom) / 2.0;

and additionally to get it's relative dimensions, albeit using the slightly different method-call operator expression syntax:

float aspect = (float)box.get_width() / (float)box.get_height();

The Problem

One could argue, however, that it is equally valid to store the relative width and height instead of absolute right and bottom coordinates, and require clients that need to compute the right and bottom values to use getter methods.

My Solution

In order to avoid the need to remember which case requires method call vs. data member access operator syntax, I have come up with some code that works in the current stable gcc and clang compilers. Here is a fully functional example implementation of a rect data structure:

#include <iostream>

struct rect
{
  union {
    struct {
      union { int l; int left; };
      union { int t; int top; };
      union { int r; int right; };
      union { int b; int bot; int bottom; };
    };
    struct {
      operator int() {
        return ((rect*)this)->r - ((rect*)this)->l;
      }
    } w, width;
    struct {
      operator int() {
        return ((rect*)this)->b - ((rect*)this)->t;
      }
    } h, height;
  };

  rect(): l(0), t(0), r(0), b(0) {}
  rect(int _w, int _h): l(0), t(0), r(_w), b(_h) {}
  rect(int _l, int _t, int _r, int _b): l(_l), t(_t), r(_r), b(_b) {}

  template<class OStream> friend OStream& operator<<(OStream& out, const rect& ref)
  {
    return out << "rect(left=" << ref.l << ", top=" << ref.t << ", right=" << ref.r << ", bottom=" << ref.b << ")";
  }
};

/// @brief Small test program showing that rect.w and rect.h behave like data members

int main()
{
  rect t(3, 5, 103, 30);
  std::cout << "sizeof(rect) is " << sizeof(rect) << std::endl;
  std::cout << "t is " << t << std::endl;
  std::cout << "t.w is " << t.w << std::endl;
  std::cout << "t.h is " << t.h << std::endl;

  return 0;
}

Is there anything wrong with what I am doing here?

Something about the pointer-casts in the nested empty struct types' implicit conversion operators, i.e. these lines:

return ((rect*)this)->r - ((rect*)this)->l;

feels dirty, as though I may be violating good C++ style convention. If this or some other aspect of my solution is wrong, I'd like to know what the reasoning is, and ultimately, if this is bad practice then is there a valid way to achieve the same results.

1 Answers1

2

One thing that I would normally expect to work doesn't:

auto w = t.w;

Also, one of the following lines works, the other does not:

t.l += 3;
t.w += 3; // compile error

Thus, you have not changed the fact that users need to know which members are data and which are functions.

I'd just make all of them functions. It is better encapsulation anyway. And I would prefer the full names, i.e. left, top, bottom, right, width and length. It might be a few more characters to write, but most code is read much more often than it is written. The extra few characters will pay off.

Rumburak
  • 3,416
  • 16
  • 27
  • 1
    There is also the issue that it invokes undefined behavior according to the standard (though most compilers do the right thing, and some explicitly provide the stronger guarantee) – bcrist Feb 09 '16 at 06:43
  • But the second issue, specifically that `t.w += 3; // compile error` does not compile... because *it makes no sense to assign the width of r*. Ask yourself what you would expect this line to do if it DID compile? Move the left edge, or the right edge? Or should it move both an equal amount to maintain the midpoint of the rectangle? Since the intention is ambiguous, the operation is not allowed, and the compiler is right to flag it as an error. – Victor Condino Feb 09 '16 at 06:51
  • @bcrist Can you be more specific? What part of my example and in the standard are you referring to? – Victor Condino Feb 09 '16 at 06:58
  • @VictorCondino The second thing is an issue since you require your user to know which member is data and which member is not. That is the very thing you wanted to avoid. – Rumburak Feb 09 '16 at 07:38
  • @Rumburak if you mean that `rect.width += 3` should do the same thing as it would in an implementation where `width` was stored instead of `right` and `right` was a computed member (calculated as `left + width`), it would be simple to overload assignment for rect's empty `struct { ... } width;` member, to allow assignment to `rect.width` to cause `right = left + new_width`. is this what you expect? – Victor Condino Feb 09 '16 at 11:29
  • 1
    @VictorCondino Actually, I just wanted to point out that you did not get rid of the fact that users of your class need to distinguish between data members and non-data members. That's all. My recommendation is unchanged: Make users access everything through functions. And of course, t.width() += 3 would not make sense then, but neither would t.left() += 3. It would be consistent. Consistency goes a long way. – Rumburak Feb 09 '16 at 19:36
  • 1
    The UB I was referring to is that the effective type of a union is the last member assigned a value, and accessing any other member isn't technically allowed. – bcrist Feb 10 '16 at 06:03
  • 1
    @Rumburak `t.width() += 3` could be valid if it returns a proxy object, but I agree it's a poor design choice. – bcrist Feb 10 '16 at 06:08