3

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.)

Community
  • 1
  • 1
cf-
  • 8,598
  • 9
  • 36
  • 58
  • 4
    No, to my knowledge, the standard makes no assertion about how arguments are passed. What you're asking about is a question of ABI (Application Binary Interface) and is different across compiler vendors & versions and even hardware architectures. – Nathan Ernst Mar 27 '14 at 00:44
  • As Nathan says, standard doesn't say anything about interaction between compilers. You need to know if their ABIs are compatible. – zch Mar 27 '14 at 00:46
  • How come passing by reference works, then? Is it just a stroke of luck, or is there something I'm missing about how passing by value works? Since if passing by reference works, and passing by value implicitly uses passing by reference but fails, that makes for one very confused computerfreaker. – cf- Mar 27 '14 at 00:48
  • It you stick to C, the ABI is fixed for a given platform. This is one of the reasons why many libraries have a C interface, which wraps a C++ interface and provides functions taking the same arguments as the class function plus a pointer to the struct. – Jens Munk Mar 27 '14 at 00:49
  • Passing by reference will just pass a pointer, whereas passing by value will put the whole object on the stack and call the copy constructor, meaning the layout of memory on the stack must be compatible. – sj0h Mar 27 '14 at 00:54
  • Oh, so calling convention isn't enough to guarantee how arguments are handled with regards to the stack? Even though the class is standard-layout with fixed-location members, and the compilers agree on the calling convention (cdecl, if it matters), the class can still be stored on the stack differently between compilers? – cf- Mar 27 '14 at 01:11
  • @computerfreaker _the class can still be stored on the stack differently between compilers?_ Yes, PE format, ELF format, etc. to name just those two. – πάντα ῥεῖ Mar 27 '14 at 01:32
  • @πάνταῥεῖ This is a Windows-only question, so PE format. I've added the winapi tag to clarify. – cf- Mar 27 '14 at 01:34
  • @computerfreaker Ooops, sorry missed the tag. But AFAIR e.g. MinGw GCC still uses ELF, doesn't it? – πάντα ῥεῖ Mar 27 '14 at 01:36
  • @πάνταῥεῖ A quick bit of research indicates GCC uses PE format on Windows. No worries about the tag btw, I just added it. – cf- Mar 27 '14 at 01:41
  • @sj0h - The C++ standard doesn't say how references are implemented. A reference parameter is also not guaranteed to work across application boundaries. – PaulMcKenzie Mar 27 '14 at 02:15
  • @PaulMcKenzie So the only guaranteed way I have to pass data back and forth cross-compiler is via pointers? Or is even that not guaranteed? – cf- Mar 27 '14 at 03:15
  • @PaulMcKenzie - true. That's not meant to be prescriptive of how a reference must be passed, but just to explain why the observed compatibility behaviour of references may be different to pass by value. – sj0h Mar 27 '14 at 03:16

1 Answers1

5

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524