Should Std::Unique_Ptr<Void> Be Permitted

Should std::unique_ptrvoid be permitted

MSVC is right while GCC is wrong:

Standard(3.9/5):

Incompletely-defined object types and the void types are incomplete types

Standard(20.7.1.1.2/4):

If T is an incomplete type, the program is ill-formed

Why is shared_ptrvoid legal, while unique_ptrvoid is ill-formed?

It is because std::shared_ptr implements type-erasure, while std::unique_ptr does not.


Since std::shared_ptr implements type-erasure, it also supports another interesting property, viz. it does not need the type of the deleter as template type argument to the class template. Look at their declarations:

template<class T,class Deleter = std::default_delete<T> > 
class unique_ptr;

which has Deleter as type parameter, while

template<class T> 
class shared_ptr;

does not have it.

So, why does shared_ptr implement type-erasure?

Well, it does so, because it has to support reference-counting, and to support this, it has to allocate memory from heap and since it has to allocate memory anyway, it goes one step further and implements type-erasure — which needs heap allocation too. So basically it is just being opportunist!

Because of type-erasure, std::shared_ptr is able to support two things:

  • It can store objects of any type as void*, yet it is still able to delete the objects on destruction properly by correctly invoking their destructor.
  • The type of deleter is not passed as type argument to the class template, which means a little bit freedom without compromising type-safety.

Alright. That is all about how std::shared_ptr works.

Now the question is, can std::unique_ptr store objects as void*? Well, the answer is, yes — provided you pass a suitable deleter as argument. Here is one such demonstration:

int main()
{
auto deleter = [](void const * data ) {
int const * p = static_cast<int const*>(data);
std::cout << *p << " located at " << p << " is being deleted";
delete p;
};

std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);

} //p will be deleted here, both p ;-)

Output (online demo):

959 located at 0x18aec20 is being deleted

You asked a very interesting question in the comment:

In my case I will need a type erasing deleter, but it seems possible as well (at the cost of some heap allocation). Basically, does this mean there is actually a niche spot for a 3rd type of smart pointer: an exclusive ownership smart pointer with type erasure.

to which @Steve Jessop suggested the following solution,

I've never actually tried this, but maybe you could achieve that by using an appropriate std::function as the deleter type with unique_ptr? Supposing that actually works then you're done, exclusive ownership and a type-erased deleter.

Following this suggestion, I implemented this (though it does not make use of std::function as it does not seem necessary):

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
return unique_void_ptr(ptr, [](void const * data) {
T const * p = static_cast<T const*>(data);
std::cout << "{" << *p << "} located at [" << p << "] is being deleted.\n";
delete p;
});
}

int main()
{
auto p1 = unique_void(new int(959));
auto p2 = unique_void(new double(595.5));
auto p3 = unique_void(new std::string("Hello World"));
}

Output (online demo):

{Hello World} located at [0x2364c60] is being deleted.
{595.5} located at [0x2364c40] is being deleted.
{959} located at [0x2364c20] is being deleted.

What is the safest way to pass a unique_ptr through a function requiring a void* param?

2) Would you agree that Q1) is essentially moot because MyThreadSpawner()'s pData will be destroyed when it goes out of scope regardless of whether MyTaskLauncher had already "wrapped" its memory or not?

Yes. That's what a unique_ptr is; it represents unique ownership. You're trying to break that.

3) What's the safest way to strip, pass and re-wrap on the other side my smart pointer through the CreateThread() API?

Define "safest". It's not clear why MyThreadSpawner is using a unique_ptr at all.

You are trying to transfer ownership of a unique_ptr. So you need to actually do that; the sender must lose ownership and the receiver must acquire it. That's pretty trivial:

DWORD WINAPI MyTaskLauncher(LPVOID pData)
{
assert(pData!= nullptr);
//Gain ownership of memory.
unique_ptr<SpawnData> pSpawnData(static_cast<SpawnData*>(pData));
pSpawnData->func();
}

