Skip to content

Commit 22e1de1

Browse files
authored
Merge pull request #398 from ruby-rice/dev
Fix memory leak with abstract classes. Fixes #397
2 parents 971363a + ec5dcfc commit 22e1de1

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

rice/detail/Wrapper.ipp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@ namespace Rice::detail
122122
if constexpr (is_complete_v<T>)
123123
{
124124
// is_abstract_v requires a complete type, so nest inside is_complete_v.
125-
// Deleting an abstract class through a non-virtual destructor is UB.
126-
if constexpr (std::is_destructible_v<T> && !std::is_abstract_v<T>)
125+
// Deleting an abstract class through a non-virtual destructor is UB,
126+
// but it is safe if the destructor is virtual.
127+
if constexpr (std::is_destructible_v<T> && (!std::is_abstract_v<T> || std::has_virtual_destructor_v<T>))
127128
{
128129
if (this->isOwner_)
129130
{

test/test_Ownership.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,36 @@ namespace
188188
static inline MyCopyableClass* instance_ = nullptr;
189189
};
190190

191+
// Abstract class with virtual destructor - deletion through base pointer is safe
192+
class AbstractVirtualDtor
193+
{
194+
public:
195+
static inline int destructorCalls = 0;
196+
197+
static void reset()
198+
{
199+
destructorCalls = 0;
200+
}
201+
202+
virtual ~AbstractVirtualDtor()
203+
{
204+
destructorCalls++;
205+
}
206+
207+
virtual int compute() const = 0;
208+
};
209+
210+
class ConcreteChild : public AbstractVirtualDtor
211+
{
212+
public:
213+
int compute() const override { return 42; }
214+
};
215+
216+
AbstractVirtualDtor* createChild()
217+
{
218+
return new ConcreteChild();
219+
}
220+
191221
class OwnerBox;
192222

193223
class AliasBox
@@ -327,6 +357,14 @@ SETUP(Ownership)
327357
.define_constructor(Constructor<SharedFactory>())
328358
.define_method("create_owned", &SharedFactory::createOwned, Return().takeOwnership())
329359
.define_method("get_borrowed", &SharedFactory::getBorrowed);
360+
361+
define_class<AbstractVirtualDtor>("AbstractVirtualDtor")
362+
.define_method("compute", &AbstractVirtualDtor::compute);
363+
364+
define_class<ConcreteChild, AbstractVirtualDtor>("ConcreteChild");
365+
366+
define_module("AbstractVirtualDtorTest")
367+
.define_module_function("create_child", &createChild, Return().takeOwnership());
330368
}
331369

332370
TEARDOWN(Ownership)
@@ -541,3 +579,23 @@ TESTCASE(MultipleOwnerReferences)
541579
result = m.module_eval(code);
542580
ASSERT_EQUAL(Qfalse, result.value());
543581
}
582+
583+
TESTCASE(AbstractVirtualDestructor)
584+
{
585+
AbstractVirtualDtor::reset();
586+
587+
Module m = define_module("TestingModule");
588+
589+
std::string code = R"(child = AbstractVirtualDtorTest.create_child
590+
child.compute)";
591+
592+
Object result = m.module_eval(code);
593+
ASSERT_EQUAL(detail::To_Ruby<int>().convert(42), result.value());
594+
595+
// Force GC to clean up the object - destructor should be called
596+
// since AbstractVirtualDtor has a virtual destructor
597+
rb_gc_start();
598+
rb_gc_start();
599+
600+
ASSERT_EQUAL(1, AbstractVirtualDtor::destructorCalls);
601+
}

0 commit comments

Comments
 (0)