Using C++ struct with virtual members (i.e. non-POD) in C

Viewed 2234

In questions such as this, compatibility between C++ classes/structs and C structs is explained as possible as long as all members are of the same type, in the same order, and no virtual members are declared.

That's my problem. I have virtual methods, and I would very much like to keep them when manipulating the struct in C++.

Let's examine this toy example. It's meant to be a C and C++ compatible struct defined in a single header file.

mystr.h:

#ifdef __cplusplus
#include <string>
struct mystr_base {
    virtual ~mystr_base() {}

    virtual std::string toString() = 0;
};
#endif

#ifdef __cplusplus
extern "C" {
#endif

struct mystr
#ifdef __cplusplus
: public mystr_base
#endif
{
    const char* data;

#ifdef __cplusplus
    std::string toString() {
        return std::string(data);
    }
#endif
};

#ifdef __cplusplus
}
#endif

This may not be exactly pretty, but will do for the example. In a real scenario, the C and C++ variants may be in separate headers, with the C++ struct extending a POD struct. Regardless of implementation, the issue of alignment is still present.

With this example, if a C program was written that passes an instance of mystr to a C++ function, the vtable will interfere with the alignment:

test.h:

#include "mystr.h"

#ifdef __cplusplus
extern "C"
#endif
void mycxxfunc(struct mystr str);

test.cpp:

#include <stdio.h>
#include "test.h"

void mycxxfunc(mystr str) {
    printf("mystr: %s\n", str.data);
}

main.c:

#include "test.h"

int main(int argc, char** argv) {
    const char* testString = "abc123";
    struct mystr str;
    str.data = testString;
    mycxxfunc(str);
}

$ g++ -c test.cpp && gcc main.c test.o
$ ./a.out
Segmentation fault (core dumped)

(presuming that this is because the C++ function is attempting to read data from beyond the end of the struct's allocated memory)

What's the best way to enable this C-C++ interoperability while still retaining the ability to use virtual functions in C++?

1 Answers

I do not recommend you to clutter your header file with #ifdefs.

The first thing you should do in this case if you want to retain some kind of virtualization and C compatibility at the same time is:

  1. Make your C++ and C type an opaque pointer to the representation.
  2. Put implementation details in a .cpp file.

An idea follows.

Header file:

struct MyStrImpl;

struct MyStr {
   MyStrImpl * impl;
};

extern "C" MyReturnType myFunction(MyStr myStr);

Implementation in .cpp file:

struct MyCppString {
    virtual ...
};

#ifdef __cplusplus
struct MyStrImpl : public MyCppString {

};
#else
struct MyStrImpl {

};
#endif
MyStr::MyStr() : impl{new MyStrImpl{}} {

}

This way you have a type that can be used from both C and C++.

Advantages:

  • No #ifdef in header file.
  • Compatible with C/C++ and consumable from both languages.

Disadvantages:

  • Lose overloading due to extern "C".
  • Must use a C-style interface (but can implement with C++-like with virtual functions interface in your .cpp file.

You cannot have both a C-compatible type and virtual functions in a header file at the same time without cluttering it with #ifdef, something that I do not recommend since it can be inconvenient if you need to refactor code.

Related