Question

Is there a standard procedure for passing classes by value? In other words, if I do this:

struct Test
{
  int a;
  double b;
}

void DoSomething(Test t)
{
  std::cout << t.a << std::endl;
  std::cout << t.b << std::endl;
}

//...

Test myObj;
myObj.a = 5;
myObj.b = 10.5;

DoSomething(myObj);

Assuming standard packing and layout, does the standard provide any guarantees that the class will be sent and received in a consistent manner regardless of compiler?


Because I anticipate questions along the lines of "why do you want to do this?" or "this feels like an XY problem", here's (lengthy) context. I'm attempting to pass a class object back-and-forth between an EXE and a DLL compiled with different compilers, and it appears that the class object is not being read from the correct starting address. This problem evaporates if I pass the object by reference, however. (Bonus question - why would passing by reference work when passing by value would not? I was under the impression passing by value would copy the object and pass a reference to the copy. Clearly I'm misunderstanding something here.)

Was it helpful?

Solution

In general, ABI between different C++ compilers can vary in any way they see fit. The C++ standard does not mandate a given ABI.

However, C ABIs are extremely stable. One way to deal with this problem is to have header-only functions that translate your code into extern "C" functions, which are exported from your DLL. Inside your DLL the extern "C" functions then call a more conventional C++ interface.

For a concrete example,

struct Test;
// DLL exported:
extern "C" void Private_DoSomething_Exported( Test* );

// Interface:
namespace Interface {
  inline void DoSomething( Test t ) { return Private_DoSomething_Exported(&t); }
};

// implementation.  Using Test&& to make it clear that the reference is not an export parameter:
namespace Implementation {
  void DoSomething( Test&&t ) {
    std::cout << t.a << std::endl;
    std::cout << t.b << std::endl;
  }
}
void Private_DoSomething_Exported( Test* t ) {
  Assert(t);
  Implementation::DoSomething(std::move(*t));
}

This places the "most compatible" ABI (a pure "C" ABI) at the point where you export functions from a DLL. The client code calls Interface::DoSomething, which inline in the client code calls the "C" ABI (which doesn't even know the layout of the object), which then calls a C++ Implementation::DoSomething.

This is still not proof against every issue, because the layout of even POD structs could vary based on compilers (as a practical example, some compilers treat long as 32 bit on 64 bit machines, others treat long as 64 bits on 64 bit machines). Packing can also vary.

To reduce that impact, you'll first want to only use fixed size types from the C header files. You'll also want to examine the packing docs of both compilers.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top