简体   繁体   中英

Conditional class member based on template

I want to have a class that receives a mutex type as template parameter to be (conditionally) thread-safe. If the mutex type is equal to NoMutex(my mutex empty implementation), I don't want to add this mutex member to my class so I can save space. I want to do something like this:

class NoMutex{
// Same interface as std::mutex but empty implementation
// so optimization can remove unnecessary function calls
};

template <class Mutex>
class MyClass {
    public:
    // Actually I want to hide it somehow. I dont want it to occupy any space if Mutex == NoMutex
    some_template_trick<std::is_same_v<Mutex, NoMutex, void, Mutex> mutex;

    void someFunction(){
        std::unique_lock<Mutex> l(getMutex()); // I want to use std::unique_lock here
        // do some concurrent stuff

    }


    Mutex& getMutex(){
        if constexpr(std::is_same_v<Mutex, NoMutex>){
            //ok... return reference to temporary is wrong, but i'm hopping
            //that gcc will realize there is nothing to be done once all implementations
            //are empty.
            return NoMutex();
        }
        return mutex;
    }
};

I don't want to specialize the class nor create lock() unlock() methods, I want them to be managed by std::unique_lock();Is it possible?

Thanks in advance!

Empty data members can be optimized away using empty base-class optimization (EBO). That is, although a stateless class has non-zero size (at least 1 byte), it doesn't consume additional memory if used as a base class:

template <typename Mutex>
struct CompressedMutex
{
    Mutex mutex;
    Mutex& getMutex() { return mutex; }
};

template <>
struct CompressedMutex<NoMutex> : NoMutex
{
    NoMutex& getMutex() { return *this; }
};

template <typename Mutex>
class MyClass : CompressedMutex<Mutex>
{
public: 
    void someFunction()
    {
        std::unique_lock<Mutex> l(this->getMutex());
    }
};

DEMO

Note that this-> or CompressedMutex<Mutex>:: is required when accessing getMutex() which is a dependent name.


Usually, however, one would want to lock a mutex also in const -qualified member functions. For that, the mutable keyword could be used in the definition of a mutex data member. In the NoMutex case that would instead require using const_cast<CompressedMutex&>(*this) , or declaring a static object that is returned instead:

template <typename Mutex>
struct CompressedMutex
{
    mutable Mutex mutex;
    Mutex& getMutex() const { return mutex; }
};

template <>
struct CompressedMutex<NoMutex>
{
    static NoMutex mutex;
    NoMutex& getMutex() const { return mutex; }
};

This technique has been widely used in the standard library so that a stateless allocator (including std::allocator<T> ) does not account for the total size of a container. Such objects are usually stored in a so called compressed pair (eg, boost::compressed_pair ) or its variation, sometimes together with a non-empty data member, so that the interface of an enclosing class does not change:

#include <boost/compressed_pair.hpp>

template <typename Mutex>
class MyClass
{
public: 
    void someFunction()
    {
        std::unique_lock<Mutex> l(data.second());
    }
private:
    boost::compressed_pair<SomeDataMemberType, Mutex> data;
};

You can do this with a struct wrapper holding either one or two things (untested code):

template <class T1, class T2> class Pair {
   Pair (const T1 &t1, const T2 &t2) : t1_(t1), t2_(t2) {}
   T1 & first () { return t1_; }
   T2 & second () { return t2_; }

   T1 t1_;
   T2 t2_;
};

template <class T1> class Pair<T1, NoMutex> {
   Pair (const T1 &t1) : t1_(t1) {}
   T1 & first () { return t1_; }
   NoMutex second () { return NoMutex(); }

   T1 t1_;
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM