6

I have a class vector

class vector3f
{
public:
float x;
float y;
float z;
};

If I want to access one of the members I have to type vec.x, vec.y and vec.z. Now, I have an algorithm that should access the data members per index, for example:

for(int i = 0; i < 3; i++)
{
vec[i] = i*i;
}

Doing this without an index would result in several if-conditions:

for(int i = 0; i < 3; i++)
{
if(i == 0){vec.x = i*i;}
else if(i == 1){vec.y = i*i;}
else if(i == 2){vec.z = i*i;}
}

Is there a way to index the data members so that they are resolved in compile-time? I don't want to use an array in the vector class because accessing the data members with vec.x, vec.y and vec.z is more convenient for the programmer. What would be the best solution to avoid these if-conditions?

Ajay
  • 18,086
  • 12
  • 59
  • 105
user3067395
  • 560
  • 6
  • 17

3 Answers3

7

You can overload operator[] for your class to localise those if conditions (or preferably a switch) into one place:

class vector3f
{
public:
  float x;
  float y;
  float z;

  float& operator[] (size_t i)
  {
    switch (i) {
      case 0: return x;
      case 1: return y;
      case 2: return z;
      default: throw "something";
    }
  }

  float operator[] (size_t i) const
  { return (*const_cast<vector3f*>(this))[i];
}

That's the generic and safe way of implementing this.

If your class is actually a standard-layout class1, as you have shown, and if you know the alignment and padding scheme of your compiler/platform, and if you know from these that the floats will just be arranged after each other in memory, you can implement the operator like this as well:

float& operator[] (size_t i)
{
  return (&x)[i];
}

This treats x as the start of an array of floats. It will be faster, but relies on the ifs I mentioned above, and does not do bounds checking.


1 Which basically means it doesn't have non-empty base classes, doesn't have virtual functions and doesn't have a data member of non-standard-layout type.

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • I think by your *"if"* paragraph you mean that the type is a [Plain-old-data type](http://en.cppreference.com/w/cpp/concept/PODType). I also don't think you need to know paddings nor ABI. If it's a POD it is guaranteed to work. I don't think binary compatibility is needed there. – luk32 Oct 31 '14 at 08:48
  • @luk32 Well, I believe that in the general case, the platform ABI can dictate the padding scheme as well (I just put it in as a safeguard). You don't need POD (trivial construction/copying), just standard-layout (as I mention). But padding in an issue - standard-layout classes can still have padding inside them, just not at the beginning. Covered by C++11 9/7+ and 9.2/17+ – Angew is no longer proud of SO Oct 31 '14 at 08:54
  • And what about `union`? something like `union{ struct{ float x, y, z; }; float i[3]; }` – Quest Oct 31 '14 at 11:39
  • 1
    @Quest Same alignment/padding caveats apply, plus it's technically UB to write to one member of a union and then read another. – Angew is no longer proud of SO Oct 31 '14 at 11:41
  • @Angew i read a gcc documentation([as posted in this answer](http://stackoverflow.com/questions/11373203/accessing-inactive-union-member-undefined/11373277#11373277)) and doc lists it under [implementation defined behavior](https://gcc.gnu.org/onlinedocs/gcc-4.7.1/gcc/Structures-unions-enumerations-and-bit_002dfields-implementation.html#Structures-unions-enumerations-and-bit_002dfields-implementation) – Quest Oct 31 '14 at 12:03
  • @Quest That basically means the same sequence of *ifs* applies. It's UB according to the standard; if a particular implementation (GCC) gives it defined behaviour, you can use it, as long as you are using that implementation. – Angew is no longer proud of SO Oct 31 '14 at 12:41
1

Not absolutely at compile time, but you can optimize.

You can use pointer-to-member-variable, and then its array. Start with:

float vector3f::*px;
px = &vector3f::x;
(vec.*px) = 10.2;

You then declare array of pointer-to-member:

float vector3f::*pArray[3];

Assign them:

pArray[0] = &vector3f::x;
pArray[1] = &vector3f::y;
pArray[2] = &vector3f::z;

Use them:

(vec.*pArray[0]) = 10.2; // This is vector3f::x
(vec.*pArray[1]) = 1042.5; // This is vector3f::y

And appropriately change the loop body to index appropriate pointer-to-member!

Ajay
  • 18,086
  • 12
  • 59
  • 105
0

An interesting alternative can be had by using references.

class vector3f
{
public:
  float &x, &y, &z;
  float value[3];

  vector3f() :
    x(value[0]), y(value[1]), z(value[2]){}
};

Here we bind x to value[0], y to value[1] and z to value[2] - they are aliases for entries in the array, but note that they do allocate an extra 4 bytes per reference in the class. This way, we can name our array for the user access convenience.

We also can avoid a runtime call to an overloaded operator [ (but a good compiler should inline that access anyway on a switch/case).

Example usage:

vector3f test;
test.value[0] = 227.f;
test.value[1] = 42.f;
test.value[2] = -1.f;
printf("Values are %lf, %lf and %lf.",test.x,test.y,test.z);

Resultant output:

Values are 227.000000, 42.000000 and -1.000000.
Pcube
  • 33
  • 6
  • 1
    The problem is that this will pretty much double the size of the structure, since the compiler *has to* allocate space for the reference members (probably implemented as pointers internally). *And* it will require the author to provide the copy/move ctors and assignment ops manually, as the defaults are no longer valid. – Angew is no longer proud of SO Oct 31 '14 at 09:21
  • Using this method I get the compiler error: "Error: implicitly generated assignment operator cannot copy: reference member: vector3f::x, ..." Is this because references cannot be re-assigned? Would I have to implement a copy constructor in this case? – user3067395 Oct 31 '14 at 09:27
  • @Angew It doesn't have to; references cannot be reseated and are mere aliases. I did a sizeof on a class w/ the refs vs without and indeed with displayed an extra 12 bytes of usage up from 24. However, this is because sizeof acting on a reference returns the size of the type of the variable it's bound to; this doesn't mean that at runtime we're actually using an extra 12 bytes. Typically accessing a reference generates code that doesn't indirect to the referred to resource. Why have runtime space for a pointer that never changes when you can just inline an offset to the enclosing structure? – Pcube Oct 31 '14 at 10:39
  • @Pcube But you were doing `sizeof` on the structure, not on a reference. So it *does* take up more bytes in memory. Also, the compiler would have to realise that it knows all the definitions of all the constructors in the class, and that they all initialise the references identically. I'm still not certain it would be allowed to optimise the references out from memory, but I am quite convinced no compiler would actually do that in practice. – Angew is no longer proud of SO Oct 31 '14 at 10:49
  • @user3067395 Yes, you have to provide them yourself (see my first comment here). Actually, I believe the copy ctor can still be defaulted, it would just do the wrong thing (initialise them to the *source* object's array). – Angew is no longer proud of SO Oct 31 '14 at 10:50
  • @Angew Hmm, well it looks like the answer is dubious at best. See [this post](http://stackoverflow.com/questions/3931167/why-reference-variable-inside-class-always-taking-4-bytes-irrespect-of-type-on). References may or may not take up runtime memory. I'm not convinced that sizeof is returning true post-compile sizes rather than sizeof each member of the class. The standard states that the sizeof a reference equals the sizeof the referenced type, so this could appear as doubling up on the bytes. sizeof a reference returning 0 wouldn't make sense. – Pcube Oct 31 '14 at 11:04
  • @Pcube Regarding `sizeof`, you can be convinced. C++11 5.3.3/2 requires it to return "the number of bytes in an object of that class including any padding required for placing objects of that type in an array." No mention at all of going member-wise. – Angew is no longer proud of SO Oct 31 '14 at 11:09
  • @Angew ... Convinced! :) I had to re-read it a few times. I was tripping up on the logic that no more space should be required to reference a member that's in the same class as said reference, which while true, 5.3.3/2 does state `sizeof`'s representation of the reference's impact on the class. Thanks for bearing with me. – Pcube Oct 31 '14 at 11:14
  • @Quest `for each member in union, sizeof(union) >= sizeof(member)` In my experience the sizeof the union has always been the sizeof the largest member of the union, though the standard allows for it to be larger. – Pcube Oct 31 '14 at 12:22
  • 1
    So doing it that way: `union{ struct{ float x, y, z; }; float i[3]; }` would result in sizeof(union) = 12bytes. That is less than 24 in your example. – Quest Oct 31 '14 at 12:28