From 3a4c15bb9815b6f4652621fe6043ae18e0d202b3 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 22 Jan 2022 18:56:11 +0100 Subject: [PATCH] bpo-46417: Cleanup typeobject.c code (GH-30795) * Add comment to recurse_down_subclasses() explaining why it's safe to use a borrowed reference to tp_subclasses. * remove_all_subclasses() no longer accept NULL cases * type_set_bases() now relies on the fact that new_bases is not NULL. * type_dealloc_common() avoids PyErr_Fetch/PyErr_Restore if tp_bases is NULL. * remove_all_subclasses() makes sure that no exception is raised. * Don't test at runtime if tp_mro only contains types: rely on _PyType_CAST() assertion for that. * _PyStaticType_Dealloc() no longer clears tp_subclasses which is already NULL. * mro_hierarchy() avoids calling _PyType_GetSubclasses() if tp_subclasses is NULL. Coding style: * Use Py_NewRef(). * Add braces and move variable declarations to the first variable assignement. * Rename a few variables and parameters to use better names. --- Objects/typeobject.c | 325 ++++++++++++++++++++++--------------------- 1 file changed, 165 insertions(+), 160 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index b3c305e0bf4..bf62b538925 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -328,24 +328,26 @@ PyType_Modified(PyTypeObject *type) We don't assign new version tags eagerly, but only as needed. */ - PyObject *raw, *ref; - Py_ssize_t i; - - if (!_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) + if (!_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) { return; + } - raw = type->tp_subclasses; - if (raw != NULL) { - assert(PyDict_CheckExact(raw)); - i = 0; - while (PyDict_Next(raw, &i, NULL, &ref)) { + PyObject *subclasses = type->tp_subclasses; + if (subclasses != NULL) { + assert(PyDict_CheckExact(subclasses)); + + Py_ssize_t i = 0; + PyObject *ref; + while (PyDict_Next(subclasses, &i, NULL, &ref)) { assert(PyWeakref_CheckRef(ref)); - ref = PyWeakref_GET_OBJECT(ref); - if (ref != Py_None) { - PyType_Modified(_PyType_CAST(ref)); + PyObject *obj = PyWeakref_GET_OBJECT(ref); + if (obj == Py_None) { + continue; } + PyType_Modified(_PyType_CAST(obj)); } } + type->tp_flags &= ~Py_TPFLAGS_VALID_VERSION_TAG; type->tp_version_tag = 0; /* 0 is not a valid version tag */ } @@ -409,13 +411,12 @@ assign_version_tag(struct type_cache *cache, PyTypeObject *type) must first be done on all super classes. Return 0 if this cannot be done, 1 if Py_TPFLAGS_VALID_VERSION_TAG. */ - Py_ssize_t i, n; - PyObject *bases; - - if (_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) + if (_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) { return 1; - if (!_PyType_HasFeature(type, Py_TPFLAGS_READY)) + } + if (!_PyType_HasFeature(type, Py_TPFLAGS_READY)) { return 0; + } if (next_version_tag == 0) { /* We have run out of version numbers */ @@ -424,9 +425,9 @@ assign_version_tag(struct type_cache *cache, PyTypeObject *type) type->tp_version_tag = next_version_tag++; assert (type->tp_version_tag != 0); - bases = type->tp_bases; - n = PyTuple_GET_SIZE(bases); - for (i = 0; i < n; i++) { + PyObject *bases = type->tp_bases; + Py_ssize_t n = PyTuple_GET_SIZE(bases); + for (Py_ssize_t i = 0; i < n; i++) { PyObject *b = PyTuple_GET_ITEM(bases, i); if (!assign_version_tag(cache, _PyType_CAST(b))) return 0; @@ -679,7 +680,7 @@ static void remove_all_subclasses(PyTypeObject *type, PyObject *bases); static void update_all_slots(PyTypeObject *); typedef int (*update_callback)(PyTypeObject *, void *); -static int update_subclasses(PyTypeObject *type, PyObject *name, +static int update_subclasses(PyTypeObject *type, PyObject *attr_name, update_callback callback, void *data); static int recurse_down_subclasses(PyTypeObject *type, PyObject *name, update_callback callback, void *data); @@ -718,30 +719,33 @@ mro_hierarchy(PyTypeObject *type, PyObject *temp) } Py_XDECREF(old_mro); - /* Obtain a copy of subclasses list to iterate over. + // Avoid creating an empty list if there is no subclass + if (type->tp_subclasses != NULL) { + /* Obtain a copy of subclasses list to iterate over. - Otherwise type->tp_subclasses might be altered - in the middle of the loop, for example, through a custom mro(), - by invoking type_set_bases on some subclass of the type - which in turn calls remove_subclass/add_subclass on this type. + Otherwise type->tp_subclasses might be altered + in the middle of the loop, for example, through a custom mro(), + by invoking type_set_bases on some subclass of the type + which in turn calls remove_subclass/add_subclass on this type. - Finally, this makes things simple avoiding the need to deal - with dictionary iterators and weak references. - */ - PyObject *subclasses = _PyType_GetSubclasses(type); - if (subclasses == NULL) { - return -1; - } - - Py_ssize_t n = PyList_GET_SIZE(subclasses); - for (Py_ssize_t i = 0; i < n; i++) { - PyTypeObject *subclass = _PyType_CAST(PyList_GET_ITEM(subclasses, i)); - res = mro_hierarchy(subclass, temp); - if (res < 0) { - break; + Finally, this makes things simple avoiding the need to deal + with dictionary iterators and weak references. + */ + PyObject *subclasses = _PyType_GetSubclasses(type); + if (subclasses == NULL) { + return -1; } + + Py_ssize_t n = PyList_GET_SIZE(subclasses); + for (Py_ssize_t i = 0; i < n; i++) { + PyTypeObject *subclass = _PyType_CAST(PyList_GET_ITEM(subclasses, i)); + res = mro_hierarchy(subclass, temp); + if (res < 0) { + break; + } + } + Py_DECREF(subclasses); } - Py_DECREF(subclasses); return res; } @@ -749,14 +753,12 @@ mro_hierarchy(PyTypeObject *type, PyObject *temp) static int type_set_bases(PyTypeObject *type, PyObject *new_bases, void *context) { - int res = 0; - PyObject *temp; - PyObject *old_bases; - PyTypeObject *new_base, *old_base; - Py_ssize_t i; - - if (!check_set_special_type_attr(type, new_bases, "__bases__")) + // Check arguments + if (!check_set_special_type_attr(type, new_bases, "__bases__")) { return -1; + } + assert(new_bases != NULL); + if (!PyTuple_Check(new_bases)) { PyErr_Format(PyExc_TypeError, "can only assign tuple to %s.__bases__, not %s", @@ -769,7 +771,8 @@ type_set_bases(PyTypeObject *type, PyObject *new_bases, void *context) type->tp_name); return -1; } - for (i = 0; i < PyTuple_GET_SIZE(new_bases); i++) { + Py_ssize_t n = PyTuple_GET_SIZE(new_bases); + for (Py_ssize_t i = 0; i < n; i++) { PyObject *ob = PyTuple_GET_ITEM(new_bases, i); if (!PyType_Check(ob)) { PyErr_Format(PyExc_TypeError, @@ -789,39 +792,42 @@ type_set_bases(PyTypeObject *type, PyObject *new_bases, void *context) below), which in turn may cause an inheritance cycle through tp_base chain. And this is definitely not what you want to ever happen. */ - (base->tp_mro != NULL && type_is_subtype_base_chain(base, type))) { - + (base->tp_mro != NULL && type_is_subtype_base_chain(base, type))) + { PyErr_SetString(PyExc_TypeError, "a __bases__ item causes an inheritance cycle"); return -1; } } - new_base = best_base(new_bases); + // Compute the new MRO and the new base class + PyTypeObject *new_base = best_base(new_bases); if (new_base == NULL) return -1; - if (!compatible_for_assignment(type->tp_base, new_base, "__bases__")) + if (!compatible_for_assignment(type->tp_base, new_base, "__bases__")) { return -1; + } - Py_INCREF(new_bases); - Py_INCREF(new_base); + PyObject *old_bases = type->tp_bases; + assert(old_bases != NULL); + PyTypeObject *old_base = type->tp_base; - old_bases = type->tp_bases; - old_base = type->tp_base; + type->tp_bases = Py_NewRef(new_bases); + type->tp_base = (PyTypeObject *)Py_NewRef(new_base); - type->tp_bases = new_bases; - type->tp_base = new_base; - - temp = PyList_New(0); - if (temp == NULL) + PyObject *temp = PyList_New(0); + if (temp == NULL) { goto bail; - if (mro_hierarchy(type, temp) < 0) + } + if (mro_hierarchy(type, temp) < 0) { goto undo; + } Py_DECREF(temp); /* Take no action in case if type->tp_bases has been replaced through reentrance. */ + int res; if (type->tp_bases == new_bases) { /* any base that was in __bases__ but now isn't, we need to remove |type| from its tp_subclasses. @@ -834,6 +840,9 @@ type_set_bases(PyTypeObject *type, PyObject *new_bases, void *context) res = add_all_subclasses(type, new_bases); update_all_slots(type); } + else { + res = 0; + } Py_DECREF(old_bases); Py_DECREF(old_base); @@ -842,7 +851,8 @@ type_set_bases(PyTypeObject *type, PyObject *new_bases, void *context) return res; undo: - for (i = PyList_GET_SIZE(temp) - 1; i >= 0; i--) { + n = PyList_GET_SIZE(temp); + for (Py_ssize_t i = n - 1; i >= 0; i--) { PyTypeObject *cls; PyObject *new_mro, *old_mro = NULL; @@ -1413,8 +1423,9 @@ subtype_dealloc(PyObject *self) and if self is tracked at that point, it will look like trash to GC and GC will try to delete self again. */ - if (type->tp_weaklistoffset && !base->tp_weaklistoffset) + if (type->tp_weaklistoffset && !base->tp_weaklistoffset) { PyObject_ClearWeakRefs(self); + } if (type->tp_del) { _PyObject_GC_TRACK(self); @@ -1929,20 +1940,14 @@ pmerge(PyObject *acc, PyObject **to_merge, Py_ssize_t to_merge_size) static PyObject * mro_implementation(PyTypeObject *type) { - PyObject *result; - PyObject *bases; - PyObject **to_merge; - Py_ssize_t i, n; - if (!_PyType_IsReady(type)) { if (PyType_Ready(type) < 0) return NULL; } - bases = type->tp_bases; - assert(PyTuple_Check(bases)); - n = PyTuple_GET_SIZE(bases); - for (i = 0; i < n; i++) { + PyObject *bases = type->tp_bases; + Py_ssize_t n = PyTuple_GET_SIZE(bases); + for (Py_ssize_t i = 0; i < n; i++) { PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(bases, i)); if (base->tp_mro == NULL) { PyErr_Format(PyExc_TypeError, @@ -1959,13 +1964,14 @@ mro_implementation(PyTypeObject *type) */ PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(bases, 0)); Py_ssize_t k = PyTuple_GET_SIZE(base->tp_mro); - result = PyTuple_New(k + 1); + PyObject *result = PyTuple_New(k + 1); if (result == NULL) { return NULL; } + Py_INCREF(type); PyTuple_SET_ITEM(result, 0, (PyObject *) type); - for (i = 0; i < k; i++) { + for (Py_ssize_t i = 0; i < k; i++) { PyObject *cls = PyTuple_GET_ITEM(base->tp_mro, i); Py_INCREF(cls); PyTuple_SET_ITEM(result, i + 1, cls); @@ -1986,20 +1992,19 @@ mro_implementation(PyTypeObject *type) linearization implied by a base class. The last element of to_merge is the declared tuple of bases. */ - - to_merge = PyMem_New(PyObject *, n + 1); + PyObject **to_merge = PyMem_New(PyObject *, n + 1); if (to_merge == NULL) { PyErr_NoMemory(); return NULL; } - for (i = 0; i < n; i++) { + for (Py_ssize_t i = 0; i < n; i++) { PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(bases, i)); to_merge[i] = base->tp_mro; } to_merge[n] = bases; - result = PyList_New(1); + PyObject *result = PyList_New(1); if (result == NULL) { PyMem_Free(to_merge); return NULL; @@ -2010,8 +2015,8 @@ mro_implementation(PyTypeObject *type) if (pmerge(result, to_merge, n + 1) < 0) { Py_CLEAR(result); } - PyMem_Free(to_merge); + return result; } @@ -2651,20 +2656,20 @@ type_new_slots_bases(type_new_ctx *ctx) (ctx->may_add_weak && ctx->add_weak == 0))) { for (Py_ssize_t i = 0; i < nbases; i++) { - PyObject *base = PyTuple_GET_ITEM(ctx->bases, i); - if (base == (PyObject *)ctx->base) { + PyObject *obj = PyTuple_GET_ITEM(ctx->bases, i); + if (obj == (PyObject *)ctx->base) { /* Skip primary base */ continue; } - PyTypeObject *type = _PyType_CAST(base); + PyTypeObject *base = _PyType_CAST(obj); if (ctx->may_add_dict && ctx->add_dict == 0 && - type->tp_dictoffset != 0) + base->tp_dictoffset != 0) { ctx->add_dict++; } if (ctx->may_add_weak && ctx->add_weak == 0 && - type->tp_weaklistoffset != 0) + base->tp_weaklistoffset != 0) { ctx->add_weak++; } @@ -3739,8 +3744,8 @@ _PyType_GetModuleByDef(PyTypeObject *type, struct PyModuleDef *def) // to check i < PyTuple_GET_SIZE(mro) at the first loop iteration. assert(PyTuple_GET_SIZE(mro) >= 1); - Py_ssize_t i = 0; - do { + Py_ssize_t n = PyTuple_GET_SIZE(mro); + for (Py_ssize_t i = 0; i < n; i++) { PyObject *super = PyTuple_GET_ITEM(mro, i); // _PyType_GetModuleByDef() must only be called on a heap type created // by PyType_FromModuleAndSpec() or on its subclasses. @@ -3753,8 +3758,7 @@ _PyType_GetModuleByDef(PyTypeObject *type, struct PyModuleDef *def) if (module && _PyModule_GetDef(module) == def) { return module; } - i++; - } while (i < PyTuple_GET_SIZE(mro)); + } PyErr_Format( PyExc_TypeError, @@ -3770,10 +3774,7 @@ _PyType_GetModuleByDef(PyTypeObject *type, struct PyModuleDef *def) static PyObject * find_name_in_mro(PyTypeObject *type, PyObject *name, int *error) { - Py_ssize_t i, n; - PyObject *mro, *res, *base, *dict; Py_hash_t hash; - if (!PyUnicode_CheckExact(name) || (hash = ((PyASCIIObject *) name)->hash) == -1) { @@ -3785,8 +3786,7 @@ find_name_in_mro(PyTypeObject *type, PyObject *name, int *error) } /* Look in tp_dict of types in MRO */ - mro = type->tp_mro; - + PyObject *mro = type->tp_mro; if (mro == NULL) { if ((type->tp_flags & Py_TPFLAGS_READYING) == 0) { if (PyType_Ready(type) < 0) { @@ -3801,20 +3801,19 @@ find_name_in_mro(PyTypeObject *type, PyObject *name, int *error) } } - res = NULL; + PyObject *res = NULL; /* Keep a strong reference to mro because type->tp_mro can be replaced during dict lookup, e.g. when comparing to non-string keys. */ Py_INCREF(mro); - assert(PyTuple_Check(mro)); - n = PyTuple_GET_SIZE(mro); - for (i = 0; i < n; i++) { - base = PyTuple_GET_ITEM(mro, i); - assert(PyType_Check(base)); - dict = _PyType_CAST(base)->tp_dict; + Py_ssize_t n = PyTuple_GET_SIZE(mro); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *base = PyTuple_GET_ITEM(mro, i); + PyObject *dict = _PyType_CAST(base)->tp_dict; assert(dict && PyDict_Check(dict)); res = _PyDict_GetItem_KnownHash(dict, name, hash); - if (res != NULL) + if (res != NULL) { break; + } if (PyErr_Occurred()) { *error = -1; goto done; @@ -4066,10 +4065,12 @@ _PyDictKeys_DecRef(PyDictKeysObject *keys); static void type_dealloc_common(PyTypeObject *type) { - PyObject *tp, *val, *tb; - PyErr_Fetch(&tp, &val, &tb); - remove_all_subclasses(type, type->tp_bases); - PyErr_Restore(tp, val, tb); + if (type->tp_bases != NULL) { + PyObject *tp, *val, *tb; + PyErr_Fetch(&tp, &val, &tb); + remove_all_subclasses(type, type->tp_bases); + PyErr_Restore(tp, val, tb); + } PyObject_ClearWeakRefs((PyObject *)type); } @@ -4089,7 +4090,7 @@ _PyStaticType_Dealloc(PyTypeObject *type) Py_CLEAR(type->tp_bases); Py_CLEAR(type->tp_mro); Py_CLEAR(type->tp_cache); - Py_CLEAR(type->tp_subclasses); + // type->tp_subclasses is NULL type->tp_flags &= ~Py_TPFLAGS_READY; } @@ -4154,6 +4155,7 @@ _PyType_GetSubclasses(PyTypeObject *self) continue; } assert(PyType_Check(obj)); + if (PyList_Append(list, obj) < 0) { Py_DECREF(list); return NULL; @@ -6227,7 +6229,7 @@ type_ready_mro(PyTypeObject *type) Py_ssize_t n = PyTuple_GET_SIZE(mro); for (Py_ssize_t i = 0; i < n; i++) { PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(mro, i)); - if (PyType_Check(base) && (base->tp_flags & Py_TPFLAGS_HEAPTYPE)) { + if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) { PyErr_Format(PyExc_TypeError, "type '%.100s' is not dynamically allocated but " "its base type '%.100s' is dynamically allocated", @@ -6515,15 +6517,15 @@ add_subclass(PyTypeObject *base, PyTypeObject *type) // Only get tp_subclasses after creating the key and value. // PyWeakref_NewRef() can trigger a garbage collection which can execute // arbitrary Python code and so modify base->tp_subclasses. - PyObject *dict = base->tp_subclasses; - if (dict == NULL) { - base->tp_subclasses = dict = PyDict_New(); - if (dict == NULL) + PyObject *subclasses = base->tp_subclasses; + if (subclasses == NULL) { + base->tp_subclasses = subclasses = PyDict_New(); + if (subclasses == NULL) return -1; } - assert(PyDict_CheckExact(dict)); + assert(PyDict_CheckExact(subclasses)); - int result = PyDict_SetItem(dict, key, ref); + int result = PyDict_SetItem(subclasses, key, ref); Py_DECREF(ref); Py_DECREF(key); return result; @@ -6532,35 +6534,30 @@ add_subclass(PyTypeObject *base, PyTypeObject *type) static int add_all_subclasses(PyTypeObject *type, PyObject *bases) { + Py_ssize_t n = PyTuple_GET_SIZE(bases); int res = 0; - - if (bases) { - Py_ssize_t i; - for (i = 0; i < PyTuple_GET_SIZE(bases); i++) { - PyObject *base = PyTuple_GET_ITEM(bases, i); - if (PyType_Check(base) && - add_subclass((PyTypeObject*)base, type) < 0) - { - res = -1; - } + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *obj = PyTuple_GET_ITEM(bases, i); + // bases tuple must only contain types + PyTypeObject *base = _PyType_CAST(obj); + if (add_subclass(base, type) < 0) { + res = -1; } } - return res; } static void remove_subclass(PyTypeObject *base, PyTypeObject *type) { - PyObject *dict, *key; - - dict = base->tp_subclasses; - if (dict == NULL) { + PyObject *subclasses = base->tp_subclasses; // borrowed ref + if (subclasses == NULL) { return; } - assert(PyDict_CheckExact(dict)); - key = PyLong_FromVoidPtr((void *) type); - if (key == NULL || PyDict_DelItem(dict, key)) { + assert(PyDict_CheckExact(subclasses)); + + PyObject *key = PyLong_FromVoidPtr((void *) type); + if (key == NULL || PyDict_DelItem(subclasses, key)) { /* This can happen if the type initialization errored out before the base subclasses were updated (e.g. a non-str __qualname__ was passed in the type dict). */ @@ -6568,7 +6565,7 @@ remove_subclass(PyTypeObject *base, PyTypeObject *type) } Py_XDECREF(key); - if (PyDict_Size(dict) == 0) { + if (PyDict_Size(subclasses) == 0) { // Delete the dictionary to save memory. _PyStaticType_Dealloc() // callers also test if tp_subclasses is NULL to check if a static type // has no subclass. @@ -6579,15 +6576,17 @@ remove_subclass(PyTypeObject *base, PyTypeObject *type) static void remove_all_subclasses(PyTypeObject *type, PyObject *bases) { - if (bases) { - Py_ssize_t i; - for (i = 0; i < PyTuple_GET_SIZE(bases); i++) { - PyObject *base = PyTuple_GET_ITEM(bases, i); - if (PyType_Check(base)) { - remove_subclass((PyTypeObject*) base, type); - } + assert(bases != NULL); + // remove_subclass() can clear the current exception + assert(!PyErr_Occurred()); + + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(bases); i++) { + PyObject *base = PyTuple_GET_ITEM(bases, i); + if (PyType_Check(base)) { + remove_subclass((PyTypeObject*) base, type); } } + assert(!PyErr_Occurred()); } static int @@ -8466,9 +8465,9 @@ static int update_slots_callback(PyTypeObject *type, void *data) { slotdef **pp = (slotdef **)data; - - for (; *pp; pp++) + for (; *pp; pp++) { update_one_slot(type, *pp); + } return 0; } @@ -8654,29 +8653,33 @@ type_new_init_subclass(PyTypeObject *type, PyObject *kwds) /* recurse_down_subclasses() and update_subclasses() are mutually recursive functions to call a callback for all subclasses, - but refraining from recursing into subclasses that define 'name'. */ + but refraining from recursing into subclasses that define 'attr_name'. */ static int -update_subclasses(PyTypeObject *type, PyObject *name, +update_subclasses(PyTypeObject *type, PyObject *attr_name, update_callback callback, void *data) { - if (callback(type, data) < 0) + if (callback(type, data) < 0) { return -1; - return recurse_down_subclasses(type, name, callback, data); + } + return recurse_down_subclasses(type, attr_name, callback, data); } static int -recurse_down_subclasses(PyTypeObject *type, PyObject *name, +recurse_down_subclasses(PyTypeObject *type, PyObject *attr_name, update_callback callback, void *data) { - PyObject *ref, *subclasses, *dict; - Py_ssize_t i; - - subclasses = type->tp_subclasses; - if (subclasses == NULL) + // It is safe to use a borrowed reference because update_subclasses() is + // only used with update_slots_callback() which doesn't modify + // tp_subclasses. + PyObject *subclasses = type->tp_subclasses; // borrowed ref + if (subclasses == NULL) { return 0; + } assert(PyDict_CheckExact(subclasses)); - i = 0; + + Py_ssize_t i = 0; + PyObject *ref; while (PyDict_Next(subclasses, &i, NULL, &ref)) { assert(PyWeakref_CheckRef(ref)); PyObject *obj = PyWeakref_GET_OBJECT(ref); @@ -8687,18 +8690,20 @@ recurse_down_subclasses(PyTypeObject *type, PyObject *name, PyTypeObject *subclass = _PyType_CAST(obj); /* Avoid recursing down into unaffected classes */ - dict = subclass->tp_dict; + PyObject *dict = subclass->tp_dict; if (dict != NULL && PyDict_Check(dict)) { - int r = PyDict_Contains(dict, name); - if (r > 0) { - continue; - } + int r = PyDict_Contains(dict, attr_name); if (r < 0) { return -1; } + if (r > 0) { + continue; + } } - if (update_subclasses(subclass, name, callback, data) < 0) + + if (update_subclasses(subclass, attr_name, callback, data) < 0) { return -1; + } } return 0; }