Skip to content

Commit c8bf4b1

Browse files
committed
[CPyCppyy] Remove pointer-based fallback in CPPInstance equality check
The Python proxy for C++ objects previously implemented a fallback equality comparison based on proxy type and the held C++ pointer address when no C++ `operator==` / `operator!=` was available. This behavior was misleading, because it made expressions like `a == b` appear to perform a value comparison even though no corresponding C++ operator was defined. This patch removes the implicit pointer-based fallback and instead raises a TypeError when equality is requested between two CPPInstance proxies for which no C++ equality operator can be resolved. This avoids silently changing semantics and makes unsupported comparisons explicit. The only case where the pointer-based fallback is kept is for proxies of the exact same type when at least one wraps a `nullptr`: in this case, comparison continues to be performed on the pointer value, because comparing by value would not be possible. This preserves existing cppyy behavior in a case that is not semantically ambiguous. Although C++ would also allow comparisons between nullptr pointers of types related by inheritance, broadening the rule would silently change previous cppyy behavior, where such comparisons returned `False`. To summarize: this change prevents ambiguous equality semantics while avoiding silent behavior changes for existing code by raising a type error in cases that are ambiguous or where previous behavior was different from C++ semantics. Closes #21347.
1 parent 7c8a73a commit c8bf4b1

8 files changed

Lines changed: 61 additions & 52 deletions

File tree

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

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -532,22 +532,6 @@ static inline PyObject* eqneq_binop(CPPClass* klass, PyObject* self, PyObject* o
532532
Py_RETURN_TRUE;
533533
}
534534

535-
static inline void* cast_actual(void* obj) {
536-
void* address = ((CPPInstance*)obj)->GetObject();
537-
if (((CPPInstance*)obj)->fFlags & CPPInstance::kIsActual)
538-
return address;
539-
540-
Cppyy::TCppType_t klass = ((CPPClass*)Py_TYPE((PyObject*)obj))->fCppType;
541-
Cppyy::TCppType_t clActual = Cppyy::GetActualClass(klass, address);
542-
if (clActual && clActual != klass) {
543-
intptr_t offset = Cppyy::GetBaseOffset(
544-
clActual, klass, address, -1 /* down-cast */, true /* report errors */);
545-
if (offset != -1) address = (void*)((intptr_t)address + offset);
546-
}
547-
548-
return address;
549-
}
550-
551535

