Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional class member based on template

Tags:

c++

c++17

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!

like image 538
MasterID Avatar asked Aug 29 '26 05:08

MasterID


1 Answers

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 (e.g., 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;
};
like image 149
Piotr Skotnicki Avatar answered Aug 31 '26 22:08

Piotr Skotnicki



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!