5

I was unable to find a similar question to this one, please direct me to one if I missed it! I am experimenting with smart pointers, and came to this scenario where I would like to keep the value returned by use_count() in a shared_ptr object to 1 (to practice optimizing code). Here is a snippet I am working with:

#include <iostream>
#include <memory>
#include <vector>

// testFunc: displays the use_count of each shared ptr in the list
void testFunc(const std::vector<std::shared_ptr<int>> &list) {
    // reference each shared ptr in the list and display their use_count
    for (auto &elem : list) {
        std::cout << elem.use_count() << std::endl;
    }   
} // testFunc()

int main() {
    // allocate shared ptr instance of an int
    auto sharedTest = std::make_shared<int>(11);

    // allocate another shared ptr instance of another int
    auto anotherSharedTest = std::make_shared<int>(22);

    // use std::move to prevent another copy of the shared ptrs from being created
    testFunc({ std::move(sharedTest), std::move(anotherSharedTest) }); 

    return 0;
} // main()

The output by this program is

2
2

since the use_count of both shared ptrs is 2. Can anyone show me why I cannot keep them at 1? I suspect that "passing" the vector to testFunc is creating a copy of each shared ptr when the entire vector is being passed, however this surprises me since I am passing the vector by reference. Any input is greatly appreciated!

v.p.
  • 98
  • 6
  • 2
    This doesn't in any way answer your question, but if the goal here is to practice moving things, why not use a move-only type like `unique_ptr` and you will get a compile time error if you ever try to copy. – super Apr 10 '21 at 21:21
  • 3
    I don´t for sure know the answer, but I suspect the `{ ... }` is a `std::initializer_list`which then copies its argument into the temporary vector. So you have 2 of each, one in the temporary *initializer list* and the other in the *temporary vector*. The temporary *initializer list* won´t die until the function returns. – Galik Apr 10 '21 at 21:22
  • 1
    @AndersK It's not that, its the init_list. std::move does affect the use_count because of `shared_ptr`s constructor. – Hatted Rooster Apr 10 '21 at 21:24
  • @AndersK Initially, when testing this I did not use std::move, however without std::move the use_count() will increase to 3 as an end result. It seems like std::move does make a difference on the `use_count` in this case. – v.p. Apr 10 '21 at 21:28
  • 1
    You have your original *shared pointers*, then you create 2 temporary objects. An *initializer list* and a *vector*. Both the temporary *initializer list* and the temporary *vector* will keep one copy of the *shared pointers*. So if you don' t use `std::move` you have `3` (the originals plus 2 temporary objects). If you move you only have the 2 temporaries until the end of the function call. – Galik Apr 10 '21 at 21:33
  • @HattedRooster Yep u are right – AndersK Apr 10 '21 at 21:33
  • BTW I suspect a copy is as efficient as a move when dealing with smart pointers. – Galik Apr 10 '21 at 21:36
  • @Galik Thanks! I think I am understanding this now. I did not realize this about an initializer list. So if an initializer list creates a temporary copy of the objects, what is the general benefit or use case in using an intializer list? Since it seems like it is not the most optimized way of doing something like this. – v.p. Apr 10 '21 at 21:40
  • 1
    @v.p. They are purely for convenience. It is easy and tempting to think the `{ ... }` is a *vector* and to forget it's its own type that' s needed to facilitate the creation of the *vector*. – Galik Apr 10 '21 at 21:44
  • @Galik thanks for the input! – v.p. Apr 10 '21 at 22:14

2 Answers2

6

The problem is the temporary initializer_list<shared_ptr> keeps a copy of the elements, and it lives until the end of the full-expression (the ;).

There isn't much you can do, an initializer_list always stores its elements by copy.

As a workaround, you can construct the vector beforehand:

std::vector<std::shared_ptr<int>> list{std::move(sharedTest), std::move(anotherSharedTest)};
testFunc(list);

Should print 1 1.

rustyx
  • 80,671
  • 25
  • 200
  • 267
  • 1
    Wow! This worked, I appreciate the input. This answer now leads me to wonder what the benefits or use case of using an `initializer list` would ever be since it always stores by copy until the statement returns. – v.p. Apr 10 '21 at 21:36
  • 1
    Yep, it's a shortcoming of `initializer_list` which often goes unnoticed due to its transient nature. – rustyx Apr 10 '21 at 22:00
1

According to this answer, std::initializer_list only allows const access to its elements. This means that the std::vector constructor needs to copy all elements from the list (because move is non-const).

pschill
  • 5,055
  • 1
  • 21
  • 42