0

I have a vector of class instances. Each of those instances has a unique_ptr to another class. Since I never try to copy the class instance or even share the pointer, I felt like unique_ptr are more appropriate than shared_ptrs since the pointer is not shared, but only accessible through the class instance.

Is it bad practice? And why wouldn't this work? I understand that copying an instance to a unique pointer would be ill-formed, but since I move it, I do not understand why this would not be allowed?

Would I have to create a custom move constructor? And what should it do?

The unique ptr should be deleted as soon as the object instance is being removed from the vector as there are no references left, right?

Code Example for better understanding:

class A {
private:
    int number;

public:
    void f() {
        std::cout << "The number is: " << number;
    }
    A(int i) : number{i} {}
    ~A() = default;
};

class B {
    std::unique_ptr<A> good_a;

    B() : good_a{ std::make_unique<A>(1) } {}
    ~B() = default;
};

int main()
{
    std::vector<B> all;

    B my_b(123);

    all.emplace_back(std::move(my_b));

}
Troganda
  • 121
  • 8
  • 1
    You should try to focus your post to a single question. What specifically are you asking? If `B` is expected to be the only owned of the pointed `A` then `unique_ptr` would be the ideal type of pointer to use. – François Andrieux Mar 22 '21 at 15:08
  • 1
    Also note that `all.emplace_back(std::move(my_b));` has the same effect as `all.push_back(std::move(my_b));`. If you really want to use `emplace_back`, try `all.emplace_back(123);` – alter_igel Mar 22 '21 at 15:09
  • 1
    Note that `~A() = default;` is not needed. It is implied for any class type that unless another destructor is explicitly declared. – François Andrieux Mar 22 '21 at 15:09
  • 1
    @alterigel only that it is not. The move constructor is inhibited. – SergeyA Mar 22 '21 at 15:09
  • https://godbolt.org/z/dr8KrsTfq – Marek R Mar 22 '21 at 15:20
  • @MarekR I don't see the point. The point of `emplace_back()` is to directly construct the object where it will live in the container by perfectly forwarding the constructor arguments. Copying or moving an object using `emplace_back()` defeats that purpose. – sweenish Mar 22 '21 at 15:32

3 Answers3

5

This answer focuses on compilation error you seem to be having. Bad or good practice is left for others to chime in.

Your code have several errors there, but the main one seems to be that your custom B( ) constructor inhibits default move constructor. If you add it, your code becomes well-formed.

Here is a full working code for the reference:

#include <memory>
#include <vector>

class A {
private:
    int number;

public:
    void f();
    A(int i) : number{i} {}
    ~A() = default;
};

struct B {
    std::unique_ptr<A> good_a;

    B(int k) : good_a{ std::make_unique<A>(k) } {}

    B(B&& ) = default;
    B& operator=(B&& ) = default; // not needed for the example, but good for completeness
};

int main()
{
    std::vector<B> all;
    B my_b(123);
    all.emplace_back(std::move(my_b));
}
SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • You said it has several errors, do you mind pointing them out? A few keywords would be enough! I can read up on them myself, just need some guidance on what the problem is – Troganda Mar 22 '21 at 15:13
  • @Troganda try to put your example into any online compiler, several errors will be hightlighted. – SergeyA Mar 22 '21 at 15:16
  • Oh okey so you were actually talking about compilation errors and not just bad practices / works but shouldn't be used type of errors – Troganda Mar 22 '21 at 15:28
  • @Troganda solve compilation errors first, worry about *bad practices* second. – SergeyA Mar 22 '21 at 15:31
1

And why wouldn't this work?

What you described could work.

I do not understand why this would not be allowed?

What you described would be allowed.

Would I have to create a custom move constructor?

No, that wouldn't be necessary, unless you define other special member functions, or have other members (beside the unique pointer) that have deleted or private move constructor.

The unique ptr should be deleted as soon as the object instance is being removed from the vector as there are no references left, right?

