0

I have a macro that either declares a RAII mutex when I'm compiling a program normally, or skips it for efficiency when I'm compiling in a single-threaded mode (where I set SINGLE_THREADED=1). Obviously I would prefer to not use macros as much as possible. I'm using C++20 (g++) - is there a way to refactor a macro like this to use constexpr or a template instead of #define?

My current implementation is:

#ifdef SINGLE_THREADED
#define READ_LOCK(name, mutex) ({})
#else
#define READ_LOCK(name, mutex) folly::SharedMutex::ReadHolder name(mutex);
#endif

I use it in my code like:

void foo() {
  READ_LOCK(lg, my_mutex);
  // ....
}
Thomas Johnson
  • 10,776
  • 18
  • 60
  • 98
  • You could just let the build system choose between two headers to be provided. Same type name, different implementations. When they're conditionally copied into some subdirectory in your build directory tree and added to the include paths, this doesn't need any macros nor `constexpr` computation. It's admittedly boring, though and doesn't directly answer the qestion I'm afraid... – lubgr Mar 03 '21 at 06:54
  • 1
    I think using a macro is a good way of coding that – Basile Starynkevitch Mar 03 '21 at 06:56
  • 2
    Macros aren't necessarily a bad thing, they are just often used in bad ways(often by ppl who learned from someone who is... not on the cutting edge of things, to say the least), but in your case, I think this is a valid use. – Nikita Demodov Mar 03 '21 at 07:12
  • The empty variant of the macro (`({})`) is non-standard, I'd use something like `do {} while (false)`. – HolyBlackCat Mar 03 '21 at 07:14
  • @HolyBlackCat Why is the `do {} while (false)` better? – Thomas Johnson Mar 03 '21 at 07:17
  • @ThomasJohnson - It's *standard*, like HolyBlackCat indicated. `({})` is a GNU extension, and not supported on all compilers. – StoryTeller - Unslander Monica Mar 03 '21 at 07:42
  • There is a standard way to create an empty macro? I mean like written in the ISO documents? – Bktero Mar 03 '21 at 07:43
  • 2
    @Bktero - Not standard like "this is *the* official way". Rather we just propose ways that a standard compliant compiler must support, thus increasing portability. – StoryTeller - Unslander Monica Mar 03 '21 at 07:46

2 Answers2

3

You can use a constexpr variable (can we really say variable when something is constexpr?) to select between two implementations:

#include <iostream>

static constexpr bool SINGLE_THREADED = false;

struct Mutex {
    void lock() { std::cout << "MUTEX LOCK\n"; };
    void unlock() {std::cout << "MUTEX UNLOCK\n"; };
};

struct SingleThreadLock {};

struct MultiThreadLock {
    MultiThreadLock() {
        mutex_m.lock();
    }

    ~MultiThreadLock() {
        mutex_m.unlock();
    }

private:
    Mutex mutex_m;
};

using Lock = std::conditional<SINGLE_THREADED, SingleThreadLock, MultiThreadLock>::type;

int main() {
    {
        Lock lock;
        std::cout << "do stuff while locked\n";
    }
    
    std::cout << "do other stuff after unlocking\n";
}

You can initialize SINGLE_THREADED with a define from your build system (for instance with CMake: target_compile_definitions(foo PUBLIC ENABLE_SINGLE_THREADED) and then in your C++ code: SINGLE_THREADED = ENABLE_SINGLE_THREADED). But what the point of avoiding macros in C++ when you rely on defines from you compiler? @vll solution is good then.

This solution is C++11-compatible.

Can be tried with Compiler Explorer: https://godbolt.org/z/bEWs5j

Bktero
  • 722
  • 5
  • 15
1

You can define a class that locks the mutex and make it no-op in the single threaded mode. This will call a function every time (assuming the class isn't optimized out), but time spent on a function call is neglible compared to locking a mutex.

class Lock {
public:
#ifdef SINGLE_THREADED
    Lock(mutex&) {}
#else
    Lock(mutex& m) : m_(m) {
        m_.lock();
    }
    ~Lock() {
        m_.unlock();
    }
private:
    mutex& m_;  
#endif
};

void foo() {
    Lock lg(my_mutex);
    // ...
}
VLL
  • 9,634
  • 1
  • 29
  • 54