Skip to main content
added 177 characters in body
Source Link
Davislor
  • 8.3k
  • 17
  • 37

Use the Swap Idiom to Move Instances

Among other benefits, swaps can be thread-safe atomic operations that never throw exceptions.

About That Converting Constructor

(Thanks to @chrysante for pointing out a serious oversight in my original answer.)

One important piece of functionality here is the ability to do things like:

 std::unique_ptr<Base> foo = std::make_unique<Derived>(bar, baz);

You implement this with the converting constructor

    // Added this to allow for runtime polymorphism, but I didn't 
    // see such a constructor on cppreference. Am I just missing something
    // or should this be handled another way? 
    template<typename U, 
        std::enable_if_t<std::is_convertible_v<U*, T*>, bool> = false>
    UniquePtr(UniquePtr<U>&& other) noexcept :
        m_ptr{other.release()} 
    {}

First, in a reinventing-the-wheel project, you get to use requires from C++20, which is much nicer than std::enable_if. So I recommend that.

Second, this implementation will break if the pointer conversion changes the bits of the pointer, even if it is from a derived class to a base class with a virtual destructor. This is because not all pointer conversions are pointer-interconvertible. That is, some pointer conversions (such as multiple or virtual inheritance) produce a pointer with different bits. Calling the original deleter, which expects a T* and not a U*, with the converted U*, would then fail. The current approach is only safe in two cases:

  • Where std::is_pointer_interconvertible_base_class<T,U> is true and the destructor of T is either virtual or trivial, or
  • Where std::is_layout_compatible<T,U>, std::is_trivially_destructible<T> and std::is_trivially_destructible<U> are all true. (The standard does not say std::unique_ptr should support this case.)

At present, however, the implementation supports this for all possible conversions, including arbitrary one-way custom conversion operators. The only way to make this work robustly would be to store the closure of the deleter and the original pointer inside each instance.

If you aren’t going to go through all that, you could declare non-pointer-interconvertible aliasing unsupported, add a check for that to your restrict clauses, and keep the simple, high-performance implementation you have now. In that case, you want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

Otherwise, you could put a/*** BIG IMPORTANT WARNING ***/ in your header file that people are only supposed to pass in a deleter that still works, somehow, with every possible conversion to a base pointer. And then say, the bugs this causes are on them for not reading the documentation.

Finally, since you ask, the converting constructor appears to be number 6 on Cppreference, as of July 2023.

Use the Swap Idiom to Move Instances

Among other benefits, swaps can be thread-safe atomic operations that never throw exceptions.

About That Converting Constructor

(Thanks to @chrysante for pointing out a serious oversight in my original answer.)

One important piece of functionality here is the ability to do things like:

 std::unique_ptr<Base> foo = std::make_unique<Derived>(bar, baz);

You implement this with the converting constructor

    // Added this to allow for runtime polymorphism, but I didn't 
    // see such a constructor on cppreference. Am I just missing something
    // or should this be handled another way? 
    template<typename U, 
        std::enable_if_t<std::is_convertible_v<U*, T*>, bool> = false>
    UniquePtr(UniquePtr<U>&& other) noexcept :
        m_ptr{other.release()} 
    {}

First, in a reinventing-the-wheel project, you get to use requires from C++20, which is much nicer than std::enable_if. So I recommend that.

Second, this implementation will break if the pointer conversion changes the bits of the pointer, even if it is from a derived class to a base class with a virtual destructor. This is because not all pointer conversions are pointer-interconvertible. That is, some pointer conversions (such as multiple or virtual inheritance) produce a pointer with different bits. Calling the original deleter, which expects a T* and not a U*, with the converted U*, would then fail. The current approach is only safe in two cases:

  • Where std::is_pointer_interconvertible_base_class<T,U> is true and the destructor of T is either virtual or trivial, or
  • Where std::is_layout_compatible<T,U>, std::is_trivially_destructible<T> and std::is_trivially_destructible<U> are all true. (The standard does not say std::unique_ptr should support this case.)

At present, however, the implementation supports this for all possible conversions, including arbitrary one-way custom conversion operators. The only way to make this work robustly would be to store the closure of the deleter and the original pointer inside each instance.

If you aren’t going to go through all that, you could declare non-pointer-interconvertible aliasing unsupported, add a check for that to your restrict clauses, and keep the simple, high-performance implementation you have now. In that case, you want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

Otherwise, you could put a/*** BIG IMPORTANT WARNING ***/ in your header file that people are only supposed to pass in a deleter that still works, somehow, with every possible conversion to a base pointer. And then say, the bugs this causes are on them for not reading the documentation.

Use the Swap Idiom to Move Instances

Among other benefits, swaps can be thread-safe atomic operations that never throw exceptions.

About That Converting Constructor

(Thanks to @chrysante for pointing out a serious oversight in my original answer.)

One important piece of functionality here is the ability to do things like:

 std::unique_ptr<Base> foo = std::make_unique<Derived>(bar, baz);

