How to consume the soul of an object

Viewed 138

I have a movable type that has no default constructor and I want to take away its internal data and destroy them. My thought was to std::move it to a temporary, and let the temporary destructor happen (to destroy the internal data), leaving the original now-empty object behind.

The closest I've got makes use of a little helper template that's sole purpose is to destroy its argument (which is why it's passed by value, and why the body of the function is empty.) (Maybe there's a better way?) Then I call std::move to move the internals of the original object to a temp, and the temp will be destroyed, destroying the original internals, and leaving the object in whatever state it is in after having its internals moved to another object.

template <typename T>
void reset(T t) {}
};

reset(std::move(somevar));

For example, if I had a vector of really long strings and I wanted to "free up" the memory being used by one of the strings in the middle of the vector. This is not the perfect example because string does have a parameterless constructor, but my type does not. Nevertheless this captures the spirit of my motivation for attempting this.

std::vector<std::string> vec; // suppose I have a populated vector of strings

// Let's "reset" vec[3] to an empty string by moving it to a temp and destroying the temp
reset(std::move(vec[3]));

This moves the internals of vec[3] into the argument t of the reset function, and then, the destructor for t cleans up the internals. This leaves vec[3] as an empty string.

The important part of this stunt is that we didn't need to construct an empty string. This is important because my moveable type can be moved, but cannot be default constructed.

Other things I've tried:

{ std::move(somevar); } // this doesn't work

std::move all by itself doesn't work because you have to actually put the contents somewhere.

{ T t = std::move(somevar); } // works but cumbersome, and I have to name `T`

I'm just looking for a function I can call like reset(somevar). That consumes the soul of some object. And the std::move part isn't so bad. It is kind of like the authorization of I expect you to die. So it's ok, in my mind, to say reset(std::move(somevar)) But it doesn't finish the job. So, maybe there's some other function that does do what I want.

So, is there a std::consume_soul that will accomplish the same as moving the contents of an object to a dummy and then destroing the dummy, leaving the original object behind with no contents (or whatever state the move constructor will leave it in)?

Maybe I'm unaware of some obvious approach? I feel like there's likely some super easy way I'm just not aware of.

2 Answers

Moving isn’t automatic, but non-const references are, so you can write

template<class T>
void dementor(T &t) {T(std::move(t));}

In addition to the great answer from @Davis Herring i'll provide a few more options you could use instead of moving to a temporary that might work better, dending on your usecase:

1. Adding a reset() method for your type

The easiest solution would be to just add a reset() method to your type that clears all internal data.

Then you don't need to perform the move-to-temporary just for the side-effect of clearing your object.

2. Using std::optional

This could also be a good use-case for std::optional.

If you wrap your expensive class into an std::optional you can destroy the contained value anytime by calling .reset(). Additionally with optional you're able to check if a value currently exists.

This is even more powerful than moving to a temporary, since calling reset() on an optional will result in destructing the contained object. So whatever was inside it gets yeeted straight out of existence.

For your array case you could e.g. use:

std::vector<std::optional<ExpensiveClass>> arr;

// add a new element
arr.emplace_back(std::in_place, /* constructor args for ExpensiveClass */);
// add a new (empty) element without needing a default constructor:
arr.push_back({});

// yeeting an instance out of existence
arr[0].reset();

// creating a new instance in an empty slot
arr[2].emplace(/* constructor args for ExpensiveClass */);

// checking if there's an element
if(arr[0]) { /* ... */ }

// using it
arr[0]->doSomething();

godbolt example

That way you can easily control the lifetimes of the ExpensiveClass instances.

You also avoid the shenanigans of moving objects, namely:

  • The move constructor / move assignment operator might actually copy some (or all) of the members instead of moving them
  • The moved-from object only needs to be in a valid state, but can be indeterminate. So the only things that need to work on a moved-from object are (1) destructing it or (2) moving (or copying) another object into it.
    Example:
       std::string a = "FOO";
       std::string b = "BAR";
       a = std::move(b);
       std::cout << b << std::endl;
    
    Could result in printing FOO, BAR, nothing or some random value. (depending on how move is implemented)

3. Smart Pointers

If your objects are rather large and you'd prefer to allocate them on the heap you could exchange std::optional in the above examples with std::unique_ptr / std::shared_ptr.

They also provide a .reset() function that will reset the pointer back to an empty state.

Related