3

I'm writing code that passes around a lot of multi-parameter variables. For example, I might pass an "orientation," which is six doubles (three Cartesian coordinates and three rotations about the axes). It would be reasonable to define an Orientation struct and use that as an argument type. However, due to API limitations, these parameters must be stored as, and often passed to functions as, pointers to arrays of parameters:

// The sane version, using a struct
double distance_from_origin(const Orientation & o) {
  return sqrt(o.x * o.x + o.y * o.y + o.z * o.z);
}

// The version I must write due to API constraints
double distance_from_origin(const double * const p) {
  return sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
}

Obviously, this is error-prone. I have three potential solutions, with one favorite.

Solution 1

I can use #define or const globals, in a header somewhere, to alias names to indexes.

const size_t x = 0;
const size_t y = 1;
const size_t z = 2;

double distance_from_origin(const double * const p) {
  return sqrt(p[x] * p[x] + p[y] * p[y] + p[z] * p[z]);
}

This makes sure x is always consistent, but pollutes the global namespace. I could hide it in a namespace, but then it's more awkward to use.

Solution 2

An idea previously mentioned here:

struct Orientation {double x, y, z, rot_x, rot_y, rot_z};

Orientation& asOrientation(double * p) {
  return *reinterpret_cast<Orientation*>(p);
}

double distance_from_origin(const double * const p) {
  Orientation& o = asOrientation(p)  
  return sqrt(o.x * o.x + o.y * o.y + o.z * o.z);
}

This has nicer syntax, but relies on the rules of C/C++ struct packing. I think that it's safe as long as Orientation is a POD. I'm nervous about relying on that.

Solution 3

struct Orientation {
  Orientation(double * p): x{p[0]}, y{p[1]}, z{p[2]}, rot_x{p[3]}, 
    rot_y{p[4]}, rot_z{p[5]} {}

  double &x, &y, &z, &rot_x, &rot_y, &rot_z
};

double distance_from_origin(const double * const p) {
  Orientation o{p};
  return sqrt(o.x * o.x + o.y * o.y + o.z * o.z);
}

This no longer relies on struct-packing rules, and has nice syntax. However, it relies on compiler optimizations to ensure that it has zero overhead.

Solution 4

Based on this comment by GManNickG.

constexpr double& x(double * p) {return p[0];}
constexpr double& y(double * p) {return p[1];}
constexpr double& z(double * p) {return p[2];}
// ... etc.

double distance_from_origin(const double * const p) {
  return sqrt(x(p) * x(p) + y(p) * y(p) + z(p) * z(p));
}

Questions

  1. Solution 3 seems like the best to me. Does it have a potential downside that I'm missing, beyond reliance on compiler optimization?

  2. Is there another solution that's superior to any of these three?

Community
  • 1
  • 1
sw001
  • 31
  • 4
  • Is just using the indices really that error prone? – GManNickG Oct 08 '16 at 19:15
  • Most of the time, it's not difficult to get the indices correct. But, the actual parameters are slightly more complex than my examples. Also, the math is _far_ more complicated. Troubleshooting is a lot easier when you're tracing through matrix multiplications that use `orientation.x` instead of `p[0]`. – sw001 Oct 08 '16 at 19:48
  • 2
    Something to consider is just a free function: `double x(const double* const p) { return p[0]; }` and `x(p)`. – GManNickG Oct 08 '16 at 20:10
  • Nice, this is a fourth solution. It looks like Haskell record access. A slightly modified `constexpr double& x(double * p) {return p[0];}` is also [completely optimized away by the compiler](https://godbolt.org/g/R8MnVY), and allows modification of the original values. It will look slightly strange to many C++ programmers, I think. – sw001 Oct 08 '16 at 20:36
  • 1
    It should look perfectly normal to any C++ programmer. :) You should prefer free functions to member functions. It might look weird to Java programmers who think they're C++ programmers. – GManNickG Oct 08 '16 at 20:40

2 Answers2

1

Firstly, I wouldn't discount just copying the parameters from the array into a simple struct. Copying 6 doubles into a struct will be very quick.

Otherwise, I suggest wrapping the array in a class and expose the parameters as member functions:

class Orientation {
    const double *p_;
public:
    Orientation(const double *p) : p_(p) {}
    double x() const { return p_[0]; }
    double y() const { return p_[1]; }
    double z() const { return p_[2]; }
    double rot_x() const { return p_[3]; }
    double rot_y() const { return p_[4]; }
    double rot_z() const { return p_[5]; }
};

With your Solution 3 I doubt a compiler can optimize the size of your Orientation struct, it will have the size to contain 6 references. With Solution 3 it will not be assignable due to the references.

Chris Drew
  • 14,926
  • 3
  • 34
  • 54
  • Check the link I posted under Solution 3. Compiling with g++ using `-O3` entirely eliminates the struct: reads/writes from struct members are replaced directly by array access. – sw001 Oct 08 '16 at 19:51
  • @swarn OK, I stand corrected! It never ceases to amaze me what optimizers are capable of. – Chris Drew Oct 08 '16 at 19:55
  • That's a pretty good answer. Keep the data as an array, and use access functions. – Malcolm McLean Oct 08 '16 at 20:54
  • Solution 3 has no default assignment operator, but that's OK, one can be written. I'd probably do the same for this answer; the default shallow pointer copy is probably not what's desired. – sw001 Oct 08 '16 at 21:25
  • @swarn No, an assignment operator cannot be written. References cannot be reseated. I was assuming my wrapper solution would be non-owning in which case a shallow pointer copy would be exactly what is desired. If it is owning then it should not be a raw pointer. – Chris Drew Oct 09 '16 at 03:45
1

Don't try to reinterpet an array of doubles as a struct. Of course it will work, but it's not safe, and its confusing. The function signature is perfectly well-defined, it takes an array of six doubles, of which the first three are x, y, and z and the second three are Euler angles. So that's your interface.

/*
   get distance of an orientation from an origin

  Params: p[0] = x, p[1] = y, p[2] = z, p[4].p[4],p[6] Euler angles (unused)
  Returns: Euclidean distance of x,y,z from origin. 
*/
double distance_for_origin(const double *p);

No problem here, it's a bit hard to call but that's what you have to live with.

Now how to implement it? You've got several choices, depending on how many of these "orientation" structures you have in the code. If you've only one or two

double distance_from_origin(const double *p)
{
   double x = p[0];
   double y = p[1];
   double z = p[2];

   return sqrt(x*x + y*y + z*z);
}

This is fine, even the worst optimiser in the world will optimise out the assignments if for some reason it runs out of registers.

The question is however, when and where will this interface be likely to break? Let's say we go to describing orientations with quaternions. Now of course you'll have seven doubles, x,y,z, and normal vector, and an angle round that normal vector. Quite likely that someone will want to do that.

But in that case, who would take that decision, and what is the process for updating the code? If you are using an API provided by Megacorp, then only Megacorp can take the decision to go to quaternions, and probably only in formal release of a new version of the API. If you wrote the code yourself, presumably you yourself decided on Euler angles rather the quaternions and you might even change the representation in response to this reply.

That's the real issue. Because you've stripped out type information, the compiler can't help you, so you need to plan for it breaking.

Malcolm McLean
  • 6,258
  • 1
  • 17
  • 18
  • Nice answer. You're right that, reducing the parameters to an array of doubles, type information is lost, and there's only so much one can do about it. – sw001 Oct 08 '16 at 21:06
  • Which means that, if I construct the `Orientation` from the `double * p` parameter, the type system will not save me if I passed a an array of doubles that was defined using a rotation matrix. But at least it can make sure that `x` is accessed consistently for all `Orientations`, and make it a little easier to see that I was expecting an `Orientation` when the calling function passed a `MatrixOrientation` cast to a `double * p`. – sw001 Oct 08 '16 at 21:19
  • The OP seems to want to micro-optimise. Whilst my solution is fine for the specific problem, it won't scale up to large structures and complex algorithms whist still suppressing copies (but how much benefit would you get from micro-optimising then anyway?) – Malcolm McLean Oct 08 '16 at 21:43
  • You're right, I do want to micro-optimize :). What I'm really doing is creating many functors to pass to a least-squares solving library. The functors must take raw arrays of parameters; I'm looking for zero-overhead ways of adding some semantic info back to the arrays, with a readable syntax. My hope is only to make the code a little clearer, a little more DRY, and a tiny bit safer (true safety impossible in this case), with no performance cost. – sw001 Oct 08 '16 at 22:04