You implement this with the converting constructor

    // Added this to allow for runtime polymorphism, but I didn't 
    // see such a constructor on cppreference. Am I just missing something
    // or should this be handled another way? 
    template<typename U, 
        std::enable_if_t<std::is_convertible_v<U*, T*>, bool> = false>
    UniquePtr(UniquePtr<U>&& other) noexcept :
        m_ptr{other.release()} 
    {}

First, in a reinventing-the-wheel project, you get to use requires from C++20, which is much nicer than std::enable_if. So I recommend that.

Second, this implementation will break if the pointer conversion changes the bits of the pointer, even if it is from a derived class to a base class with a virtual destructor. This is because not all pointer conversions are pointer-interconvertible. That is, some pointer conversions (such as multiple or virtual inheritance) produce a pointer with different bits. Calling the original deleter, which expects a T* and not a U*, with the converted U*, would then fail. The current approach is only safe in two cases:

  • Where std::is_pointer_interconvertible_base_class<T,U> is true and the destructor of T is either virtual or trivial, or
  • Where std::is_layout_compatible<T,U>, std::is_trivially_destructible<T> and std::is_trivially_destructible<U> are all true. (The standard does not say std::unique_ptr should support this case.)

At present, however, the implementation supports this for all possible conversions, including arbitrary one-way custom conversion operators. The only way to make this work robustly would be to store the closure of the deleter and the original pointer inside each instance.

If you aren’t going to go through all that, you could declare non-pointer-interconvertible aliasing unsupported, add a check for that to your restrict clauses, and keep the simple, high-performance implementation you have now. In that case, you want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

Otherwise, you could put a/*** BIG IMPORTANT WARNING ***/ in your header file that people are only supposed to pass in a deleter that still works, somehow, with every possible conversion to a base pointer. And then say, the bugs this causes are on them for not reading the documentation.

Finally, since you ask, the converting constructor appears to be number 6 on Cppreference, as of July 2023.

added 1408 characters in body
Source Link
Davislor
  • 8.3k
  • 17
  • 37

AddAbout That Converting ConstructorsConstructor

(Thanks to @chrysante for pointing out a serious oversight in my original answer.)

One important piece of missing functionality here is the ability to do things like:

This is how you pass around interface pointers with automatic memory management.

You wantimplement this to be a movewith the converting constructor

    // Added this to allow for runtime polymorphism, but I didn't 
    // see such a constructor on cppreference. Am I just missing something
    // or should this be handled another way? 
    template<typename U>U, 
        std::enable_if_t<std::is_convertible_v<U*, T*>, bool> = false>
    UniquePtr(UniquePtr<U>&& other) noexcept :
        m_ptr{other.release()} 
    {}

withFirst, in a suitablereinventing-the-wheel project, you get to use requires clause between those two linesfrom C++20, which is much nicer than std::enable_if. Note So I recommend that this will need.

template <typename> friend class UniquePtr;

Second, this implementation will break if the pointer conversion changes the bits of the pointer, even if it is from a derived class to work correctly. Note that you might also need different implementations whena base class with a is_pointer_interconvertable_base_of<T,U>virtual than when itdestructor. This is because not. When the all pointer conversions are pointer-interconvertible. That is not interconvertible, you will actually need to cast thesome pointer toconversions (such as multiple or virtual inheritance) produce a pointer with different value, and wrapbits. Calling the original deleter so it still gets called, which expects a T* and not a U*, with the originalconverted U*, unaltered valuewould then fail. This implies that The current approach is only safe in two cases:

  • Where std::is_pointer_interconvertible_base_class<T,U> is true and the destructor of T is either virtual or trivial, or
  • Where std::is_layout_compatible<T,U>, std::is_trivially_destructible<T> and std::is_trivially_destructible<U> are all true. (The standard does not say std::unique_ptr should support this case.)

At present, to support non-pointer-interconvertiblehowever, the implementation supports this for all possible conversions, youincluding arbitrary one-way custom conversion operators. The only way to make this work robustly would wantbe to store the closure of the deleter as a std::functionand the original pointer inside each instance.

If you aren’t going to go through all that, and restrict yourself to pointeryou could declare non-pointer-interconvertible aliasing unsupported, add a check for that to your restrict clauses, and keep the simple, high-performance implementation you thenhave now. In that case, you want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

Otherwise, you could put a/*** BIG IMPORTANT WARNING ***/ in your header file that people are only supposed to pass in a deleter that still works, somehow, with every possible conversion to a base pointer. And then say, the bugs this causes are on them for not reading the documentation.

Add Converting Constructors

One important piece of missing functionality here is the ability to do things like:

This is how you pass around interface pointers with automatic memory management.

You want this to be a move constructor

template<typename U>
UniquePtr(UniquePtr<U>&& other)

with a suitable requires clause between those two lines. Note that this will need

template <typename> friend class UniquePtr;

to work correctly. Note that you might also need different implementations when is_pointer_interconvertable_base_of<T,U> than when it is not. When the pointer is not interconvertible, you will actually need to cast the pointer to a different value, and wrap the original deleter so it still gets called with the original, unaltered value. This implies that, to support non-pointer-interconvertible conversions, you would want to store the deleter as a std::function.

