5

I have a class List which allocates memory automatically to store a list of items.

It has a destructor which deallocates this memory:

List::~List()
{
    free(memory);
}

This means, if I create a new list, I can use delete to call the destructor and free the memory.

The destructor will also be called once the variable is out of scope which is ALMOST always what I want. e.g:

int func()
{
    List list;
    list.push(...);
    ...
    return 47;
}

However, what if I want to return that list?

List func()
{
    List list;
    return list;
}

I am alright with the list being copied because it's returned by value, and doesn't have much data to copy (only a few ints, and a pointer).

However, the memory that the list allocated and has a pointer to, contains a LOT of data.

Since I am returning the list, the list is being copied along with the pointer to this data.

Since the list is now out of scope, the destructor is called, which frees up the pointer to that data, even though the copy also has the pointer.

How do I prevent this destructor from being called?


1) There is probably a solution by creating a copy constructor, however, I don't want to do this because then all the data at that pointer will likely have to be copied which is a waste of time and temporarily requires double the memory to be allocated.

2) I know I could just create a pointer List* list and return that, but I want to avoid the necessity of allocating new memory for that list if possible, and also want to avoid wasting more memory for a pointer (8 bytes or something).


Thanks in advance,

David.

David Callanan
  • 5,601
  • 7
  • 63
  • 105
  • 9
    `List` just needs [move semantics](http://en.cppreference.com/w/cpp/language/move_constructor). And the [RVO](http://en.cppreference.com/w/cpp/language/copy_elision) can come into play. – François Andrieux Jan 04 '18 at 18:19
  • 1
    There is no way of preventing a local object from being destroyed at the end of it's scope. The best you can hope for is to transfer the data or state to an object that has a longer lifetime, such as by to a function's return value. – François Andrieux Jan 04 '18 at 18:22
  • @FrançoisAndrieux "I am alright with the list being copied" and then destroyed, but I don't want the destructor to be called, because this will free memory at the pointer which the copy is now using. – David Callanan Jan 04 '18 at 18:24
  • 1
    What you need is a **move** constructor. – Angew is no longer proud of SO Jan 04 '18 at 18:25
  • You won't have a problem if your class supports move semantics correctly. The original's data pointer should be nulled or otherwise invalidated after moving, or otherwise prevent it's destruction from affecting the moved-to instance. The key is that, by the time the original is destroyed, the new instance must not point to the same data as the original. – François Andrieux Jan 04 '18 at 18:25
  • Ah, I see, I never heard of a move constructor. – David Callanan Jan 04 '18 at 18:26
  • Unless you’re programming for an embedded system, the size of an extra pointer is so cheap it’s not worth counting. The slowdown of going through an extra redirection is more significant, but also pretty small; the biggest cost would be in complicating the API. – Daniel H Jan 04 '18 at 18:35
  • @DanielH Yep, there really isn't a reason right now to worry about a single pointer, but I do know that in the future I might be worrying about up to 40 million extra pointers, which would be over 300 MB of extra memory that hopefully can be avoided. – David Callanan Jan 04 '18 at 18:38
  • 3
    Use std::vector. Done. –  Jan 04 '18 at 18:53
  • @manni66 I'd like to be in control over all the memory management. – David Callanan Jan 04 '18 at 19:01
  • I see how you control memory... –  Jan 04 '18 at 20:05

1 Answers1

9

Assuming you’re using C++11 or later, you just create a move constructor which leaves the old list empty.

In order to avoid similar problems, you also need to delete the copy constructor, or actually write it so your class can be copied (don’t worry; in most cases, including the one you were worried about with returning from a function, the compiler will use the move constructor or get rid of the copy/move entirely, especially after C++17).

This is greatly simplified by storing the pointers as unique_ptr, which will help make sure you don’t make a mistake, and will mean you don’t need to explicitly write the copy or move constructors.


If you’re stuck on pre-C++11, you can’t do this, at least not without a small storage-space penalty. You’d need to use a reference-counting pointer like boost::shared_ptr (a version was added to the standard library with C++11, but it sounds like you would rather move-only semantics), which will only free the memory when it’s the last one one left referencing that memory. This makes copying, creating, and destroying lists slightly slower (since it needs to check/update the reference counter), and it takes some space to store the count, but these costs are relatively small compared to those of actually copying the list’s contents.

Note that in this case two copies always point to actually the same list. If you update one “copy”, the other also gets updated. This is usually not the behavior that users of your class would expect in C++.

Daniel H
  • 7,223
  • 2
  • 26
  • 41
  • Do I have to write a copy constructor? If I do have a copy constructor, is there any situation when returning the object that the copy constructor will be called instead of the move constructor? – David Callanan Jan 04 '18 at 18:33
  • 3
    @DavidCallanan I feel you could benefit from reading [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three). – François Andrieux Jan 04 '18 at 18:36
  • 1
    @DavidCallanan The decision of rather or not to write a copy constructor boils down to two questions. Does the implicit copy constructor do the job? And, do you want your type to be copyable? Since you are using raw pointers, the implicit copy constructor does not do the job. – François Andrieux Jan 04 '18 at 18:38
  • 1
    @DavidCallanan If you delete the memory in your destructor, you have to either write the copy constructor or delete it (`List(List const& other) = delete;`), or you will be very open to use-after-free or double-delete bugs. – Daniel H Jan 04 '18 at 18:43
  • @FrançoisAndrieux Thank you; I was trying to find a good link for that but you beat me to it. – Daniel H Jan 04 '18 at 18:44
  • @SergeyA Ah, yes, good point. I was thinking that this wasn’t an option because `std::shared_ptr` is from C++11 onwards, but unlike `auto_ptr` third-party solutions like Boost’s work perfectly well without move semantics. I’ll update the answer. – Daniel H Jan 04 '18 at 18:46
  • @DanielH Does deleting it get rid of the default behavior? – David Callanan Jan 04 '18 at 18:50
  • @DavidCallanan Deleting the copy constructor (and copy assignment operator) means your class cannot be copied at all (at least without manually iterating through the list, etc.); you’ll get a compiler error if you try. If this is what you want, using `std::unique_ptr` will automatically do that for you (because the default copy constructor copies all the members, and `std::unique_ptr` has a deleted copy constructor). – Daniel H Jan 04 '18 at 18:56
  • Thanks, also, why are there two `&&` with a move constructor, and only one `&` with a copy constructor? What is the difference? – David Callanan Jan 04 '18 at 18:57
  • And finally in the move constructor, can you access private variables from `other`? e.g. `private_var = other.private_var; other.private_var = NULL;` – David Callanan Jan 04 '18 at 18:58
  • @DavidCallanan A move constructor uses *r-value references*, which are basically references to objects which either haven’t been assigned to a variable (e.g., `int && prvalue_ref = 3 + 5;`) or that are about to be destroyed (e.g., `int && xvalue_ref = function_call()`, since if you didn’t assign the result of `function_call` to anything it would just be destroyed). And all class methods can access any private members of that class from any arguments of that type which you pass in, so yes. – Daniel H Jan 04 '18 at 19:02
  • @DavidCallanan Note that not all `&&` types are r-value references; some are “universal references” instead. The rules for that are more complicated and not particularly relevant here; just want to point that out in case you run across them later. Also, I still *strongly* recommending just storing the pointer inside your `List` as a `unique_ptr`; there is no overhead at all on common implementations, it will be easier to use and easier for others to understand (you can use the rule of zero instead of the rule of five), and there will be less likely to be issues with memory management. – Daniel H Jan 04 '18 at 19:08
  • Thanks for all the help! Everything is now working perfectly. – David Callanan Jan 04 '18 at 19:13
  • 1
    @DanielH, would it be useful to add [copy elision](https://en.cppreference.com/w/cpp/language/copy_elision) to your answer? In many cases, the compiler is obliged to omit the move constructor, and instead "make the function-local variable directly and the variable that accepts the function result the same". – Emmef Oct 13 '21 at 12:06
  • 1
    @Emmef I donʼt know why I mentioned RVO instead of more generic copy elision, given that I wrote this post-C++17; Iʼve updated it. – Daniel H Oct 13 '21 at 20:24