samedi 4 février 2023

Is there a way to make implementing the copy constructor for a struct with a unique_ptr member of a polymorphic type less tedious / ugly?

If you have a class or struct which contain a member variable of type std::unique_ptr<T> where T is a polymorphic type, then it seems incredibly tedious to make it copyable. I find this very curious as i would assume this type of structure to not be so uncommon in modern c++.

Consider the following code:

struct Chair {/*...*/};
struct WoodenChair : public Chair {/*...*/};
struct MetalChair  : public Chair {/*...*/};

struct Room
{
  std::vector< std::unique_ptr<Chair> > Chairs;
};

Room myRoom;
Room copyOfMyRoom{myRoom}; //<-- error use of deleted copy constructor

As advocated in modern c++ the code uses smart pointers to manage its resources, in this case Chairs. It uses unique_ptr because the chairs are owned by the Room, once a Room gets deleted so should the Chairs unless they have been otherwise moved out of the room.

This code on the surface seems fine and i suspect is exactly how people coming from languages such as java or c# would write it. However, this code obviously wont compile because unique_ptr's copy constructor is deleted.

In order to make it copyable you would have to define a copy constructor, which seems like a potentially tedious process for 2 primary reasons:

  1. In the case the struct has many members, you would have to write out the nescesairy boilerplate to invidivually copy all of them, which in a normal struct would just happened automatically via the implicit copy constructor.

  2. For the polymorphic members themselves, you would either have to modify the underlying base struct & all derived types to have some sort of Base* Clone() method, capable of returning a newly allocated instance of themselves (which you might not be able to do, say in the case the structs are actually defined somewhere out of your reach in a library for example) or you would have to do some tedious process of dynamic_cast'ing and checking for nullptr, to figure out which which derived type the pointer is actually pointing to, before allocating a new copy of it.

Heres a slightly modified version of the previous code with how i would implement a copy constructor:

struct Chair {/*...*/};
struct WoodenChair : public Chair {/*...*/};
struct MetalChair  : public Chair {/*...*/};
struct OfficeChair : public Chair {/*...*/};

struct Room
{
  int foo;
  int bar;
  int baz;
  std::vector< std::unique_ptr<Chair> > Chairs;

  Room(const Room& other)
  {
    this->foo = other.foo;
    this->bar = other.bar;
    this->baz = other.baz;
    /*
    ...imagine doing this for 20+ members...
    */

    for(auto itr : this->Chairs )
    {
      if( dynamic_cast<WoodenChair*>(*itr) != nullptr )
        other.Chairs.emplace_back( std::make_unique<WoodenChair>() );
      if( dynamic_cast<MetalChair*>(*itr) != nullptr )
        other.Chairs.emplace_back( std::make_unique<MetalChair>() );
      if( dynamic_cast<OfficeChair*>(*itr) != nullptr )
        other.Chairs.emplace_back( std::make_unique<OfficeChair>() );
      /*
      ...imagine doing this for 10+ derived types...
      */
    }
  }
};

Room myRoom;
Room copyOfMyRoom{myRoom};

I understand from this answer that there is indeed a good reason for unique_ptr's copy constuctor to be deleted, but my question is: do we really have no better way of making objects such as Room copyable?

As i stated at the top, i would assume this kind of structure is very commmon & the advocated way of writing structs in modern c++. If i really need to implement a custom copy constructor in every single structure that contains a unique_ptr (which is most structures i would assume), it seems to contradict the general advice that is you usually shouldn't have to define custom copy constructors yourself unless you do something complicated.

In practice, what do you guys do? Maybe you dont actually need to copy structs so often and so one solution might be to simply not define copy constructors until you neeed them. However that seems kind of pedantically incorrect. An object that is copyable SHOULD have a copy constructor, no?

What about libraries? surely any struct defined a library would have to have its copy constructors defined, in case the user wants to make a copy.

Aucun commentaire:

Enregistrer un commentaire