ROSE 0.11.145.147
|
Paradigms for allocation/deallocation of objects.
Any time an object is allocated on the heap the library needs to be clear about who owns that object, i.e., which other object(s) has a pointer to this heap-allocated object. Lack of clear ownership rules leads to either unsafe deletion caused deleting an object when something else is still using it, or memory leaks caused by users not deleting objects because they're afraid of unsafe deletion. A number of ownership paradigms exist:
The single ownership paradigm is when a heap-allocated object has exactly one owner. If the owner object is copied then the heap-allocated object must also be copied so that each owner continues to point to its own singly-owned child. This paradigm usually leads to excessive copying and is not generally used by ROSE.
The shared, no-owner paradigm is when heap-allocated objects can be referenced from multiple parent objects and none of those objects clearly own the heap-allocated object. Thus, none of the parent objects can ever safely delete the heap-allocated object because the parents generally don't know about each other. This obviously leads to memory leaks. This is the paradigm used by most of the older parts of ROSE, such as the parent and child pointers in the AST. Avoid this paradigm in new code if possible since it has been an unending source of problems in ROSE.
The shared ownership paradigm allows multiple parent objects to point to the same heap-allocated child object, and all parents share the ownership of the that child. The parents coordinate with each other so that when the last parent relinquishes ownership then the child object is automatically deleted. This is the paradigm used by most of the binary analysis parts of ROSE.
Within ROSE, shared ownership is implemented using smart pointers. A smart pointer behaves for the most part like a native C++ pointer, but performs additional functions. The binary analysis parts of ROSE use three shared pointer implementations: std::shared_ptr for most new code, boost::shared_ptr for most legacy code written before ROSE used C++11, and Sawyer::SharedPointer for situations that need higher performance.
To make the use of these three implementations more uniform, most objects name their pointer types Ptr
and ConstPtr
. Since these nested types are not available for forward class declarations when using only the "BasicTypes.h" headers, ROSE also has incomplete pointer definitions named by concatenating the class name with the word "Ptr" and "ConstPtr". For instance, the "BasicTypes.h" headers would declare Foo
, FooPtr
, and FooConstPtr
and the class definition for Foo
(probably in a header named "Foo.h") defines Foo::Ptr
and Foo::ConstPtr
. The concatenated names are usually used in other header files when the class definition is not available (only its declaration), and the qualified (or unqualified when possible) names are used everywhere else.
Objects that support shared ownership in ROSE make their normal constructors private or protected so such objects aren't accidentally allocated on the stack. Instead, they provide a public, static member function usually named "instance" that takes the place of the normal C++ constructors and returns a shared pointer to a newly allocated object. Since the destructors are called automatically, the convention in ROSE is to not document them.
When passing shared pointers as arguments to functions, the convention in ROSE is to pass them as const references. This avoids the book keeping necessary whenever shared pointers are copied. An alternative in the C++ world in general is to pass raw pointers since the caller holds a reference to the object that keeps it from being deleted, but this is suboptimal in ROSE because it splits the API between functions that take smart pointers and those that take raw pointers, making the API less consistent and more difficult for users.
Because reference counting by itself doesn't work well with ownership cycles, and since detecting ownership cycles in C++ is too invasive to the API, ROSE is designed to not use internal data structures that could introduce cycles. If you find a cycle you've found a flaw in the design. On the other hand, nothing prevents the user from creating data structures that cause ownership cycles. Any shared object that can be reached from such a cycle will never be freed until the cycle is broken by the user. One way to break these cycles is to reset one of the cycle's shared-ownership pointers, but the details of which pointer to reset is entirely the responsibility of the person that designed the cyclical data structure.