Search code examples
c++optimizationmutable

Can I empty-base optimize mutable data?


I have a class template which looked like this:

template<typename T, typename Mutex, typename SomePolicy>
class my_class {
public:
  T f() const {
    resource_lock a_lock(some_mutex);
    return some_policy.some_operation(some_data);
  }
private:
  T             some_data;
  mutable Mutex some_mutex;
  SomePolicy    some_policy;
};

If not used concurrently, we have a dummy mutex type which has all the member functions as inlined empty functions and no data. There are policies that have per-instance data and those that do not have any data.

This is library code and it turns out that this class template gets used in application code where the extra bytes matter which are needed for the data members some_mutex and some_policy even when they are empty classes. So I want to make use of the empty base optimization. For the policy, this is easy:

template<typename T, typename Mutex, typename SomePolicy>
class my_class {
public:
  T f() const {
    resource_lock a_lock(the_data.some_mutex);
    return the_data.some_operation(the_data.some_data);
  }
private:
  struct data : SomePolicy {
    T             some_data;
    mutable Mutex some_mutex;
  };
  data the_data;
};

However, given that some_mutex is mutable, I don't know how to make it a base class without making the_data, and thus all data, mutable, thereby completely taking over the compiler's responsibility to protect me from silly constness mistakes.

Is there a way to make a turn a mutable data member into a base of a non-mutable data member's class?


Solution

  • What you could do is use a mutex wrapper and specialize it for the empty mutex for which you then can perform the EBCO.

    class EmptyMutex{
        void lock() const {};
        void unlock() const {};
    };
    
    template< class MUX>
    class MutexWrapper {
        mutable MUX mux;
    public:
        void lock() const {mux.lock();};
        void unlock() const { mux.unlock() ;};
    };
    
    template<>
    class MutexWrapper<EmptyMutex> : public EmptyMutex {};
    
    
    template<typename T, typename Mutex, typename SomePolicy>
    class my_class {
    public:
        T f() const {
            resource_lock a_lock(the_data);
            return the_data.some_operation(the_data.some_data);
        }
    private:
        struct data : SomePolicy ,MutexWrapper<Mutex> {
            T             some_data;
    
        };
        data the_data;
    };
    

    The caveat of this solution is, that - inside a const memberfunction - while you can use the lock() and unlock() functions directly, you can only pass const references to the MutexWrapper as parameters. So in this case, your resource_lock would have to take a const reference to a MutexWrapper - when one would expect (and rightly so) that it actually changes the state of the mutex. This is quite missleading for someone, who doesn't know how MutexWrapper is implemented.

    For that reason I think it is more sensible to just const_cast the mutex when needed instead of using a wrapper:

    template<typename T, typename Mutex, typename SomePolicy>
    class my_class {
    public:
        T f() const {
            resource_lock a_lock(getNonConstMuxRef());
            return the_data.some_operation(the_data.some_data);
        }   
    private:
        struct data : SomePolicy, Mutex {
            T  some_data;
        };
        data the_data;
        Mutex& getNonConstMuxRef() const { return const_cast<my_class<T, Mutex, SomePolicy>*>(this)->the_data; }
    };