Members are destroyed when the super object is destroyed. And the destructor of the unique pointer invokes the deleter on its owned pointer.

Whether there are references to the pointed object has no effect on whether it is deleted or not. Anything referring to the deleted object will be left dangling.

Is it bad practice?

There isn't necessarily anything particularly bad about what you described in general, but that depends on exact details.

One potential issue is that dynamic allocation can be expensive in some cases, and using it unnecessarily would then be unnecessarily expensive. As such, you should to have some reason to allocate the pointed objects dynamically rather than storing them directly as members.


Bugs in your example:

  • You attempt to initialise B(123) but B has no constructor accepting an integer.
  • You attempt to initialise a B outside a member function of B, but its constructors and the destructor have private access.
  • You have user declared destructor for B, but no user declared move constructor or assignment operators and therefore the class isn't movable, which is a requirement for storing in std::vector.

Here is a fixed version that doesn't use unnecessary dynamic allocation:

struct A {
    int number;
};

struct B {
    A good_a;
};

B my_b{123};
all.push_back(my_b);
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Especially your last paragraph had a huge impact on me. I realized that you are right and that there is no reason to have 2 classes for this. I now decided to merge the classes and avoid the ptr chaos once and for all. Even though @SergeyA gave me a beautiful copy&paste solution, rethinking my design is the better choice. Thank you! – Troganda Mar 22 '21 at 15:25
  • @Troganda • Sometimes pointers *are* the right thing to do. I had assumed your example in the original question was simplified from a real world problem. But, as with eerorika's answer, I find dealing with objects to be my go-to, and only if that's not the right solution do I look at `std::unique_ptr` (I tend to shy away from `std::shared_ptr`). – Eljay Mar 22 '21 at 15:41
  • @Eljay well yes and no. It is simplified of course, but in the end it boiled down to this. and since the example class A really only had 2 functions that were only being called once from class B, so it was not a lot of work to merge them. Dealing with objects is fine, but I usually try to put them inside smart pointers to avoid errors because of missing destructor calls , etc. But who am I to talk. I'm still a rookie in c++! :)) – Troganda Mar 22 '21 at 15:50
1

Please read this answear.

Depending what is explicitly declared respective constructors with default implementation are implicitly defined or dropped. Rules are described by this table:

enter image description here

Since you have used explicit defined destructor (as default) you have disabled ("not declared") move constructor.

So to fix it you have to explicitly define move constructor or remove definition of destructor: https://godbolt.org/z/dr8KrsTfq

Marek R
  • 32,568
  • 6
  • 55
  • 140
  • I don't believe that this is correct. Defaulted destructors do not prevent compiler-provided move constructors from being generated. Per [this page](https://en.cppreference.com/w/cpp/language/destructor), the `= default` is *Forcing a destructor to be generated by the compiler*, which will not stop the compiler from providing a move constructor. This was a key point in our company's presentation of why we should be defaulting destructors and certain constructors instead of continuing the empty-body user-defined methods. – sweenish Mar 22 '21 at 15:38
  • @sweenish `~T() = default;` is not user-provided, but it is user-declared. This answer is correct. – François Andrieux Mar 22 '21 at 15:48
  • I still don't buy it. The declaration tells the compiler to provide its own version. That's no different than the compiler just providing it, so it shouldn't make a difference. And it doesn't. https://godbolt.org/z/Yr7eGos69 I commented out the move ctor and move assignment, and it still compiles just fine. [This site](https://cppinsights.io/s/e2a0b7b6) also supports my view. – sweenish Mar 22 '21 at 15:55
  • I found [this](https://isocpp.org/wiki/faq/cpp11-language-classes#default-move-copy) which supports your view, but now I'm confused as to what's really happening. – sweenish Mar 22 '21 at 16:03
  • note you can declare constructors and destructor in header and then define them as default implemented in cpp file. I'm doing this quite often so forward declaration could be used. – Marek R Mar 22 '21 at 16:21