If you aren’t going to go through all that, and restrict yourself to pointer-interconvertible aliasing, you then want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

About That Converting Constructor

(Thanks to @chrysante for pointing out a serious oversight in my original answer.)

One important piece of functionality here is the ability to do things like:

You implement this with the converting constructor

    // Added this to allow for runtime polymorphism, but I didn't 
    // see such a constructor on cppreference. Am I just missing something
    // or should this be handled another way? 
    template<typename U, 
        std::enable_if_t<std::is_convertible_v<U*, T*>, bool> = false>
    UniquePtr(UniquePtr<U>&& other) noexcept :
        m_ptr{other.release()} 
    {}

First, in a reinventing-the-wheel project, you get to use requires from C++20, which is much nicer than std::enable_if. So I recommend that.

Second, this implementation will break if the pointer conversion changes the bits of the pointer, even if it is from a derived class to a base class with a virtual destructor. This is because not all pointer conversions are pointer-interconvertible. That is, some pointer conversions (such as multiple or virtual inheritance) produce a pointer with different bits. Calling the original deleter, which expects a T* and not a U*, with the converted U*, would then fail. The current approach is only safe in two cases:

  • Where std::is_pointer_interconvertible_base_class<T,U> is true and the destructor of T is either virtual or trivial, or
  • Where std::is_layout_compatible<T,U>, std::is_trivially_destructible<T> and std::is_trivially_destructible<U> are all true. (The standard does not say std::unique_ptr should support this case.)

At present, however, the implementation supports this for all possible conversions, including arbitrary one-way custom conversion operators. The only way to make this work robustly would be to store the closure of the deleter and the original pointer inside each instance.

If you aren’t going to go through all that, you could declare non-pointer-interconvertible aliasing unsupported, add a check for that to your restrict clauses, and keep the simple, high-performance implementation you have now. In that case, you want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

Otherwise, you could put a/*** BIG IMPORTANT WARNING ***/ in your header file that people are only supposed to pass in a deleter that still works, somehow, with every possible conversion to a base pointer. And then say, the bugs this causes are on them for not reading the documentation.

added 631 characters in body
Source Link
Davislor
  • 8.3k
  • 17
  • 37

Use the Swap Idiom to Move Instances

Among other benefits, swaps can be thread-safe atomic operations that never throw exceptions.

Add Converting Constructors

One important piece of missing functionality here is the ability to do things like:

 std::unique_ptr<Base> foo = std::make_unique<Derived>(bar, baz);

This is how you pass around interface pointers with automatic memory management.

You want this to be a move constructor

template<typename U>
UniquePtr(UniquePtr<U>&& other)

with a suitable requires clause between those two lines. Note that this will need

template <typename> friend class UniquePtr;

to work correctly. Note that you might also need different implementations when is_pointer_interconvertable_base_of<T,U> than when it is not. When the pointer is not interconvertible, you will actually need to cast the pointer to a different value, and wrap the original deleter so it still gets called with the original, unaltered value. This implies that, to support non-pointer-interconvertible conversions, you would want to store the deleter as a std::function.

If you aren’t going to go through all that, and restrict yourself to pointer-interconvertible aliasing, you then want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

Use the Swap Idiom to Move Instances

Among other benefits, swaps can be thread-safe atomic operations that never throw exceptions.

Add Converting Constructors

One important piece of missing functionality here is the ability to do things like:

 std::unique_ptr<Base> foo = std::make_unique<Derived>(bar, baz);

This is how you pass around interface pointers with automatic memory management.

You want this to be a move constructor

template<typename U>
UniquePtr(UniquePtr<U>&& other)

with a suitable requires clause between those two lines. Note that this will need

template <typename> friend class UniquePtr;

to work correctly. Note that you might also need different implementations when is_pointer_interconvertable_base_of<T,U> than when it is not.

Use the Swap Idiom to Move Instances

Among other benefits, swaps can be thread-safe atomic operations that never throw exceptions.

Add Converting Constructors

One important piece of missing functionality here is the ability to do things like:

 std::unique_ptr<Base> foo = std::make_unique<Derived>(bar, baz);

This is how you pass around interface pointers with automatic memory management.

You want this to be a move constructor

template<typename U>
UniquePtr(UniquePtr<U>&& other)

with a suitable requires clause between those two lines. Note that this will need

template <typename> friend class UniquePtr;

to work correctly. Note that you might also need different implementations when is_pointer_interconvertable_base_of<T,U> than when it is not. When the pointer is not interconvertible, you will actually need to cast the pointer to a different value, and wrap the original deleter so it still gets called with the original, unaltered value. This implies that, to support non-pointer-interconvertible conversions, you would want to store the deleter as a std::function.

If you aren’t going to go through all that, and restrict yourself to pointer-interconvertible aliasing, you then want to declare your deleter with the [[no_unique_address]] attribute, so that the vast majority of deleters, which are unit types whose instance contain nothing, don’t need to be created and stored as objects.

Source Link
Davislor
  • 8.3k
  • 17
  • 37
Loading