ThreadId MyThreadSpawner(Functor func)
{
auto pData = std::make_unique<SpawnData>(func);

//CreateThread() is Windows API with a void* signature for the data param
... = CreateThread(..., static_cast<LPTHREAD_START_ROUTINE>(MyTaskLauncher),
//Lose ownership of memory.
static_cast<LPVOID>(pData.release()), ...);
threadId = ...;
return threadId;
}

5) The line marked (B), above is legal, whereas (A) far above (which does the same to a std::unique_ptr*), is not. Can anyone shed some light on as to why?

Legal in what sense? The void* does not point to a shared_ptr of any sort. Therefore casting it to a shared_ptr and then accessing it is not legal C++.

Just because your compiler just so happened to let you do it doesn't make it legal.

And finally, any suggestions on simpler and/or safer techniques for passing safe data through a function signature requiring a void*?

Yes: use std::thread. You can get a Win32 thread handle via thread::native_handle.

std::unique_ptrvoid not accepted by gcc 4.9.0

Will this exhibit undefined behaviour with certain polymorphic types?

If you use unique_ptr<void, void(*)(void*)>(new X(...), Deleter<X>) that will be correct for any X, because the deleter uses the correct dynamic type of the object.

If you used unique_ptr<void, void(*)(void*)>(new Derived(...), Deleter<Base>) that will have undefined behaviour (for both polymorphic and non-polymorphic types) if Base does not have a virtual destructor, or if the Base sub-object is not at the same address as the Derived object that contains it.

However, you should use static_cast not reinterpret_cast in both places. You should prefer to use the weakest cast that will work for a given situation, reinterpret_cast is a sledgehammer.

To make the code less error-prone I would write a helper function so you only ever name the type once, something like:

template<typename T, typename... Args>
std::unique_ptr<void, void(*)(void*)>
make_unique_void_ptr(Args&&... args)
{
using Ptr = std::unique_ptr<void, void(*)(void*)>;
return Ptr{ new T(std::forward<Args>(args)...), Deleter<T> };
}

Returning a unique void pointer from a function

You need to specify custom deleter in order to use void as unique_ptr's type argument like that:

#include <memory>
#include <cstdlib>

struct deleter {
void operator()(void *data) const noexcept {
std::free(data);
}
};

std::unique_ptr<void, deleter> get_ptr(std::size_t size) {
return std::unique_ptr<void, deleter>(std::malloc(size));
}

#include <cstdio>
int main() {
const auto p = get_ptr(1024);
std::printf("%p\n", p.get());
}

Can I throw a unique_ptr?

As a surrogate I'll be using Rextester which has MSVC version 18.00.21005.1. For GCC 4.8.1 and Clang 3.5, I'll be using Coliru. Now, initially when giving a hasty answer, I said that unique_ptrs cannot be copied and so you should be catching them by reference. However it appears the error occurs when you throw the object in MSVC. So the above advice will only apply to GCC and Clang.

catch(unique_ptr<IError>& Report)

It appears that they differ in how MSVC handles copy/move elision and/or move semantics, I'm not good enough at C++ to be more specific than that, but let's show some compilable examples. First a basic struct with a deleted copy constructor:

#include <iostream>
struct D {
D() {};
D(const D& other) = delete;
D(D&& other) { std::cout << "call D move constructor... \n"; }
};

int main()
{
try {
throw D();
} catch(D const& d)
{
}
}

Regardless of optimization level, for both GCC and Clang, no output unless you also add -fno-elide-constructors to the invocation and we see that they both call the move constructor. For MSVC, we get this error:

source_file.cpp(22) : error C2280: 'D::D(const D &)' : attempting to reference a deleted function
source_file.cpp(7) : see declaration of 'D::D'

