samedi 17 avril 2021

C++11's "const==mutable", Provide two classes for efficiency?

Introduction: In C++98, or before threads, albeit incorrect in some contexts, it was common to write class with internal hidden state in this way:

struct Widget {
  int getValue() const{
    if (cacheValid) return cachedValue;
    else {
      cachedValue = expensiveQuery(); // write data mem
      cacheValid = true;                    // write data mem
      return cachedValue;
    }
}
...
  private:
  mutable int cachedValue;
  mutable bool cacheValid;
...
};

This question is to know how this evolved in modern C++, and specifically:

How the rest of the class should be implemented at all under more general usage?

In C++11 Herb Sutter said that from now on const == thread safe. To me the logic is pretty solid and at the least, although not written in stone, this gives a good use of the language features (mutable/const/mutex) to express the new situation.

sutterconst

So, the new way to write the class would be:

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;
...
};

During the presentation, I though this was the end of the story, solid logic, elegant solution, but actually it seem that this is just the tip of the iceberg:

For efficiency, users of the C++98 version, although recognizing that thread safety is necessary, could claim that locking a mutex is not necessary in certain contexts and that this is a pessimization. Sometimes, they could say that they know that they are not in multiple thread context and they shouldn't need to be using this version of the class.

One possibility is to maintain two versions of the class, Widget_nonmt (c++98) and Widget_mt (C++11 version with mutex). I have seen in some libraries, if I understand correctly, like in the logging library spdlog.

This seems to be an overkill, specially because you end up duplicating most of the code, it seems. Is it?

One can have the best of both worlds by having two classes and the option to use const-non const members:

struct Widget_nonmt { // better naming?
  int getValue() const{
    if (cacheValid) return cachedValue;
    else {
      cachedValue = expensiveQuery(); // write data mem
      cacheValid = true;                    // write data mem
      return cachedValue;
    }
}
...
  private:
  mutable int cachedValue;
  mutable bool cacheValid;
...
};

struct Widget : private Widget_nonmt { // or struct Widget_mt
  int getValue() {
    return Widget_nonmt::getValue();
  }
  int getValue() const{
    std::lock_guard<std::mutex> guard{m}; // lock mutex
    return Widget_nonmt::getValue();
  }                                      // unlock mutex
...
  private:
  mutable std::mutex m; // now it can be though as protecting the Widget_nonmt part
...
};

Now there are two classes, and one also serves for an implementation of the other and it can in non-const context to be used without the mutex. Does it make sense? Is it a good option? is it used in real code?

Aucun commentaire:

Enregistrer un commentaire