Tuesday, April 18, 2023

std::unique_ptr<>

Okay, it shouldn't be that hard to have a variable that gives access to an object RAII-style while allowing late initialization: I should use std::unique_ptr

RAII is neat. Despite having an impossible-to-use-and-remember-name (don't be surprised if I use 'Rabi' instead ;), it means that you spend less time catching/rethrowing exceptions, less time checking return codes (because you used exceptions) and can think of your software as something as reliable as a building instead of some Jenga game. Want to have some code dealing with some file, but don't want to forget closing the file handle if anything goes wrong ? Simple.

  • have a RabiFile class that encapsulate your file handler
  • have the ctor of RabiFile do *everything* that's needed to be working on the file
  • have the destructor always going to a clean state.

As soon as you've written something like RabiFile music("forest.xm", RabiFile::RO), you're good to go

  • either your RabiFile exists and all its methods are now valid
  • or you failed to create it and an exception has been thrown, taking you out

So with that approach, you'll never write any music->open("underwater.xm") nor any music->close() anywhere. That makes writing of constructors a *bit* more complicated, I admit, especially those for objects that contains some RabiObjects: they have to catch exceptions and rollback any 'personal' resource acquisition they performed because nothing invokes destructor on objects that haven't been fully constructed. If your constructor fails with an exception, it's up to your constructor to ensure it leaves no mines behind.

But that usually don't happens a lot. It does happen when I stretch the RAII fashion to long-lived objects like GameLevel that has LevelMap and SpriteSet(s), or to GobState that has GobTransitions, though. But Rabi* is mostly for temporary things that need us to hold something while something else.

There's one common drawback to both, though. Because my objects have to be allocated on the stack to benefit automatic cleanup on failure, it is tricky to benefit from polymorphism. Say I should either create an instance that reads the script from a file or an instance that reads it from a buffer received over WiFi by runME ... I can't just replace BufferReader reader(...) at the head of my function by


loaderCode() {
   if (fromFile) {
       FileReader reader(whichFile);
   } else {
       BufferReader reader(whichBuffer);
   }
   DoStuffWith(reader); // no such reader, dude.
}

because there, the condition-dependant objects are no longer valid when I then want to use them. And migrating the DoStuffWith into the condition will soon turn unpleasant as well because it is not DRY. So instead, I can have


loaderCode() {
   std::unique_ptr<InputReader> reader;
   if (fromFile) { 
      reader = std::unique_ptr<FileReader>(new FileReader(...));
   } else {
      reader = std::unique_ptr<BufferReader>(new BufferReader(...));
   }
   DoStuffWith(*reader);
}

Granted, that will not allocate the object on the stack but at least it guarantees that the created object gets deleted whatever happens during DoStuffWit(*reader). Given the size of the NDS stack, it might not be a bad move.
Maybe I could have done it with auto_ptr instead. It seems it was the way to do it before C++11 came, declared auto_ptr obsolete and unique_ptr as being the way to do this instead.

A few things to remember when working with unique_ptr

  • you must use std::move(reader) if you intend to return the unique pointer as function value
  • you must use std::move(aswell) if you want to capture some unique pointer you've received as a member of a new uniquely pointed thing

  •  

3 comments:

  1. moving a local object in a return statement prevents copy elision

    ReplyDelete
  2. Yes, but here, it's a std::unique_ptr. It *has* to be moved and cannot be copied.

    ReplyDelete
  3. Jigé's twin10:34 am

    I'd rather use Auto* instead of Rabi*. That feels more readable.

    ReplyDelete

this is the right place for quickstuff