13

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?

sbi
  • 219,715
  • 46
  • 258
  • 445
  • tuple and 5 reference accessors? – Yakk - Adam Nevraumont Feb 24 '15 at 12:04
  • You could write a wrapper class template `mutable_if_nonempty` that provides a `T& get() const;` member function IFF `T` is nonempty (by storing a `mutable T t;`), and a `T const& get() const;` member function otherwise (by inheriting from `T`). Alternatively, I think a `const_cast(*this)` is safe if `T` is empty, allowing to implement a `T& get() const;` when inheriting from `T`. – dyp Feb 24 '15 at 12:19

3 Answers3

7

No, a base class cannot be mutable. But...

thereby completely taking over the compiler's responsibility to protect me from silly constness mistakes.

...that's not necessarily the result of that. You can still let the compiler help you, by creating accessor functions instead of using your data structure directly. And you can name it in such a way that it should be obvious to everyone that those accessor functions are the only supported interface to the data.

mutable struct : SomePolicy, Mutex {
  T some_data;
} _dont_use_directly;

T &some_data() { return _dont_use_directly.some_data; }
const T &some_data() const { return _dont_use_directly.some_data; }

SomePolicy &some_policy() { return _dont_use_directly; }
const SomePolicy &some_policy() const { return _dont_use_directly; }

Mutex &some_mutex() const { return _dont_use_directly; }
  • Here's two upvotes for cleverness, and a downvote for obfuscation. `:)` (I think this would work, but it's a bit clumsy and takes a well-sized comment to be explained to any maintainer.) – sbi Feb 24 '15 at 12:25
  • @sbi Heh, yeah, a comment would be appropriate. But a simple one like "`// Use inheritance instead of multiple members to reduce class size when instantiated with empty dummy policy and/or mutex. Mutex is supposed to be mutable, unfortunately this means all data is technically mutable.`" would be clear enough, would it not? –  Feb 24 '15 at 12:34
5

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; }
};
MikeMB
  • 20,029
  • 9
  • 57
  • 102
  • I cannot change the lock class. (There's also more than one dummy mutex class, but there's template meta magic to detect them at compile time, so I could specialize `MutexWrapper` nonetheless.) – sbi Feb 24 '15 at 12:42
  • @sbi: Well, you could const_cast the this pointer (I'd wrap that in a member function) but this is getting uglier by the minute. – MikeMB Feb 24 '15 at 13:06
  • @sbi: Actually I just realized that, if you have to const cast at some point anyway, the whole wrapper idea is more or less useless (see my last edit) – MikeMB Feb 24 '15 at 13:41
  • I ended up using a variant of this where I made the dummy mutex' `lock()`/`unlock()` functions `const`. This also needed some more template-magic to in the lock class, but I got it working now. Since you've been closest to what I did in the end, I'll accept this answer. – sbi Feb 27 '15 at 11:27
  • @sbi: Thanks, If it doesn't involve too much code, could you show what your final solution actually looks like? – MikeMB Feb 27 '15 at 14:14
4

Assuming your std::tuple implements the empty base optimization (do check), then this might help:

mutable std::tuple<T, Mutex, SomePolicy> raw;
T          const& data()   const { return std::get<0>(raw); }
T               & data()         { return std::get<0>(raw); }
Mutex           & mutex()  const { return std::get<1>(raw); }
SomePolicy const& policy() const { return std::get<2>(raw); }
SomePolicy      & policy()       { return std::get<2>(raw); }

basically we put the optimization into a .raw mutable member that we never otherwise access (as a bonus, tuple's access is messy). Then we create reference-accessors that enforce const.

You may also want to:

my_class(my_class const&  )=default;
my_class(my_class      && )=default;
my_class&operator=(my_class const&  )=default;
my_class&operator=(my_class      && )=default;

to be explicit that my_class const&& is not in play. This also assumes that T and other types have well-behaved copy ctors and the like. (as in, they don't have a T(T&) ctor or operator= that feels overly entitled about the non-const-ness of the rhs)

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524