1

I'm working my way through a C++ class template in a book I'm reading, and while most of it is clear, this particular function is really bugging me:

template <typename T> 
struct Vector3 {

T x; T y; T z;

//... several methods, constructors, etc

//Then this one, which is really confusing me:

template <typename P> 
P* Write(P* pData)
   {
    Vector3<T>* pVector = (Vector3<T>*) pData; 
    *pVector++ = *this;
    return (P*) pVector;
   }

First things first, this function appears to be treating an array of P or a pointer to P as if it can easily be casted from a pointer to a Vector3, the name of the class. How's that? If I have a Vector3<float> someVector, what makes a pointer to this someVector castable into a pointer to float ? (or even int ?) It also does the opposite: feed the function an array of floats, and then it can just cast it into an array of Vector3.

So that's the first area of my confusion.

Next is the *pVector++ = *this; -- I take it this is pointer arithmetic here, such that if pVector pointed to the second element in an array, with this expression we are incrementing the pointer to the next element of the array, but only after we first assign *this to the current element pointed at by pVector. Assuming I am correct in this, wouldn't pVector always point to the first element in the array, since it is just created on the previous line?! What's the purpose of the ++ operator in such a context?

johnbakers
  • 24,158
  • 24
  • 130
  • 258
  • 2
    It stores the contents of the Vector3 in some other place, pointed to by a pointer of *any* type. Horrible! The `++` increments the pointer to point past the stored data. – Bo Persson Apr 14 '13 at 13:14
  • I still don't understand the casting. Is it because the `Vector3` is just a set of `floats` or whatever, that it can be treated like an array of the same type? This would assume the struct's members were aligned in memory like that of an array, unless I don't understand this conversion from `Vector3` to array. – johnbakers Apr 14 '13 at 13:52

1 Answers1

5

Let's decompose how it works:

  1. This is the function declaration. The important point is that P is unrelated to T. So, we need to pay more attention, as some bad things might occur...

    template <typename P> 
    P* Write(P* pData)
    {
    

    Let's represent the memory pointed by pData. For the sake of the explanation, I will assume that pData points to a sufficiently large area in the memory (or else Write will probably result in a segfault), and that Vector3<T> is only the size of 3 Ts. In the following, I will take T = float, with sizeof(float) = 4, but my explanations will still be valid with other types. So, here sizeof(Vector3<float>) = 12.

    So, here is the memory, |-| is one byte:

    |-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
    ^
    |
    pData : P*
    
  2. We interpret pData as a pointer to a Vector3<T>. This is bad style in general, as P can be anything.

        Vector3<T>* pVector = (Vector3<T>*) pData; 
    
  3. The following line:

        *pVector++ = *this;
    

    can be divided in:

        *pVector = *this;
        pVector++;
    
    • *pVector = *this; assign the contents of the vector (*this) to the data. The memory is now:

      pVector : Vector3<float>*
      |
      v
      |X|X|X|X|Y|Y|Y|Y|Z|Z|Z|Z|-|-|-|-|-|
      ^^^^^^^^^^^^^^^^^^^^^^^^^
      copied content of the vector
      
    • pVector++; increments the vector by 1 * sizeof(Vector3<float>), so it now points to the non-yet-written memory:

                              pVector : Vector3<float>*
                              |
                              v
      |X|X|X|X|Y|Y|Y|Y|Z|Z|Z|Z|-|-|-|-|-|
      
  4. pVector is casted back to P* and returned.

        return (P*) pVector;
    }
    

    This enables to chain writes, as a write to the returned pointer will not overwrite the first write:

    char data[2 * sizeof(Vector3<float>)];
    v2.Write(v1.Write(data));
    // now data is:
    //                                                 contents of v2
    //                                     vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    // |X1|X1|X1|X1|Y1|Y1|Y1|Y1|Z1|Z1|Z1|Z1|X2|X2|X2|X2|Y2|Y2|Y2|Y2|Z2|Z2|Z2|Z2
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //          contents of v1
    

Now, some flaws:

This function can be useful for io, and is equivalent to a memcpy on the vector. However, it has a lot of flaws, the major one being the template parameter. Writing directly to memory for such operations should use char, and not something else. Writing in memory to overwrite the content of a class (ie, not io) is a very bad practice, (assignment operators are made for such a task, and they are safer). Also, the following example is not self-describing:

vec.Write<MyTimerClass>(pointer); // Does not make sense if you are reading this line for the first time

Secondly, there are problems in copying memory. Here, in the explanations, I assumed Vector3 was a simple type. It is not always the case. If it had virtual functions and members of different sizes, the layout in memory would have been implementation-defined, for example:

                 X, Y, Z            padding
        ------------------------    ----
|P|P|P|P|X|X|X|X|Y|Y|Y|Y|Z|Z|Z|Z|a|a|.|.
--------                        ----
 vtable                         some
 pointer                        other
                                member

And this is not reliable for io. See this SO question (the accepted answer is: "Here is how it works for msvc"...). And for multiple virtual inheritance it becomes a true nightmare.

By the way, the secure implementation of such a method would have been something like:

template<typename IOBuffer>
bool Write(IOBuffer& buffer)
{
    return buffer << X << Y << Z;
}

or better:

virtual bool Write(IOBufferInterface& buffer)
{
    return buffer << X << Y << Z;
}

It is way more simple to understand, maintain, debug, etc...

Community
  • 1
  • 1
Synxis
  • 9,236
  • 2
  • 42
  • 64