Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moving an object with a file descriptor

Tags:

c++

I have a class, say Foo, defined like this:

class Foo {
public:
  Foo(int i) {
    valid = false;
    char devname[] = "/dev/device0";
    sprintf(devname, "/dev/device%d", i);
    fd = open(devname, O_RDWR);
    if (fd > 0) {
      valid = true;
    }

  ~Foo() {
    if (valid) {
      close(fd);
    }
  }

  bool valid;
private:
  int fd;
};

I have another class, say Bar, defined like this:

class Bar {
public:
  Bar() {
    for (int i = 0; i < 4; i++) {
      Foo foo(i);
      if (foo.valid) {
        vector_of_foos.push_back(foo);
      }
    }
  }

  std::vector<Foo> vector_of_foos;
};

The problem with this is that push_back makes a copy of the Foo object, which copies the fd property. Then the destructor for the original Foo object is called, which closes the file that fd points to, rendering fd invalid.

Unfortunately I cannot use emplace_back since I need to instantiate my Foo object before adding it to the vector_of_foos vector so that I can check the valid property.

I have also tried using std::move but this still calls the destructor for the original Foo object once it goes out of scope which closes the file.

What is the recommended way of managing a resource like this? Should I use an array of smart pointers? Should my vector_of_foos be instead a std::vector<Foo *> where I maintain a vector of pointers to Foo which I allocate dynamically?

like image 268
gpanders Avatar asked May 29 '19 22:05

gpanders


3 Answers

Foo needs a copy constructor and copy assignment operator so it can dup() the fd of the copied-from object (or, you need to delete them so Foo objects can't be copied at all, only moved).

When implementing move semantics, after moving the fd value from the moved-from object to the moved-to object, you need to update the moved-from object so its fd no longer refers to a valid file descriptor. Simply set it to -1, which is what open() and dup() return on error.

You don't need your valid member at all. That is a source of error waiting to happen if it ever gets out of sync with your fd.

Try something more like this:

class Foo {
public:
  Foo(int i) {
    std::string devname = "/dev/device" + std::to_string(i);
    fd = open(devname.c_str(), O_RDWR);
  }

  Foo(const Foo &src) {
    fd = dup(src.fd);
  }
  // or: Foo(const Foo &) = delete;

  Foo(Foo &&src) : fd(-1) {
    src.swap(*this);
  }

  ~Foo() {
    if (fd != -1) {
      close(fd);
    }
  }

  bool valid() const {
    return (fd != -1); 
  }

  Foo& operator=(Foo rhs) {
    rhs.swap(*this);
    return *this;
  }
  // optional: Foo& operator=(const Foo &) = delete;

  void swap(Foo &other) {
    std::swap(fd, other.fd);
  }

private:
  int fd;
};
like image 149
Remy Lebeau Avatar answered Oct 23 '22 21:10

Remy Lebeau


n.m beat me to the punch, but you need to define a move constructor that 'forgets' about your file descriptor in the moved-from object. Something like:

Foo (Foo &&move_from)
{
    valid = move_from.valid;
    fd = move_from.fd;
    move_from.valid = false;
}

Also, delete your copy constructor and copy assignment operator and implement a move-assignment operator. You then have an object that can be moved but not copied, rather like std::unique_ptr.

like image 1
Paul Sanders Avatar answered Oct 23 '22 22:10

Paul Sanders


You probably want to delete copy constructors altogether (or provide some logic to have two valid instances with different descriptors). If you delete them, compiler will make sure you use move semantics always.

Then, you can declare move constructors, that will perform valid moving:

class Foo {
public:
  Foo(int i) {
    valid = false;
    char devname[] = "/dev/device0";
    sprintf(devname, "/dev/device%d", i);
    fd = open(devname, O_RDWR);
    if (fd > 0) {
      valid = true;
    }

  ~Foo() {
    if (valid) {
      close(fd);
    }
  }

  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;
  Foo(Foo&& other) {
    valid = other.valid;
    fd = other.fd;
    other.valid = false;
  };
  Foo& operator=(Foo&& other) {
    valid = other.valid;
    fd = other.fd;
    other.valid = false;
  };

  bool valid;
private:
  int fd;
};

A bit of explanation:

You violated the Rule of Three/Five/Zero. In short, it says "whenever your class needs a user defined destructor/copy constructor/move constructor, it will most likely the rest of them". Your class does require a destructor, which you provided, but you didn't provide neither copy or move operators, which is a mistake. You have to provide both copy constructor/copy assignment operator (here deleted) and move constructor/move assignment operator.

As to why: compiler is not smart enough to guess what do you need. Ignoring the fact that your class doesn't support move semantics (user-defined destructor prevents implicit creation of move constructors), implicit move constructor is very, very simple. It just calls std::move on every class member. And std::move for primitive types is also very simple - it just copies that variable, as it's the fastest operation.
It could possibly zero the other variable, but that's not necessary. As per definition, the variable must be left in "unknown, but valid state". No change to that variable fits this definition.

like image 1
Yksisarvinen Avatar answered Oct 23 '22 23:10

Yksisarvinen