4

I have the following classs:

class SSVec
{
public:
    float values[8];
}

I have an object SSVec obj, I pass the float* pointer obj.values to another function. Elsewhere in the code, I get this float* pointer, and wand to cast it back to a SSVec* pointer.

Is this possible in a C++ standard defined behavior way? Most of the time this will work with a static cast, but my guess is it's in fact undefined behavior.

The reason for this, is the float* pointer is passed to and from a DLL, which knows nothing about SSVec. I have the guarantee that the passed pointer always point to a SSVec::value[8] object member.

The class could be more complex, but it does not derive from anything, has no virtual function, and contains only POD types. values is the first member

The question could be reformulated : are the class address and the first member address guaranteed to be the same through static_cast?

galinette
  • 8,896
  • 2
  • 36
  • 87

3 Answers3

6

It is defined behavior if SSVec is a POD type. You can statically assert this when implementing a special cast function for your type:

SSVec* SSVec_cast(float* ptr) {
    // Break if someone changes SSVec to be no POD anymore:
    static_assert(std::is_pod<SSVec>::value, "SSVec is no longer a POD!");

    // Break if someone changes SSVec to contain more than the array:
    // [ NOTE: This is optional. Behavior is still defined if the structure
    //   changes(*), but then only if the pointer really points into an SSVec.
    //   With these assertions included, you can even cast from a different
    //   float array of size 8, even if it hasn't been declared as a SSVec. ]
    static_assert(sizeof(SSVec) == 8 * sizeof(float), "SSVec has wrong size!");
    static_assert(sizeof(SSVec::values) == sizeof(SSVec), "SSVec has wrong structure!");
    static_assert(offsetof(SSVec, values) == 0, "SSVec has wrong structure!");

    // Now it is safe to reinterpret cast the pointer:
    // [ (*) NOTE: If above assertions are removed, please change (ptr)
    //   to (reinterpret_cast<char*>(ptr) - offsetof(SSVec, values)). ]
    return reinterpret_cast<SSVec*>(ptr);
}

The same can be done with const pointers by overloading; of course you can then move those assertions to some common function or in global scope (preferably).


PS: Please look into std::array. It does exactly what you want:

typedef std::array<float,8> SSVec;
leemes
  • 44,967
  • 21
  • 135
  • 183
  • Thanks for the detailed answer. This class is about SSE/AVX intrinsic optimizations while keeping portability on non SSE/AVX platforms, that's the reason why I do not use plain std::array – galinette Dec 17 '14 at 09:54
  • Would I be able to cast back the `float*` to a `std::array*` ? I cannot pass the `std::array` by pointer/reference since it crosses a DLL boundary – galinette Dec 17 '14 at 10:01
2

You may use offsetof to get pointer to the class. Some example:

#include <cstddef>
#include <iostream>

class SSVec
{
public:
    int someOtherValueThatMakeEverythingMoreComplicated;
    float values[8];
};

SSVec* getSSVeciFromValuesPointer(float* floatPointer)
{
    char* rawPointer = reinterpret_cast<char*>(floatPointer);
    char* movedPointer = rawPointer - offsetof(class SSVec, values);

    return reinterpret_cast<SSVec*>(movedPointer);
}

void callBack(float* p)
{
    std::cout << "Callback: float*: " << p << std::endl; 

    SSVec* vec = getSSVeciFromValuesPointer(p);

    std::cout << "Callback: vec*: " << vec << std::endl;
}

typedef void DummyCallback(float*);

void functionThatCanNotBeChanged(float* parameter, DummyCallback callback)
{
    std::cout << "FunctionThatCanNotBeChanged: " << parameter << std::endl;
    callback(parameter);
}

int main()
{
    SSVec vec;

    std::cout << "Vec pointer: " << &vec << std::endl;
    functionThatCanNotBeChanged(vec.values, &callBack);

    return 0;
}

The result I have got:

Vec pointer: 0x7fff85655f90
FunctionThatCanNotBeChanged: 0x7fff85655f94
Callback: float*: 0x7fff85655f94
Callback: vec*: 0x7fff85655f90
luantkow
  • 2,809
  • 20
  • 14
2

I just came across this question and the two previous answers. While the chosen answer is beautifully succinct, correct, and specific to the particulars of the question (the given class is a POD), I wanted to clarify something for folks arriving here looking for information about "C++ Casting back from member pointer to holding class pointer" as the title suggests.

Briefly, the C++11 standard split the technical definition of a POD into two separate concepts: a trivial class and a standard-layout class. Since C++11 became a standard, calling a type "POD" (or more formally a "POD struct") implies it is both trivial and standard-layout.

However, for the purposes of casting a pointer-to-object to/from a pointer-to-first-member, the important thing is that the object's type is a standard-layout class. Particularly, it doesn't matter whether or not the object's type is also a trivial class.

This means, for example, that the SSVec class in the original question could have user-defined (non-trivial): default constructors, copy/move constructors/assignment operators, and/or destructor.

I believe it is important to note this, as it means this kind of cast, as well as some other interesting casts, are defined behavior for many more types of classes than if it were restricted to a POD. The C++11 concept of standard-layout was originally sometimes informally referred to as a "relaxed" POD for this very reason.

So, the first part of the example code from the chosen answer could safely be changed to:

SSVec* SSVec_cast(float* ptr) {
    // Break if someone changes SSVec to be a non-standard-layout class:
    static_assert(std::is_standard_layout<SSVec>::value, "SSVec is no longer a standard-layout class!");
    ...

Notes:

  • The C++14 standard did not change the definition of POD struct, standard-layout class, or trivial class.

  • I haven't checked the C++17 standard, but I haven't read anywhere that it changed these definitions significantly either.

  • It cannot be stressed enough that when writing code that depends on a class being standard-layout, a static_assert is always used as the chosen answer shows; just use std::is_standard_layout instead of std::is_pod in the test.

  • Under C++17 you can use std::is_standard_layout_v<SSVec> instead of std::is_standard_layout<SSVec>::value.

See Also

C++ standard type traits at cppreference:

  • std::is_pod: both trivial and standard-layout.

  • std::is_standard_layout: a scalar type, a standard-layout class, or an array of such type/class, possibly cv-qualified

  • std::is_trivial: a scalar type, a trivially copyable class with a trivial default constructor, or array of such type/class, possibly cv-qualified.

Pseudo-concepts at cppreference:

For advanced information, these stackoverflow articles:

drifty0pine
  • 136
  • 1
  • 3