552536
#define CPYCPPYY_ORDERED_OPERATOR_STUB(op, ometh, label) \
553537
if (!ometh) { \
@@ -592,25 +576,47 @@ static PyObject* op_richcompare(CPPInstance* self, PyObject* other, int op)
592576
result = eqneq_binop((CPPClass*)Py_TYPE(other), other, (PyObject*)self, op);
593577
if (result) return result;
594578

595-
// default behavior: type + held pointer value defines identity; if both are
596-
// CPPInstance objects, perform an additional autocast if need be
597-
bool bIsEq = false;
598-
599-
if ((Py_TYPE(self) == Py_TYPE(other) && \
600-
self->GetObject() == ((CPPInstance*)other)->GetObject())) {
601-
// direct match
602-
bIsEq = true;
603-
} else if (CPPInstance_Check(other)) {
604-
// try auto-cast match
605-
void* addr1 = cast_actual(self);
606-
void* addr2 = cast_actual(other);
607-
bIsEq = addr1 && addr2 && (addr1 == addr2);
579+
// for non-CPPInstance objects, let Python handle dispatch/fallback
580+
if (!CPPInstance_Check(other)) {
581+
Py_INCREF(Py_NotImplemented);
582+
return Py_NotImplemented;
608583
}
609584

610-
if ((op == Py_EQ && bIsEq) || (op == Py_NE && !bIsEq))
611-
Py_RETURN_TRUE;
585+
// if both proxies have the same type and at least one wraps nullptr,
586+
// comparing nullness is unambiguous
587+
auto ptr1 = self->GetObject();
588+
auto ptr2 = ((CPPInstance*)other)->GetObject();
589+
if(ptr1 == nullptr || ptr2 == nullptr) {
590+
// Take this branch only for exact proxy type matches. Broadening this
591+
// to inheritance-related types would be allowed in C++, but it would
592+
// silently change previous cppyy behavior, where equality comparison
593+
// of different-typed nullptr always resulted in `False`, straying away
594+
// from C++.
595+
if (Py_TYPE(self) == Py_TYPE(other)) {
596+
bool bIsEq = ptr1 == ptr2;
597+
if ((op == Py_EQ && bIsEq) || (op == Py_NE && !bIsEq))
598+
Py_RETURN_TRUE;
599+
Py_RETURN_FALSE;
600+
}
601+
}
612602

613-
Py_RETURN_FALSE;
603+
// in the remaining cases, the semantics of the comparison are ambiguous, so we raise an exception
604+
const char* opstr = op == Py_EQ ? "==" : "!=";
605+
const char* lhs = Py_TYPE(self)->tp_name;
606+
const char* rhs = Py_TYPE(other)->tp_name;
607+
const char *msg =
608+
"\nC++ equality operator '%s' is not defined for objects of type \"%s\" and \"%s\"."
609+
"\n\nThe Python proxy no longer falls back to comparing by type and held C++ pointer "
610+
"address, because that can be misleading: using `%s` may look like a value comparison "
611+
"even though no corresponding C++ operator is available."
612+
"\n\nSome comparisons that are well-formed in C++ are still rejected here to avoid "
613+
"changing legacy cppyy behavior silently."
614+
"\n\nDefine a suitable C++ equality operator for these operands, or compare object identity explicitly "
615+
"through another mechanism if that is what you intend."
616+
"\n";
617+
618+
PyErr_Format(PyExc_TypeError, msg, opstr, lhs, rhs, opstr);
619+
return NULL;
614620
}
615621

616622
// ordered comparison operators

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -445,10 +445,10 @@ def test09_opaque_pointer_passing(self):
445445
#assert o == cppyy.bind_object(cobj, o.__class__)
446446
#assert o == cppyy.bind_object(cobj, "some_concrete_class")
447447
assert cppyy.addressof(o) == cppyy.addressof(cppyy.bind_object(addr, some_concrete_class))
448-
assert o == cppyy.bind_object(addr, some_concrete_class)
449-
assert o == cppyy.bind_object(addr, type(o))
450-
assert o == cppyy.bind_object(addr, o.__class__)
451-
assert o == cppyy.bind_object(addr, "some_concrete_class")
448+
assert o is cppyy.bind_object(addr, some_concrete_class)
449+
assert o is cppyy.bind_object(addr, type(o))
450+
assert o is cppyy.bind_object(addr, o.__class__)
451+
assert o is cppyy.bind_object(addr, "some_concrete_class")
452452
raises(TypeError, cppyy.bind_object, addr, "does_not_exist")
453453
raises(TypeError, cppyy.bind_object, addr, 1)
454454

@@ -493,7 +493,6 @@ def test10_object_identity(self):
493493
r4 = r1.m_other
494494
assert r3 is r4
495495

496-
assert r3 == r2
497496
assert r3 is r2
498497

499498
r3.extra = 42
@@ -531,13 +530,13 @@ def test12_actual_type(self):
531530
b = base_class()
532531
d = derived_class()
533532

534-
assert b == b.cycle(b)
533+
assert b is b.cycle(b)
535534
assert id(b) == id(b.cycle(b))
536-
assert b == d.cycle(b)
535+
assert b is d.cycle(b)
537536
assert id(b) == id(d.cycle(b))
538-
assert d == b.cycle(d)
537+
assert d is b.cycle(d)
539538
assert id(d) == id(b.cycle(d))
540-
assert d == d.cycle(d)
539+
assert d is d.cycle(d)
541540
assert id(d) == id(d.cycle(d))
542541

543542
assert isinstance(b.cycle(b), base_class)

bindings/pyroot/cppyy/cppyy/test/test_datatypes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ def test09_global_ptr(self):
644644

645645
d = gbl.get_global_pod()
646646
assert gbl.is_global_pod(d)
647-
assert c == d
647+
assert c is d
648648
assert id(c) == id(d)
649649

650650
e = gbl.CppyyTestPod()
@@ -1008,8 +1008,8 @@ def test19_object_and_pointer_comparisons(self):
10081008
l1 = cppyy.bind_object(0, gbl.FourVector)
10091009
assert not l1
10101010

1011-
assert c1 != l1
1012-
assert l1 != c1
1011+
# assert c1 != l1
1012+
# assert l1 != c1
10131013

10141014
l2 = cppyy.bind_object(0, gbl.FourVector)
10151015
assert l1 == l2

bindings/pyroot/cppyy/cppyy/test/test_regression.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def test11_cobject_addressing(self):
253253
a = cppyy.gbl.CObjA()
254254
co = cppyy.ll.as_cobject(a)
255255

256-
assert a == cppyy.bind_object(co, 'CObjA')
256+
assert a is cppyy.bind_object(co, 'CObjA')
257257
assert a.m_int == 42
258258
assert cppyy.bind_object(co, 'CObjA').m_int == 42
259259

bindings/pyroot/pythonizations/test/tobject_comparisonops.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_eq(self):
1919
o = ROOT.TObject()
2020

2121
# TObject::IsEqual compares internal address
22-
self.assertTrue(o == o)
22+
self.assertTrue(o.IsEqual(o))
2323

2424
# Test comparison with no TObject
2525
self.assertFalse(o == 1)
@@ -31,7 +31,7 @@ def test_ne(self):
3131
o = ROOT.TObject()
3232

3333
# TObject::IsEqual compares internal address
34-
self.assertFalse(o != o)
34+
self.assertFalse(!o.IsEqual(o))
3535

3636
# Test comparison with no TObject
3737
self.assertTrue(o != 1)

roottest/python/basic/PyROOT_datatypetest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,6 @@ def test10_global_ptr(self):
598598

599599
d = gbl.get_global_pod()
600600
assert gbl.is_global_pod(d)
601-
assert c == d
602601
assert c is d
603602

604603
e = gbl.CppyyTestPod()

roottest/python/cpp/PyROOT_cpptests.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def test12VoidPointerPassing( self ):
230230

231231
pZ = Z.getZ(0)
232232
self.assertEqual( Z.checkAddressOfZ( pZ ), True )
233-
self.assertEqual( pZ , Z.getZ(1) )
233+
# self.assertEqual( pZ , Z.getZ(1) )
234234

235235
import array
236236
# Not supported in p2.2
@@ -302,8 +302,11 @@ def test15ObjectAndPointerComparisons( self ):
302302
l1 = MakeNullPointer( TLorentzVector )
303303
self.assertFalse( l1 )
304304

305-
self.assertNotEqual( c1, l1 )
306-
self.assertNotEqual( l1, c1 )
305+
# Forbidden by the type system
306+
with self.assertRaises(TypeError):
307+
self.assertNotEqual( c1, l1 )
308+
with self.assertRaises(TypeError):
309+
self.assertNotEqual( l1, c1 )
307310

308311
l2 = MakeNullPointer( TLorentzVector )
309312
self.assertEqual( l1, l2 )

roottest/python/regression/PyROOT_regressiontests.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,9 @@ def test1IterateWithBaseIterator( self ):
360360

361361
b = a.begin()
362362
e = a.end()
363-
self.assertNotEqual( a, e )
363+
# This comparison is forbidden by the type system
364+
with self.assertRaises(TypeError):
365+
a == e
364366

365367
b.__preinc__()
366368
self.assertEqual( b, e )

0 commit comments

Comments
 (0)