问题
The problem
I am writing a thin C++ wrapper around an object oriented C library. The idea was to automate memory management, but so far its not been very automatic. Basically when I use my wrapper classes, I get all kinds of memory access and inappropriate freeing problems.
Minimal example of C library
Lets say the C library consists of A
and B
classes, each of which have a few 'methods' associated with them:
#include <memory>
#include "cstring"
#include "iostream"
extern "C" {
typedef struct {
unsigned char *string;
} A;
A *c_newA(const char *string) {
A *a = (A *) malloc(sizeof(A)); // yes I know, don't use malloc in C++. This is a demo to simulate the C library that uses it.
auto *s = (char *) malloc(strlen(string) + 1);
strcpy(s, string);
a->string = (unsigned char *) s;
return a;
}
void c_freeA(A *a) {
free(a->string);
free(a);
}
void c_printA(A *a) {
std::cout << a->string << std::endl;
}
typedef struct {
A *firstA;
A *secondA;
} B;
B *c_newB(const char *first, const char *second) {
B *b = (B *) malloc(sizeof(B));
b->firstA = c_newA(first);
b->secondA = c_newA(second);
return b;
}
void c_freeB(B *b) {
c_freeA(b->firstA);
c_freeA(b->secondA);
free(b);
}
void c_printB(B *b) {
std::cout << b->firstA->string << ", " << b->secondA->string << std::endl;
}
A *c_getFirstA(B *b) {
return b->firstA;
}
A *c_getSecondA(B *b) {
return b->secondA;
}
}
Test the 'C lib'
void testA() {
A *a = c_newA("An A");
c_printA(a);
c_freeA(a);
// outputs: "An A"
// valgrind is happy =]
}
void testB() {
B *b = c_newB("first A", "second A");
c_printB(b);
c_freeB(b);
// outputs: "first A, second A"
// valgrind is happy =]
}
Wrapper classes for A
and B
class AWrapper {
struct deleter {
void operator()(A *a) {
c_freeA(a);
}
};
std::unique_ptr<A, deleter> aptr_;
public:
explicit AWrapper(A *a)
: aptr_(a) {
}
static AWrapper fromString(const std::string &string) { // preferred way of instantiating
A *a = c_newA(string.c_str());
return AWrapper(a);
}
void printA() {
c_printA(aptr_.get());
}
};
class BWrapper {
struct deleter {
void operator()(B *b) {
c_freeB(b);
}
};
std::unique_ptr<B, deleter> bptr_;
public:
explicit BWrapper(B *b)
: bptr_(std::unique_ptr<B, deleter>(b)) {
}
static BWrapper fromString(const std::string &first, const std::string &second) {
B *b = c_newB(first.c_str(), second.c_str());
return BWrapper(b);
}
void printB() {
c_printB(bptr_.get());
}
AWrapper getFirstA(){
return AWrapper(c_getFirstA(bptr_.get()));
}
AWrapper getSecondA(){
return AWrapper(c_getSecondA(bptr_.get()));
}
};
Wrapper tests
void testAWrapper() {
AWrapper a = AWrapper::fromString("An A");
a.printA();
// outputs "An A"
// valgrind is happy =]
}
void testBWrapper() {
BWrapper b = BWrapper::fromString("first A", "second A");
b.printB();
// outputs "first A"
// valgrind is happy =]
}
Demonstration of the problem
Great, so I move on and develop the full wrapper (lot of classes) and realise that when classes like this (i.e. aggregation relationship) are both in scope, C++ will automatically call the descructors of both classes separately, but because of the structure of the underlying library (i.e. the calls to free), we get memory problems:
void testUsingAWrapperAndBWrapperTogether() {
BWrapper b = BWrapper::fromString("first A", "second A");
AWrapper a1 = b.getFirstA();
// valgrind no happy =[
}
Valgrind output
Things I've tried
Cloning not possible
The first thing I tried was to take a copy of A
, rather than having them try to free the same A
. This, while a good idea, is not possible in my case because of the nature of the library I'm using. There is actually a catching mechanism in place so that when you create a new A
with a string its seen before, it'll give you back the same A
. See this question for my attempts at cloning A.
Custom destructors
I took the code for the C library destructors (freeA
and freeB
here) and copied them into my source code. Then I tried to modify them such that A does not get freed by B. This has partially worked. Some instances of memory problems have been resolved, but because this idea does not tackle the problem at hand (just kind of temporarily glosses over the main issue), new problems keep popping up, some of which are obscure and difficult to debug.
The question
So at last we arive at the question: How can I modify this C++ wrapper to resolve the memory problems that arise due to the interactions between the underlying C objects? Can I make better use of smart pointers? Should I abandon the C wrapper completly and just use the libraries pointers as is? Or is there a better way I haven't thought of?
Thanks in advance.
Edits: response to the comments
Since asking the previous question (linked above) I have restructed my code so that the wrapper is being developed and built in the same library as the one it wraps. So the objects are no longer opaque.
The pointers are generated from function calls to the library, which uses calloc
or malloc
to allocate.
In the real code A
is raptor_uri*
(typdef librdf_uri*
) from raptor2
and is allocated with librdf_new_uri while B
is raptor_term*
(aka librdf_node*
) and allocated with librdf_new_node_* functions. The librdf_node
has a librdf_uri
field.
Edit 2
I can also point to the line of code where the same A
is returned if its the same string. See line 137 here
回答1:
The problem is that getFirstA
and getSecondA
return instances of AWrapper
, which is an owning type. This means that when constructing an AWrapper
you're giving up the ownership of an A *
, but getFirstA
and getFirstB
don't do that. The pointers from which the returned objects are constructed are managed by a BWrapper
.
The easiest solution is that you should return an A *
instead of the wrapper class. This way you're not passing the ownership of the inner A
member. I also would recommend making the constructors taking pointers in the wrapper classes private, and having a fromPointer
static method similar to fromString
, which takes ownership of the pointer passed to it. This way you won't accidently make instances of the wrapper classes from raw pointers.
If you want to avoid using raw pointers or want to have methods on the returned objects from getFirstA
and getSecondA
you could write a simple reference wrapper, which has a raw pointer as a member.
class AReference
{
private:
A *a_ref_;
public:
explicit AReference(A *a_ref) : a_ref_(a_ref) {}
// other methods here, such as print or get
};
回答2:
You are freeing A twice
BWrapper b = BWrapper::fromString("first A", "second A");
When b goes out of scope, c_freeB
is called which also calls c_freeA
AWrapper a1 = b.getFirstA();
Wraps A with another unique_ptr, then when a1 goes out of scope it will call c_freeA
on the same A.
Note that getFirstA in BWrapper gives ownership of an A to another unique_ptr when using the AWrapper constructor.
Ways to fix this:
- Don't let B manage A memory, but since you are using a lib that won't be possible.
- Let BWrapper manage A, don't let AWrapper manage A and make sure the BWrapper exists when using AWrapper. That is, use a raw pointer in AWrapper instead of a smart pointer.
- Make a copy of A in the AWrapper(A *) constructor, for this you might want to use a function from the library.
Edit:
- shared_ptr won't work in this case because c_freeB will call c_freeA anyways.
Edit 2:
In this specific case considering the raptor lib you mentioned, you could try the following:
explicit AWrapper(A *a)
: aptr_(raptor_uri_copy(a)) {
}
assuming that A is a raptor_uri
. raptor_uri_copy(raptor_uri *)
will increase the reference count and return the same passed pointer. Then, even if raptor_free_uri
is called twice on the same raptor_uri *
it will call free only when the counter becomes zero.
来源:https://stackoverflow.com/questions/62109576/how-to-resolve-memory-related-errors-that-arise-from-interaction-between-c-objec