Skip to content

Commit 22976fc

Browse files
committed
[cppyy] Auto-downcast objects returned through smart pointers
Returning an object by raw pointer already triggers an automatic downcast to its actual (most derived) class, but returning it through a smart pointer (`std::unique_ptr`, `std::shared_ptr`) did not: the object was bound as the declared underlying type, so derived-class members were not accessible. This became more and more of a nuisance as smart pointers become more common in C++ interface. Therefore, this commit implements automatic downcasting also for returned smart pointers. Closes #16210.
1 parent 283b12f commit 22976fc

7 files changed

Lines changed: 130 additions & 2 deletions

File tree

bindings/pyroot/cppyy/CPyCppyy/src/CPPInstance.cxx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,17 @@ Cppyy::TCppType_t CPyCppyy::CPPInstance::GetSmartIsA() const
187187
return SMART_TYPE(this);
188188
}
189189

190+
//----------------------------------------------------------------------------
191+
Cppyy::TCppType_t CPyCppyy::CPPInstance::GetSmartUnderlyingType() const
192+
{
193+
// The declared underlying type of the embedded smart pointer (e.g. 'Base' for
194+
// a std::unique_ptr<Base>). This is independent of any auto-down-cast applied
195+
// to the dereferenced object, and so is what must be used to decide whether the
196+
// smart pointer can be passed to a function expecting a particular smart type.
197+
if (!IsSmart()) return (Cppyy::TCppType_t)0;
198+
return SMART_CLS(this)->fUnderlyingType;
199+
}
200+
190201
//----------------------------------------------------------------------------
191202
CPyCppyy::CI_DatamemberCache_t& CPyCppyy::CPPInstance::GetDatamemberCache()
192203
{

bindings/pyroot/cppyy/CPyCppyy/src/CPPInstance.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class CPPInstance {
7777
void SetSmart(PyObject* smart_type);
7878
void* GetSmartObject() { return GetObjectRaw(); }
7979
Cppyy::TCppType_t GetSmartIsA() const;
80+
Cppyy::TCppType_t GetSmartUnderlyingType() const;
8081

8182
// cross-inheritance dispatch
8283
void SetDispatchPtr(void*);

bindings/pyroot/cppyy/CPyCppyy/src/Converters.cxx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3066,7 +3066,12 @@ bool CPyCppyy::SmartPtrConverter::SetArg(
30663066
}
30673067

30683068
// final option, try mapping pointer types held (TODO: do not allow for non-const ref)
3069-
if (pyobj->IsSmart() && Cppyy::IsSubtype(oisa, fUnderlyingType)) {
3069+
// Note: this must be decided on the smart pointer's *declared* underlying type, not
3070+
// on the (possibly auto-down-cast) type of the dereferenced object. A
3071+
// std::unique_ptr<Base> holding a Derived must not be accepted where a
3072+
// std::unique_ptr<Derived> is expected: the held smart pointer is still a
3073+
// unique_ptr<Base> and does not convert to unique_ptr<Derived>.
3074+
if (pyobj->IsSmart() && Cppyy::IsSubtype(pyobj->GetSmartUnderlyingType(), fUnderlyingType)) {
30703075
para.fValue.fVoidp = ((CPPInstance*)pyobject)->GetSmartObject();
30713076
para.fTypeCode = 'V';
30723077
return true;

bindings/pyroot/cppyy/CPyCppyy/src/ProxyWrappers.cxx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,31 @@ PyObject* CPyCppyy::BindCppObjectNoCast(Cppyy::TCppObject_t address,
842842
PyObject* smart_type = (!(flags & CPPInstance::kNoWrapConv) && \
843843
(((CPPClass*)pyclass)->fFlags & CPPScope::kIsSmart)) ? pyclass : nullptr;
844844
if (smart_type) {
845-
pyclass = CreateScopeProxy(((CPPSmartClass*)smart_type)->fUnderlyingType);
845+
Cppyy::TCppType_t underlying = ((CPPSmartClass*)smart_type)->fUnderlyingType;
846+
847+
// Down-cast the underlying object to its actual (most derived) class, just
848+
// as BindCppObject does for raw pointers. Two conditions must hold:
849+
// * the reported actual class must really be a subtype of the declared one.
850+
// Cppyy::GetActualClass() can return a *base* class (e.g. when the actual
851+
// class has no dictionary of its own and inherits IsA() from a base with
852+
// ClassDef, ROOT reports that base) -- such an up-cast must be rejected.
853+
// * the cast must require no pointer adjustment, because the smart pointer's
854+
// dereferencer always yields a pointer to the underlying (declared) class,
855+
// so a non-zero offset can not be applied consistently on later member
856+
// access (e.g. with multiple inheritance).
857+
if (address && !isRef) {
858+
void* deref = Cppyy::CallR(
859+
((CPPSmartClass*)smart_type)->fDereferencer, address, 0, nullptr);
860+
if (deref) {
861+
Cppyy::TCppType_t clActual = Cppyy::GetActualClass(underlying, deref);
862+
if (clActual && clActual != underlying &&
863+
Cppyy::IsSubtype(clActual, underlying) &&
864+
Cppyy::GetBaseOffset(clActual, underlying, deref, -1 /* down-cast */) == 0)
865+
underlying = clActual;
866+
}
867+
}
868+
869+
pyclass = CreateScopeProxy(underlying);
846870
if (!pyclass) {
847871
// simply restore and expose as the actual smart pointer class
848872
pyclass = smart_type;

bindings/pyroot/cppyy/cppyy/test/cpp11features.cxx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@ TestSmartPtr create_TestSmartPtr_by_value() {
4040
return TestSmartPtr{};
4141
}
4242

43+
std::shared_ptr<TestSmartPtr> create_shared_ptr_to_derived() {
44+
return std::shared_ptr<TestSmartPtr>(new PubDerivedTestSmartPtr);
45+
}
46+
47+
std::unique_ptr<TestSmartPtr> create_unique_ptr_to_derived() {
48+
return std::unique_ptr<TestSmartPtr>(new PubDerivedTestSmartPtr);
49+
}
50+
51+
std::unique_ptr<TestSmartPtrIface> create_unique_ptr_to_offset_derived() {
52+
return std::unique_ptr<TestSmartPtrIface>(new MultiDerivedTestSmartPtr);
53+
}
54+
55+
int pass_unique_ptr_to_derived(std::unique_ptr<PubDerivedTestSmartPtr> p) {
56+
return p->only_in_derived();
57+
}
58+
59+
int pass_shared_ptr_to_derived(std::shared_ptr<PubDerivedTestSmartPtr> p) {
60+
return p->only_in_derived();
61+
}
62+
4363

4464
// for move ctors etc.
4565
int TestMoving1::s_move_counter = 0;

bindings/pyroot/cppyy/cppyy/test/cpp11features.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,34 @@ int move_unique_ptr_derived(std::unique_ptr<DerivedTestSmartPtr>&& p);
3636

3737
TestSmartPtr create_TestSmartPtr_by_value();
3838

39+
// for auto-downcast of objects returned through a smart pointer
40+
class PubDerivedTestSmartPtr : public TestSmartPtr {
41+
public:
42+
int only_in_derived() { return 27; }
43+
};
44+
45+
// second base so that the cross-cast to the most derived type needs a
46+
// non-zero pointer adjustment, which the smart pointer's dereferencer can
47+
// not apply consistently (so no down-cast should happen in that case)
48+
class TestSmartPtrIface {
49+
public:
50+
virtual ~TestSmartPtrIface() {}
51+
long m_pad = 0;
52+
int only_in_iface() { return 37; }
53+
};
54+
55+
class MultiDerivedTestSmartPtr : public PubDerivedTestSmartPtr, public TestSmartPtrIface {
56+
};
57+
58+
std::shared_ptr<TestSmartPtr> create_shared_ptr_to_derived();
59+
std::unique_ptr<TestSmartPtr> create_unique_ptr_to_derived();
60+
std::unique_ptr<TestSmartPtrIface> create_unique_ptr_to_offset_derived();
61+
62+
// sinks expecting a smart pointer to the *derived* type; a base-class smart
63+
// pointer (even when its object was auto-down-cast) must not be accepted here
64+
int pass_unique_ptr_to_derived(std::unique_ptr<PubDerivedTestSmartPtr> p);
65+
int pass_shared_ptr_to_derived(std::shared_ptr<PubDerivedTestSmartPtr> p);
66+
3967

4068
//===========================================================================
4169
class TestMoving1 { // for move ctors etc.

bindings/pyroot/cppyy/cppyy/test/test_cpp11features.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,45 @@ def test20_tuple_element(self):
593593

594594
cppyy.gbl.std.tuple_element[1, ATuple].type
595595

596+
def test21_smart_ptr_downcast(self):
597+
"""Object returned through a smart pointer is auto-downcast"""
598+
599+
import cppyy
600+
601+
from cppyy.gbl import TestSmartPtr, PubDerivedTestSmartPtr, TestSmartPtrIface
602+
from cppyy.gbl import create_unique_ptr_instance, create_shared_ptr_to_derived
603+
from cppyy.gbl import create_unique_ptr_to_derived, create_unique_ptr_to_offset_derived
604+
from cppyy.gbl import pass_shared_ptr, pass_unique_ptr_to_derived, pass_shared_ptr_to_derived
605+
606+
# unique_ptr<Base> holding a Derived comes back as Derived, with the
607+
# derived-only method callable, just like a raw pointer return
608+
for cf in [create_unique_ptr_to_derived, create_shared_ptr_to_derived]:
609+
obj = cf()
610+
assert type(obj) == PubDerivedTestSmartPtr
611+
assert obj.only_in_derived() == 27
612+
assert obj.__smartptr__() # smart-pointer semantics preserved
613+
614+
# an object that really is of the declared type stays that type
615+
obj = create_unique_ptr_instance()
616+
assert type(obj) == TestSmartPtr
617+
618+
# the most derived type sits at a non-zero offset from the declared
619+
# interface, which the dereferencer can not apply: stay the declared
620+
# type and keep behaving correctly
621+
obj = create_unique_ptr_to_offset_derived()
622+
assert type(obj) == TestSmartPtrIface
623+
assert obj.only_in_iface() == 37
624+
625+
# the auto-down-cast must not enable C++-invalid conversions: the proxy
626+
# still embeds a smart pointer to the *base* type, which does not convert
627+
# to a smart pointer to the derived type (no implicit down-conversion of
628+
# smart pointers in C++), so passing it to such a sink must be rejected
629+
raises(TypeError, pass_unique_ptr_to_derived, create_unique_ptr_to_derived())
630+
raises(TypeError, pass_shared_ptr_to_derived, create_shared_ptr_to_derived())
631+
632+
# passing it where the matching base smart pointer is expected still works
633+
assert pass_shared_ptr(create_shared_ptr_to_derived()) == 17
634+
596635

597636
if __name__ == "__main__":
598637
exit(pytest.main(args=['-sv', '-ra', __file__]))

0 commit comments

Comments
 (0)