2

Say I have a Box. It contains Things, which are made by the Box, and only the Box. The Things are also uncopyable.

There is also an ownership relation, so I'd like to use unique_ptrs. (If the Box goes, everything goes with it.)

#include <memory>
#include <vector>

class Thing {
    friend class Box;
    public:
    ~Thing() {/* delete thing */};

    private:
    Thing() {/* construct thing */};
    Thing(const Thing& other) = delete;
    Thing& operator=(Thing&& other) = delete;
};

class Box {
    private:
    std::vector<std::unique_ptr<Thing> > contents{};

    public:
    void makeThing() {
        contents.push_back(nullptr);
        contents.back().reset(new Thing()); // I can live with the 'new' keyword here.
    }
};

My question is: is it bad to add to the vector by pushing a nullptr then resetting it to a new object? (Aside from the new keyword, which seems fine if the constructor doesn't throw?)

Note: I have already seen alternative ways to overcome the private constructor and deleted copy semantics. But is there anything dangerously wrong with what I wrote above?

Note 2: Explicitly stating it here since it already caught a few people: std::make_unique will not work as the Thing constructor is private.

jpo38
  • 20,821
  • 10
  • 70
  • 151
Remellion
  • 166
  • 2
  • 7

4 Answers4

4

I would go with this construct:

auto thing = std::unique_ptr<Thing>(new Thing());
contents.push_back(std::move(thing));

Because this describes more what you are actually want to do. Your Box object creates a Thing object, passes the ownership to a unique_ptr and then you move that unique_ptr to contents.

And don't use contents.emplace_back(new Thing()); one reason for sure it that it might leak. But the - imho - even more important part is readability and maintainability, and if you write contents.emplace_back(new Thing()); then you need to check if contents.emplace_back really claims ownership (that contents is really of the type std::vector<std::unique_ptr<Thing> > and not std::vector<Thing *>, with std::move(thing) and thing being a unique_ptr it is clear that the ownership is moved.

The downside with your solution is that at the time of reading contents.push_back(nullptr); it is not clear why nullptr is added and you need to read the next line to understand why.

t.niese
  • 39,256
  • 9
  • 74
  • 101
  • 3
    I think that a comment like "DO NOT replace with emplace_back. Temporary prevents leekage in case of exception." would be a very valuable addition to any variant of writing this code. (Adding this to your post, since I think that is the way to do it). – n314159 Feb 04 '20 at 11:31
  • @n314159 you only mean replacing `contents.push_back(std::move(thing));` with `contents.emplace_back(new Thing());`? Because replacing `contents.push_back(std::move(thing));` with `contents.emplace_back(std::move(thing));` for the given code, should not result in leakage. Because any `unique_ptr` that is created on the way to insertion would have automatic storage duration. – t.niese Feb 04 '20 at 12:04
  • `contents.emplace_back(std::move(thing))` is leak-free. The problem lies only with `contents.emplace_back(new Thing());` – bolov Feb 04 '20 at 12:07
3

Normally you would use make_unique. But since the constructor of Thing is private and Box is friend, you need to construct the new Thing in Box, you can't use std::make_unique. But your approach is ... weird.

If you can't use make_unique it is very easy to create the possibility of leaks. For instance, the first obvious thing to do:

contents.emplace_back(new Thing()); // this can leak

Will leak (if the vector resizing fails)

There are solutions to solve this, like the longer:

contents.emplace_back(std::unique_ptr<Thing>(new Thing()));

Or reserving ahead:

contents.reserve(contents.size() + 1); // this prevents the amortized constant complexity
                                       // of `push_back`
                                       // resulting in worst performance
contents.emplace_back(new Thing);

But the problem still remains: It's easy to write a leaky code and the correct code does extra things which can look unnecessary so you need to comment and document their purpose so that the next programmer doesn't optimize them.

The best solution in my opinion is to make the constructor available to make_unique:

class Thing {

    friend std::unique_ptr<Thing> std::make_unique<Thing>();
    // ...
};

class Box {
    // ...

    void makeThing() {
        contents.emplace_back(std::make_unique<Thing>());
    }
};

Since making make_unique friend defeats the purpose of private constructor, I like more n314159's solution of using a private allocate_thing method.


Thanks to all of the commenters down bellow for helping make this post correct and better.

bolov
  • 72,283
  • 15
  • 145
  • 224
  • `emplace_back` can leak, `push_back` doesn't. https://stackoverflow.com/questions/15783342/should-i-use-c11-emplace-back-with-pointers-containters – Remellion Feb 04 '20 at 11:02
  • @Remellion `push_back` will also leak. `make_unique` is what prevents the leak, not the `push_back` – bolov Feb 04 '20 at 11:04
  • A `reserve` before would prevent emplace_back from throwing. – n314159 Feb 04 '20 at 11:06
  • @bolov but in the above case, the `push_back` would throw before the creation of a new object on the heap and hence there would be no leekage. – n314159 Feb 04 '20 at 11:06
  • @n314159 you are correct. Still thinking about the problem. Editing – bolov Feb 04 '20 at 11:08
  • 4
    Wouldn't `friend std::unique_ptr std::make_unique` defeat the purpose of the private constructor? Now everyone could construct `Thing` using `make_unqiue` and you could then make it `public` anyway. – t.niese Feb 04 '20 at 11:56
  • 1
    @t.niese yep. Man, this is more of a challenge that it appears. – bolov Feb 04 '20 at 12:05
  • 1
    `contents.reserve(contents.size() + 1);` don't ever do that – Sopel Feb 04 '20 at 12:16
  • @Sopel why? What is the matter? – bolov Feb 04 '20 at 12:16
  • you end up with O(n^2) operations for n calls – Sopel Feb 04 '20 at 12:17
  • @Sopel Not necessarily. `reserve` needs to reserve *at least* n elements, but can reserve far more, and usually does. Still I share a bad feeling... – Aconcagua Feb 04 '20 at 17:47
  • 2
    About `push_back`: It *is* safe, but not because of throwing before a new object is created (that's not the case!), but because it would accept a `std::unique_ptr` only, either as const reference (theoretically – we couldn't move out) or as rvalue reference. In both cases, the smart pointer needs to be created first before the argument can be passed to `push_back`... – Aconcagua Feb 04 '20 at 17:50
  • @Aconcagua "and usually does" Which implementation does this? MSVC's and clang's don't. – Sopel Feb 04 '20 at 23:17
  • @Sopel So far the bad the bad feeling... Then we'd have to go the classic variant (`if(size() == capacity() { reserve(capacity() * 2); }` or similar). Much easier to leave that to `push_back` – unless we knew the number of all existing elements in advance, then we could have one single `reserve` (and of course should do the same even with `push_back`). – Aconcagua Feb 07 '20 at 07:37
3

I think @t.niese has the way to do it. I want to add an alternative that hides the new a bit and maybe prevents anyone from refactoring with emplace_back(new Thing) (that can leak).

#include <memory>
#include <vector>

class Thing {
    friend class Box;
    public:
    ~Thing() {/* delete thing */};

    private:
    Thing() {/* construct thing */};
    Thing(const Thing& other) = delete;
    Thing& operator=(Thing&& other) = delete;

    static std::unique_ptr<Thing> allocate_thing() {
        return std::unique_ptr<Thing>(new Thing);
    }
};

class Box {
    private:
    std::vector<std::unique_ptr<Thing> > contents{};

    public:
    void makeThing() {
        // DO NOT replace with emplace_back. Temporary prevents leekage in case of exception.
        contents.push_back(Thing::allocate_thing());
    }
};

What I don't like about this, is that one could imagine that Thing::allocate_thing() does some weird magic (which is not the case) and hence this makes the code more confusing.

I further want to say that maybe you should think about your design. While trying to reduce the access to Thing is good, maybe there is a better way to do that (maybe a anonymous namespace? Of course this will not prevent someone from creating an object if they can do a decltype of an element of the vector at some point). Maybe you can get some good recommnendations if you present your problem in more generality and not just ask for comments on this one solution.

n314159
  • 4,990
  • 1
  • 5
  • 20
  • Best solution IMO. I would consider naming it `Thing::make_unique` though. – Sopel Feb 04 '20 at 12:20
  • Interesting approach, I like the idea behind it. I actually already knew about t.niese's solution before asking, but wondered specifically why my admittedly odd approach was not also fine. About the design: if you put the vector in `Thing` instead of `Box`, it becomes a well-known exercise. `Box` is a wrapper I want for the structure. – Remellion Feb 04 '20 at 14:06
  • @Remellion I don't think I was clear enough. See the example [here](https://godbolt.org/z/oaFynZ). Both things are not save against a user that really wants to get at your type. But you don't have to defend against a user determined to do something wrong. You should not be a security guard but more like a kindergarten teacher. (Note that I am not sure how my example works with ADL). – n314159 Feb 04 '20 at 14:24
  • @n314159 I see your point, no need to guard vs malice. But as a beginner I have to defend against my future self, and as a beginner my toolkit for doing so is limited, so I appreciate answers and comments like this that explain other options. – Remellion Feb 04 '20 at 14:52
2

I've been playing around with this and just noticed that I landed approximately where @n31459 ended up. The only addition is that it uses perfect forwarding to the Thing constructors so you only need one makeThing function and factory function in Thing if you add more constructors later.

#include <memory>
#include <utility>
#include <vector>

class Thing {
public:
    ~Thing() = default;

private:
    explicit Thing(int Value) : value(Value) {}

    Thing(const Thing&) = delete;
    Thing(Thing&&) noexcept = delete;
    Thing& operator=(const Thing&) = delete;
    Thing& operator=(Thing&&) noexcept = delete;

    int value; // some data

    template<class... Args>
    static std::unique_ptr<Thing> make_unique(Args&&... args) {
        return std::unique_ptr<Thing>{new Thing(std::forward<Args>(args)...)};
    }

    friend class Box;
};

class Box {
public:
    template<class... Args>
    void makeThing(Args&&... args) {
        contents.emplace_back(Thing::make_unique(std::forward<Args>(args)...));
    }

private:
    std::vector<std::unique_ptr<Thing>> contents{};
};
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108