For a more complicated example, see Throwing movable objects. The question is two years old yet we observe the same behavior in that GCC and Clang both call the move constructor in certain situations but MSVC calls the copy constructor in all situations (GCC and Clang differ for the Throw with object not about to die anyhow (enter non-zero integer part.)

Throw directly: 
C
caught: 007FFA7C
~
Throw with object about to die anyhow
C
c
~
caught: 007FFA74
~
Throw with object not about to die anyhow (enter non-zero integer)
C
c
caught: 007FFA70
~
1
~

TL;DR GCC and Clang will compile it but MSVC won't. A crappy workaround is to throw a pointer instead:

throw new unique_ptr<IError>;

catch(unique_ptr<IError>* Report);

std::unique_ptr reset() order of operations

It's often useful when analysing a prescribed order of steps, to consider which ones could throw and and what state that would leave everything in - with the intent that we should never end up in an unrecoverable situation.

Note that from the docs here that:

Unlike std::shared_ptr, std::unique_ptr may manage an object through any custom handle type that satisfies NullablePointer. This allows, for example, managing objects located in shared memory, by supplying a Deleter that defines typedef boost::offset_ptr pointer; or another fancy pointer.

So, in the current order:

  1. Saves a copy of the current pointer old_ptr = current_ptr

    if the copy constructor of fancy pointer throws, unique_ptr still owns the original object and new object is un-owned: OK

  2. Overwrites the current pointer with the argument current_ptr = ptr

    if the copy assignment of fancy pointer throws, unique_ptr still owns the original object and new object is un-owned: OK

    (this assumes the fancy pointer's copy assignment operator meets the usual exception-safety guarantee, but there's not much unique_ptr can do without that)

  3. If the old pointer was non-empty, deletes the previously managed object if(old_ptr) get_deleter()(old_ptr)

    at this stage unique_ptr owns the new object and it is safe to delete the old one.

In other words, the two possible outcomes are:

std::unique_ptr<T, FancyDeleter> p = original_value();
try {
auto tmp = new_contents();
p.reset(tmp);
// success
}
catch (...) {
// p is unchanged, and I'm responsible for cleaning up tmp
}

In your proposed order:

  1. If the original pointer is non-empty, delete it

    at this stage unique_ptr is invalid: it has committed the irreversible change (deletion) and there is no way to recover a good state if the next step fails

  2. Overwrite the current pointer with the argument current_ptr = ptr

    if the copy assignment of fancy pointer throws, our unique_ptr is rendered unusable: the stored pointer is indeterminate and we can't recover the old one

In other words, the unrecoverable situation I'm talking about is shown here:

std::unique_ptr<T, FancyDeleter> p = original_value();
try {
auto tmp = new_contents();
p.reset(tmp);
// success
}
catch (...) {
// I can clean up tmp, but can't do anything to fix p
}

After that exception, p can't even be safely destroyed, because the result of calling the deleter on its internal pointer might be a double-free.


NB. The deleter itself is not permitted to throw, so we don't have to worry about that.

The note saying

... the call to get_­deleter() might destroy *this.

sounds wrong, but the call get_­deleter()(old_­p) really might ... if *old_p is an object containing a unique_ptr to itself. The deleter call has to go last in that case, because there is literally nothing you can safely do to the unique_ptr instance after it.

Although this corner case is a solid reason for putting the deleter call at the end, I feel like the strong exception safety argument is perhaps less contrived (although whether objects with unique pointers to themselves are more or less common than fancy pointers with throwing assignment is anyone's guess).

How std::unique_ptr have no size overhead if using lambda

A capture-less lambda does not need to have any subobjects; it's just a type that has an operator() overload. As such, it can be (but is not required to be) an empty type. unqiue_ptr is allowed (but not required) to optimize the way it "contains" the deleter type so that, if the deleter type is an empty class type, then it can use various techniques to make sure that this type does not take up storage within the unique_ptr instance itself.

There are several ways to do this. The unique_ptr can inherit from the type, relying on EBO to optimize away the base class. With C++20, it can just make it a member subobject, relying on the [[no_unique_address]] attribute to provide empty member optimization. In either case, the only actual storage the unique_ptr<T> needs is for the pointer to T.

By contrast, a function pointer is a function pointer. It's a fundamental type that has to have storage, because it could point to any function with that signature. A type essentially contains the member function to call as a part of the type itself; a function pointer does not. The instance of the type doesn't actually need storage to find its operator().



Related Topics



Leave a reply



Submit