samedi 17 avril 2021

C++11's "const==mutable", How to implement copy? efficiently?

As I introduced in this question and this question , it seem that the modern way to implement a thread safe class with hidden state is this:

struct Widget {
  int getValue() const{
    std::lock_guard<std::mutex> guard{m}; // lock mutex
    if (cacheValid) return cachedValue;
    else {
      cachedValue = expensiveQuery(); // write data mem
      cacheValid = true;                    // write data mem
      return cachedValue;
    }
  }                                      // unlock mutex
...
  private:
  mutable std::mutex m;
  mutable int cachedValue;
  mutable bool cacheValid;
...
};

The logic seems solids and explained here: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

The question is how do I implement the rest of the class? In particular the copy constructor.

One option is not to implement copy and that's it. (=delete). But Widget can have bonafide state (in ...) and that state should be copyable or one might want to make copies precisely to alleviate the usage of a single mutex and balance it with the expense of expensiveQuery.

So lets assume we want to make Widget copyable: one cannot make it default-copyable because the mutex is not copyable so something needs to be implemented manually.

This is already complicated because one needs to protect the source instance other before copying and for that one seems to need a body for the function.

struct Widget : private Widget_nonmt { // or struct Widget_mt
  Widget(Widget const& other){
    std::lock_guard<std::mutex> guard{m}; // lock mutex
     Widget_nonmt::cachedValue = other.cachedValue;
     Widget_nonmt::cachedValid = other.cachedValid;
  }
  ...
};

This is getting very ugly. Maybe one needs to implement a lock() function that RAII-lock and return the unprotected Widget_nonmt, this is possible but you can see how the problem diverges.

Is this the right way to make the C++11 Widget copyable?

Finally, we can have a user complaining that now he/she is being forced me to use the mutex for copies even in context that I know they are unnecessary.

And this is where it gets crazy, continuing with this logic (same as for the member function) one might think of introducing a non-const copy constructor.

struct Widget : private Widget_nonmt { // or struct Widget_mt
  Widget(Widget& other) : Widget_nonmt(static_cast<Widget_nonmt&>(other)){}
  Widget(Widget const& other) ... // same as above
};

Note that this is not a move constructor, it is something more strange. (something that killed std::auto_ptr)

It is not unusable, but is very non-standard and for example the STL containers for example will never use when copying containers of Widget.

Logic took me to this ugly place, starting from something solid. Is this a problem that still waits for a more elegant and general solution?

Is this the real end of C++98 to C++11 transition saga of a class with protected internal state?

Aucun commentaire:

Enregistrer un commentaire