In C++, what is the proper way to do copy/move/assignment for objects with a unique id?

Viewed 186
/* Example.h file */
class Example {

  public:
    Example(const std::string& unique_id_, int attribute1_, int attribute2_,): 
      unique_id(unique_id_), attribute1(attribute1_), attribute2(attribute2_){};

    void set_attribute1(int attribute1_){ attribute1 = attribute1_; }
    void set_attribute2(int attribute2_){ attribute2 = attribute2_; }

    /* Deleting copy/move/assignment operators to make each instance unique */
    Exercise_Data(const Exercise_Data&) = delete;
    Exercise_Data(Exercise_Data&&) = delete;
    Exercise_Data& operator= (Exercise_Data&) = delete;
    Exercise_Data& operator= (Exercise_Data&&) = delete;

    private:
      const std::string unique_id;
      int attribute1;
      int attribute2;
}

Currently, this is my code. In multiple places in the calling code, I want to be able to edit all of the attributes of this class with the exception of the unique_id. If I add another attribute, then the calling code (in multiple places) would have to be edited to be able to set that new attribute.

  1. Is there a good way to allow for all attributes with the exception of the unique id to be edited?
  2. Is it ok to overload the assignment and move operators to do this and still delete the copy constructor?
  3. Anyone have a good example of a class with unique ids?
2 Answers

Is there a good way to allow for all attributes with the exception of the unique id to be edited?

Just make sure that the id is private and you never return a non-const pointer / reference to it. Making sure that the member functions don't modify the id should be trivial.

Is it ok to overload the assignment and move operators to do this and still delete the copy constructor?

It is OK for class to be movable but not copyable. Such types are called move-only.

You won't be able to implement the move operations if you want to keep the member const. I recommend against const members.

Anyone have a good example of a class with unique ids?

std::unique_ptr is an example of such class essentially. The "id" represents some unique resource that is cleaned by the destructor.

If each object has a unique, permanent identity, then you probably can't really support copying--a copy of an object should be the same as the original object, and in this case you're saying each should be unique, so having a copy probably doesn't make sense.

Move construction, on the other hand, should be more reasonable. Although an ID moves from one object to another, there's still only one object with a given identity at a given time.

The move ctor is pretty simple and straightforward. Just initialize your members in the member initializer list like usual, including the fact that since it's a move ctor, you want to move from the unique_id in the source:

Example(Example&& other) 
  : unique_id(std::move(other.unique_id)) 
  , attribute1(other.attribute1)
  , attribute2(other.attribute2)  
{ 
}

In theory, you probably should use std::move on attribute1 and attribute2 as well, but since they're ints, it won't normally make any real difference.

Then we get to the one that's a little bit non-trivial: move assignment. Since we've defined unique_id as const, we can't just copy over it. To do the job, we first destroy the current contents of the destination object, then use placement new to do a move construction from the source to the destination:

Example &operator=(Example&& other) { 
 this->~Example();
 new (this) Example(std::move(other));
 return *this;
}

This works because const qualification has no effect during destruction or construction, so that's basically the only way we can implement our assignment.

Here's a sample demonstrating it all in action:

#include <string>
#include <new>
#include <iostream>

class Example {

  public:
    Example(const std::string& unique_id_, int attribute1_, int attribute2_): 
      unique_id(unique_id_), attribute1(attribute1_), attribute2(attribute2_){};

    void set_attribute1(int attribute1_){ attribute1 = attribute1_; }
    void set_attribute2(int attribute2_){ attribute2 = attribute2_; }

    /* Deleting copy/move/assignment operators to make each instance unique */
    Example(const Example&) = delete;
    Example& operator= (Example&) = delete;
    
    Example(Example&& other) 
      : unique_id(std::move(other.unique_id)) 
      , attribute1(other.attribute1)
      , attribute2(other.attribute2)  
    { 
    }

    Example &operator=(Example&& other) { 
     this->~Example();
     new (this) Example(std::move(other));
     return *this;
    }

    private:
      const std::string unique_id;
      int attribute1;
      int attribute2;

    friend std::ostream &operator<<(std::ostream &os, Example const &e) { 
      return os << "ID: " << e.unique_id;
    }
};

int main() { 
  Example a("A", 1, 2);

  std::cout << "A: " << a << "\n";

  Example b{std::move(a)}; // move construction

  std::cout << "B: " << b << "\n";

  Example c("B", 3, 4);   // construct a destination object
  c = std::move(b);       // move assign into it

  std::cout << "C: " << c << "\n";
}

As to real-life examples of unique ids in objects...without going back on searching through code, I'm not sure. I can remember a few that seem like they're probably at least pretty close (and certainly others that have const members, that use the same destroy/placement new "trick" to do assignment), but I'm not quite ambitious enough to look through them to check whether there are any that are quite identical to this or not.

Related