diff --git a/pxr/base/tf/CMakeLists.txt b/pxr/base/tf/CMakeLists.txt index 1f72005295..bb13fa7c9d 100644 --- a/pxr/base/tf/CMakeLists.txt +++ b/pxr/base/tf/CMakeLists.txt @@ -226,6 +226,7 @@ pxr_library(tf callContext.h cxxCast.h declarePtrs.h + delegatedCountPtr.h diagnosticLite.h functionTraits.h functionRef.h @@ -489,6 +490,7 @@ pxr_build_test(testTf testenv/staticTokens.cpp testenv/templateString.cpp testenv/token.cpp + testenv/delegatedCountPtr.cpp testenv/type.cpp testenv/typeMultipleInheritance.cpp testenv/typeInfoMap.cpp @@ -751,6 +753,9 @@ pxr_register_test(testTfTemplateString pxr_register_test(TfToken COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTf TfToken" ) +pxr_register_test(TfDelegatedCountPtr + COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTf TfDelegatedCountPtr" +) pxr_register_test(TfType COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTf TfType" ) diff --git a/pxr/base/tf/delegatedCountPtr.h b/pxr/base/tf/delegatedCountPtr.h new file mode 100644 index 0000000000..8969186788 --- /dev/null +++ b/pxr/base/tf/delegatedCountPtr.h @@ -0,0 +1,280 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the Apache License, Version 2.0 (the "Apache License") +// with the following modification; you may not use this file except in +// compliance with the Apache License and the following modification to it: +// Section 6. Trademarks. is deleted and replaced with: +// +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor +// and its affiliates, except as required to comply with Section 4(c) of +// the License and to reproduce the content of the NOTICE file. +// +// You may obtain a copy of the Apache License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the Apache License with the above modification is +// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the Apache License for the specific +// language governing permissions and limitations under the Apache License. +// + +#ifndef PXR_BASE_TF_DELEGATED_COUNT_PTR_H +#define PXR_BASE_TF_DELEGATED_COUNT_PTR_H + +#include "pxr/pxr.h" +#include "pxr/base/tf/tf.h" +#include "pxr/base/tf/api.h" + +#include "pxr/base/tf/diagnosticLite.h" + +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +/// When constructing a `TfDelegatedCountPtr` from a raw pointer, use the +/// `TfDelegatedCountIncrementTag` to explicitly signal that the pointer's +/// delegated count should be incremented on construction. This is the most +/// common tag. +constexpr struct TfDelegatedCountIncrementTagType{} + TfDelegatedCountIncrementTag{}; + +/// When constructing a `TfDelegatedCountPtr` from a raw pointer, use the +/// `TfDelegatedCountDoNotIncrementTag` to avoid incrementing the delegated +/// count on construction. This must be carefully used to avoid memory errors. +constexpr struct TfDelegatedCountDoNotIncrementTagType{} + TfDelegatedCountDoNotIncrementTag{}; + +/// Stores a pointer to a `ValueType` which uses `TfDelegatedCountIncrement` +/// and `TfDelegatedCountDecrement` to bookkeep. +/// +/// The `TfDelegatedCountPtr` is responsible for calling +/// `TfDelegatedCountIncrement` and `TfDelegatedCountDecrement` as needed in +/// construction, assignment, and destruction operations. +/// +/// The increment and decrement methods can assume pointers are non-null. +/// Releasing resources (e.g. freeing memory) is delegated to the user defined +/// `TfDelegatedCountDecrement` overloads. +/// +/// This may be created from a user constructed raw pointer or via +/// `TfMakeDelegatedCountPtr`. +template +class TfDelegatedCountPtr { +public: + using RawPtrType = std::add_pointer_t; + using ReferenceType = std::add_lvalue_reference_t; + using element_type = ValueType; + + static_assert( + std::is_same_v< + void, + decltype(TfDelegatedCountIncrement(std::declval()))>); + static_assert( + std::is_same_v< + void, + decltype(TfDelegatedCountDecrement(std::declval()))>); + + using IncrementIsNoExcept = + std::integral_constant< + bool, + noexcept(TfDelegatedCountIncrement(std::declval()))>; + using DecrementIsNoExcept = + std::integral_constant< + bool, + noexcept(TfDelegatedCountDecrement(std::declval()))>; + using IncrementAndDecrementAreNoExcept = + std::integral_constant< + bool, IncrementIsNoExcept() && DecrementIsNoExcept()>; + using DereferenceIsNoExcept = + std::integral_constant())>; + +private: + template + using _IsPtrConvertible = std::is_convertible< + std::add_pointer_t, RawPtrType>; + +public: + /// Create a pointer storing `nullptr` + TfDelegatedCountPtr() noexcept = default; + + /// Creates a new pointer without retaining + /// \sa TfDelegatedCountDoNotIncrementTag + TfDelegatedCountPtr(TfDelegatedCountDoNotIncrementTagType, + RawPtrType rawPointer) noexcept : + _pointer{rawPointer} { + } + + /// Creates a new retained pointer + /// + /// Retains only if the pointer is valid. + /// \sa TfDelegatedCountIncrementTag + TfDelegatedCountPtr(TfDelegatedCountIncrementTagType, + RawPtrType rawPointer) + noexcept(IncrementIsNoExcept()) : + _pointer{rawPointer} { + _IncrementIfValid(); + } + + /// Constructs by copying the pointer from `ptr`. + /// + /// Retains only if the pointer is valid. + TfDelegatedCountPtr(const TfDelegatedCountPtr& ptr) + noexcept(IncrementIsNoExcept()) : + _pointer{ptr.get()} { + _IncrementIfValid(); + } + + /// Constructs by copying a convertible pointer from `ptr`. + /// + /// This is necessary in part to allow a `const` `ValueType` to be + /// constructed from from a non-`const` `ValueType`. + /// + /// Retains only if the pointer is valid. + template + explicit TfDelegatedCountPtr( + const TfDelegatedCountPtr& ptr, + std::enable_if_t<_IsPtrConvertible::value, int> = 0) + noexcept(IncrementIsNoExcept()) : + _pointer(ptr.get()) { + _IncrementIfValid(); + } + + /// Constructs by moving the pointer from `ptr`. + /// + /// `ptr` will be reset to its default state (ie. `nullptr`). + TfDelegatedCountPtr(TfDelegatedCountPtr&& ptr) noexcept : + _pointer(ptr.get()) { + ptr._pointer = nullptr; + } + + /// Assigns by copying the pointer from `ptr`. + /// + /// Retains only if the pointer is valid. + TfDelegatedCountPtr& operator=(const TfDelegatedCountPtr& ptr) + noexcept(IncrementAndDecrementAreNoExcept()) { + // Implement copy assigment in terms of move assignment + return (*this = TfDelegatedCountPtr{ptr}); + } + + /// Assigns by copying a convertible pointer from `ptr`. + /// + /// This is necessary in part to allow a `const` `ValueType` to be + /// assigned from from a non-`const` `ValueType`. + /// + /// Retains only if the pointer is valid. + template + TfDelegatedCountPtr& operator=( + const TfDelegatedCountPtr& ptr) + noexcept(IncrementAndDecrementAreNoExcept()) { + static_assert(_IsPtrConvertible::value); + // Implement copy assigment in terms of move assignment + return (*this = TfDelegatedCountPtr{ptr}); + } + + /// Assigns by moving the pointer from `ptr`. + /// + /// `ptr` will be reset to its default state (ie. `nullptr`). + TfDelegatedCountPtr& operator=(TfDelegatedCountPtr&& ptr) + noexcept(DecrementIsNoExcept()) { + _DecrementIfValid(); + _pointer = ptr.get(); + ptr._pointer = nullptr; + return *this; + } + + /// Resets the pointer to its default state (ie. `nullptr`) + TfDelegatedCountPtr& operator=(std::nullptr_t) + noexcept(DecrementIsNoExcept()) { + reset(); + return *this; + } + + /// Releases if the underlying pointer is valid + ~TfDelegatedCountPtr() noexcept(DecrementIsNoExcept()) { + _DecrementIfValid(); + } + + /// Dereference the underlying pointer + ReferenceType operator*() const noexcept(DereferenceIsNoExcept()) { + return *get(); + } + + /// Arrow operator dispatch for the underlying pointer + RawPtrType operator->() const noexcept { + return get(); + } + + /// Returns true if the underlying pointer is non-null. + explicit operator bool() const noexcept { return get(); } + + /// Returns true if the underlying pointers are equivalent. + template + bool operator==( + const TfDelegatedCountPtr& other) const noexcept { + return get() == other.get(); + } + + /// Returns false if the underlying pointers are equivalent. + template + bool operator!=( + const TfDelegatedCountPtr& other) const noexcept { + return get() != other.get(); + } + + /// Orders based on the underlying pointer. + template + bool operator<( + const TfDelegatedCountPtr& other) const noexcept { + return get() < other.get(); + } + + /// Return the underlying pointer. + RawPtrType get() const noexcept { return _pointer; } + + /// Reset the pointer to its default state (`nullptr`), releasing if + /// necessary. + void reset() noexcept(DecrementIsNoExcept()) { + _DecrementIfValid(); + _pointer = nullptr; + } + + /// Swaps the pointer between two delegated count pointers + void swap(TfDelegatedCountPtr& other) noexcept { + std::swap(other._pointer, _pointer); + } + +private: + void _IncrementIfValid() noexcept(IncrementIsNoExcept()) { + if (_pointer) { + TfDelegatedCountIncrement(_pointer); + } + } + + void _DecrementIfValid() noexcept(DecrementIsNoExcept()) { + if (_pointer) { + TfDelegatedCountDecrement(_pointer); + } + } + + ValueType* _pointer{nullptr}; +}; + +/// Constructs a `TfDelegatedCountPtr` using `args`. +/// +/// This will necessarily call TfDelegatedCountIncrement once +template +TfDelegatedCountPtr +TfMakeDelegatedCountPtr(Args&&... args) { + return TfDelegatedCountPtr( + TfDelegatedCountIncrementTag, + new ValueType(std::forward(args)...) + ); +} + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif \ No newline at end of file diff --git a/pxr/base/tf/testenv/delegatedCountPtr.cpp b/pxr/base/tf/testenv/delegatedCountPtr.cpp new file mode 100644 index 0000000000..d5cdff76e5 --- /dev/null +++ b/pxr/base/tf/testenv/delegatedCountPtr.cpp @@ -0,0 +1,343 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the Apache License, Version 2.0 (the "Apache License") +// with the following modification; you may not use this file except in +// compliance with the Apache License and the following modification to it: +// Section 6. Trademarks. is deleted and replaced with: +// +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor +// and its affiliates, except as required to comply with Section 4(c) of +// the License and to reproduce the content of the NOTICE file. +// +// You may obtain a copy of the Apache License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the Apache License with the above modification is +// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the Apache License for the specific +// language governing permissions and limitations under the Apache License. +// + +#include "pxr/pxr.h" +#include "pxr/base/tf/delegatedCountPtr.h" +#include "pxr/base/tf/diagnosticLite.h" +#include "pxr/base/tf/regTest.h" + + +PXR_NAMESPACE_USING_DIRECTIVE + +// Test class which stores its own reference count. +struct RefCountedValue { + int value{5}; + mutable int count{0}; + RefCountedValue() = default; + explicit RefCountedValue(int v) : value(v) {} + RefCountedValue(int v, int c) : value(v), count(c) {} +}; + +struct DerivedRefCountedValue : public RefCountedValue {}; + +void TfDelegatedCountIncrement(const RefCountedValue* ptr) { + ++(ptr->count); +} + +void TfDelegatedCountDecrement(const RefCountedValue* ptr) noexcept { + if (--(ptr->count); ptr->count == 0) { + delete ptr; + } +} + +using TfTestDelegatedCountPtr = TfDelegatedCountPtr; +using TfTestDelegatedCountConstPtr = + TfDelegatedCountPtr; + +static bool +TestDefault() +{ + TF_AXIOM(TfTestDelegatedCountPtr{}.get() == nullptr); + TF_AXIOM(!TfTestDelegatedCountPtr{}); + return true; +} + +static bool +TestIncrementTag() +{ + RefCountedValue stackOwnedValue{10, 1}; + TF_AXIOM(stackOwnedValue.count == 1); + TfTestDelegatedCountPtr adopted{ + TfDelegatedCountIncrementTag, &stackOwnedValue}; + TF_AXIOM(stackOwnedValue.count == 2); + return true; +} + +static bool +TestDoNotIncrementTag() +{ + // Set the reference count to 2 to account for `adopted`'s + // usage of the DoNotIncrementTag. + RefCountedValue stackOwnedValue{10, 2}; + TF_AXIOM(stackOwnedValue.count == 2); + TfTestDelegatedCountPtr adopted{ + TfDelegatedCountDoNotIncrementTag, &stackOwnedValue}; + TF_AXIOM(stackOwnedValue.count == 2); + return true; +} + +static bool +TestScopedDecrement() +{ + RefCountedValue stackOwnedValue{7, 2}; + TF_AXIOM(stackOwnedValue.count == 2); + { + TfTestDelegatedCountPtr adopted{ + TfDelegatedCountDoNotIncrementTag, &stackOwnedValue}; + TF_AXIOM(stackOwnedValue.count == 2); + TF_AXIOM(adopted.get() == &stackOwnedValue); + } + TF_AXIOM(stackOwnedValue.count == 1); + return true; +} + +static bool +TestMake() +{ + auto made = TfMakeDelegatedCountPtr(12); + TF_AXIOM(made); + TF_AXIOM(made->count == 1); + TF_AXIOM(made->value == 12); + return true; +} + +static bool +TestEquality() +{ + RefCountedValue stackOwnedValue{10, 1}; + TfTestDelegatedCountPtr adopted{ + TfDelegatedCountIncrementTag, &stackOwnedValue}; + TfTestDelegatedCountPtr another{ + TfDelegatedCountIncrementTag, &stackOwnedValue}; + TF_AXIOM(adopted == another); + TF_AXIOM(adopted != TfTestDelegatedCountPtr{}); + TF_AXIOM(TfTestDelegatedCountPtr{} == TfTestDelegatedCountPtr{}); + + // Value equivalance doesn't imply address equivalence + TF_AXIOM(TfMakeDelegatedCountPtr(12) != + TfMakeDelegatedCountPtr(12)); + return true; +} + +static bool +TestPointerOperators() +{ + auto made = TfMakeDelegatedCountPtr(15); + TF_AXIOM(made.get()->value == 15); + TF_AXIOM(made->value == made.get()->value); + TF_AXIOM((*made).value == made.get()->value); + return true; +} + +static bool +TestNullAssignment() +{ + auto made = TfMakeDelegatedCountPtr(12); + TF_AXIOM(made->count == 1); + auto copy = made; + TF_AXIOM(made->count == 2); + copy = nullptr; + TF_AXIOM(!copy); + TF_AXIOM(made); + TF_AXIOM(made->count == 1); + return true; +} + +static bool +TestMoving() +{ + // Exercise moving back and forth using both constructor + // and assignment operator + auto made = TfMakeDelegatedCountPtr(12); + TfTestDelegatedCountPtr moved{std::move(made)}; + TF_AXIOM(!made); + TF_AXIOM(made.get() == nullptr); + TF_AXIOM(moved); + TF_AXIOM(moved->value == 12); + TF_AXIOM(moved->count == 1); + + made = std::move(moved); + TF_AXIOM(!moved); + TF_AXIOM(moved.get() == nullptr); + TF_AXIOM(made); + TF_AXIOM(made->value == 12); + TF_AXIOM(made->count == 1); + return true; +} + +static bool +TestMovingSelf() +{ + // Validate that self move-assignment leaves `adopted` in a valid + // state. + RefCountedValue stackOwnedValue{7, 1}; + TfTestDelegatedCountPtr adopted{ + TfDelegatedCountIncrementTag, &stackOwnedValue}; + TF_AXIOM(adopted); + TF_AXIOM(stackOwnedValue.count == 2); + adopted = std::move(adopted); + TF_AXIOM(adopted.get() == nullptr); + TF_AXIOM(stackOwnedValue.count == 1); + return true; +} + +static bool +TestMovingSameHeldPointer() +{ + // Validate that move assignment from two distinct TfDelegatedCountPtr + // values that hold the same pointer is successful. + RefCountedValue stackOwnedValue{7, 1}; + TfTestDelegatedCountPtr adopted{ + TfDelegatedCountIncrementTag, &stackOwnedValue}; + TfTestDelegatedCountPtr another{ + TfDelegatedCountIncrementTag, &stackOwnedValue}; + TF_AXIOM(stackOwnedValue.count == 3); + TF_AXIOM(another == adopted); + adopted = std::move(another); + TF_AXIOM(!another); + TF_AXIOM(adopted); + TF_AXIOM(stackOwnedValue.count == 2); + return true; +} + +static bool +TestCopyAssignment() +{ + auto made = TfMakeDelegatedCountPtr(85); + TF_AXIOM(made->count == 1); + + TfTestDelegatedCountPtr copied; + copied = made; + TF_AXIOM(made->count == 2); + TF_AXIOM(copied == made); + return true; +} + +static bool +TestCopyConstructor() +{ + auto made = TfMakeDelegatedCountPtr(87); + TF_AXIOM(made->count == 1); + + TfTestDelegatedCountPtr copied{made}; + TF_AXIOM(made->count == 2); + TF_AXIOM(copied == made); + return true; +} + +static bool +TestCopySelfAssignment() +{ + auto made = TfMakeDelegatedCountPtr(87); + TF_AXIOM(made); + TF_AXIOM(made->count == 1); + made = made; + TF_AXIOM(made->count == 1); + TF_AXIOM(made); + return true; +} + +static bool +TestCopySameHeldPointer() +{ + auto made = TfMakeDelegatedCountPtr(86); + TfTestDelegatedCountPtr copied{made}; + TF_AXIOM(copied == made); + TF_AXIOM(copied->count == 2); + copied = made; + TF_AXIOM(copied == made); + TF_AXIOM(copied->count == 2); + return true; +} + +static bool +TestSwap() +{ + auto made = TfMakeDelegatedCountPtr(16); + auto copy = made; + auto another = TfMakeDelegatedCountPtr(12); + TF_AXIOM(made->count == 2); + TF_AXIOM(made->value == 16); + TF_AXIOM(another->count == 1); + TF_AXIOM(another->value == 12); + made.swap(another); + TF_AXIOM(copy == another); + TF_AXIOM(copy != made); + TF_AXIOM(another->count == 2); + TF_AXIOM(another->value == 16); + TF_AXIOM(made->count == 1); + TF_AXIOM(made->value == 12); + return true; +} + +static bool +TestConstConversion() +{ + auto made = TfMakeDelegatedCountPtr(20); + TfTestDelegatedCountConstPtr constCopy{made}; + TfTestDelegatedCountConstPtr anotherConstCopy; + anotherConstCopy = made; + TF_AXIOM(made == constCopy); + TF_AXIOM(made == anotherConstCopy); + TF_AXIOM(made->count == 3); + return true; +} + +static bool +TestAssignDerived() +{ + const auto derived = TfMakeDelegatedCountPtr(); + TfDelegatedCountPtr base; + base = derived; + TF_AXIOM(derived->count == 2); + TF_AXIOM(base->count == 2); + TF_AXIOM(derived == base); + return true; +} + +static bool +TestInitializeDerived() +{ + const auto derived = TfMakeDelegatedCountPtr(); + TfDelegatedCountPtr base{derived}; + TF_AXIOM(derived->count == 2); + TF_AXIOM(base->count == 2); + TF_AXIOM(derived == base); + return true; +} + +static bool +Test_TfDelegatedCountPtr() { + return TestDefault() && + TestIncrementTag() && + TestDoNotIncrementTag() && + TestScopedDecrement() && + TestMake() && + TestEquality() && + TestPointerOperators() && + TestNullAssignment() && + TestMoving() && + TestMovingSelf() && + TestMovingSameHeldPointer() && + TestCopyAssignment() && + TestCopyConstructor() && + TestCopySelfAssignment() && + TestCopySameHeldPointer() && + TestSwap() && + TestConstConversion() && + TestInitializeDerived() && + TestAssignDerived(); +} + +TF_ADD_REGTEST(TfDelegatedCountPtr); \ No newline at end of file