Incomplete Types¶
Rice supports working with incomplete (forward-declared) types. This is essential for wrapping C++ libraries that use the PIMPL idiom or opaque handle patterns, where implementation details are hidden behind forward declarations.
What Are Incomplete Types?¶
An incomplete type is a type that has been declared but not defined. The compiler knows the type exists but doesn't know its size or layout:
// Forward declaration - incomplete type
class WidgetImpl;
class Widget {
public:
// Returns pointer to incomplete type
WidgetImpl* getImpl();
// Returns reference to incomplete type
WidgetImpl& getImplRef();
private:
WidgetImpl* pImpl_; // PIMPL idiom
};
Common Patterns¶
PIMPL Idiom¶
The Private Implementation (PIMPL) idiom hides implementation details:
// widget.hpp - public header
class WidgetImpl; // Forward declaration only
class Widget {
public:
Widget();
~Widget();
void doSomething();
WidgetImpl* getImpl();
private:
WidgetImpl* pImpl_;
};
// widget.cpp - implementation (not exposed to users)
class WidgetImpl {
int value = 42;
// ... implementation details
};
Opaque Handles¶
Common in C libraries where handles are passed around without exposing internals:
// Forward declaration only - never defined in headers
struct OpaqueHandle;
OpaqueHandle* createHandle();
void destroyHandle(OpaqueHandle* handle);
int getHandleData(OpaqueHandle* handle);
Wrapping Incomplete Types¶
To wrap functions that use incomplete types, you must register the incomplete type with Rice using define_class. Rice doesn't need to know the type's size or layout - it just needs to know the type exists:
#include <rice/rice.hpp>
// Forward declaration - WidgetImpl is never defined here
class WidgetImpl;
class Widget {
public:
WidgetImpl* getImpl();
WidgetImpl& getImplRef();
};
extern "C" void Init_widget()
{
using namespace Rice;
// Register the incomplete type - Rice just needs to know it exists
define_class<WidgetImpl>("WidgetImpl");
// Now we can wrap methods that use WidgetImpl
define_class<Widget>("Widget")
.define_constructor(Constructor<Widget>())
.define_method("get_impl", &Widget::getImpl)
.define_method("get_impl_ref", &Widget::getImplRef);
}
Supported Operations¶
Rice supports incomplete types in the following contexts:
| Operation | Example | Supported |
|---|---|---|
| Return pointer | Impl* getImpl() |
Yes |
| Return reference | Impl& getImplRef() |
Yes |
| Return const reference | const Impl& getImplRef() const |
Yes |
| Parameter pointer | void setImpl(Impl* impl) |
Yes |
| Parameter reference | void process(Impl& impl) |
Yes |
| Parameter const reference | void read(const Impl& impl) |
Yes |
Error Handling¶
If you try to use an incomplete type without registering it, Rice will raise a Ruby exception:
// Without registering OpaqueHandle...
define_global_function("create_handle", &createHandle);
// Calling from Ruby will raise:
// Rice::Exception: Type is not registered with Rice: OpaqueHandle
Always register incomplete types with define_class before wrapping functions that use them.
Real-World Example: OpenCV¶
OpenCV uses the PIMPL idiom extensively. For example, cv::dnn::Net has:
namespace cv { namespace dnn {
class Net {
public:
struct Impl; // Forward declaration
Impl* getImpl() const;
Impl& getImplRef();
private:
Ptr<Impl> impl;
};
}}
To wrap this in Rice:
#include <rice/rice.hpp>
#include <opencv2/dnn.hpp>
extern "C" void Init_dnn()
{
using namespace Rice;
Module rb_mCv = define_module("Cv");
Module rb_mDnn = define_module_under(rb_mCv, "Dnn");
// Register the incomplete Impl type
define_class_under<cv::dnn::Net::Impl>(rb_mDnn, "Impl");
// Now wrap Net with its getImpl methods
define_class_under<cv::dnn::Net>(rb_mDnn, "Net")
.define_constructor(Constructor<cv::dnn::Net>())
.define_method("get_impl", &cv::dnn::Net::getImpl)
.define_method("get_impl_ref", &cv::dnn::Net::getImplRef);
}
How It Works¶
Rice handles incomplete types by:
- Storing a pointer to the object without needing to know its size
- Using type erasure to manage the Ruby VALUE wrapper
- Tracking the type in Rice's type registry for proper conversion
Since Rice only stores pointers and never copies incomplete types by value, it doesn't need the complete type definition. The actual object lifetime is managed by the C++ code that owns it.
Limitations¶
- Cannot create instances of incomplete types from Ruby (no constructor)
- Cannot copy incomplete types by value
- Cannot access members of incomplete types directly
- The incomplete type must be registered before any function using it is called
See Also¶
- Pointers - General pointer handling in Rice
- References - Reference handling in Rice
- Memory Management - Object lifetime management