Why Use std::scoped_lock Over std::lock in C++

Introduced in C++17, std::scoped_lock provides a modern, RAII-based mechanism for acquiring multiple mutexes simultaneously without risking deadlocks. While the legacy std::lock function also avoids deadlocks across multiple locks, it merely acquires the locks without managing their lifetimes, requiring additional boilerplate to ensure they are properly released. This article outlines the key advantages of using std::scoped_lock over std::lock, highlighting improvements in exception safety, boilerplate reduction, and overall code clarity.

Built-in RAII and Automatic Unlocking

The most significant advantage of std::scoped_lock is its adherence to the Resource Acquisition Is Initialization (RAII) idiom.

When using std::lock, the function locks the supplied mutexes, but it does not unlock them. To prevent resource leaks, you must manually bind each locked mutex to an RAII wrapper like std::lock_guard or std::unique_lock using the std::adopt_lock tag:

// Legacy approach with std::lock
std::lock(mutexA, mutexB);
std::lock_guard<std::mutex> lockA(mutexA, std::adopt_lock);
std::lock_guard<std::mutex> lockB(mutexB, std::adopt_lock);

In contrast, std::scoped_lock handles both acquisition and release within a single object:

// Modern approach with std::scoped_lock
std::scoped_lock lock(mutexA, mutexB);

When lock leaves scope, both mutexA and mutexB are automatically unlocked in reverse order of destruction, eliminating the risk of dangling locks.

Robust Exception Safety

Using std::lock introduces a subtle exception-safety vulnerability. If an exception occurs after std::lock succeeds but before all corresponding std::lock_guard objects are instantiated, the mutexes remain locked permanently, leading to application-wide deadlocks.

std::scoped_lock eliminates this window entirely. Its constructor guarantees that if an exception is thrown during lock acquisition, any mutexes that were already locked during the process are automatically released before the exception propagates.

Reduced Boilerplate with Class Template Argument Deduction (CTAD)

std::scoped_lock leverages variadic templates and C++17 Class Template Argument Deduction (CTAD). Developers do not need to explicitly specify the types of mutexes being locked.

Instead of declaring multiple verbose wrapper lines, a single statement locks an arbitrary number of mutex types—even heterogeneous ones, such as a std::mutex alongside a std::shared_mutex or a custom BasicLockable type:

std::scoped_lock lock(standardMutex, sharedMutex, customMutex);

Unified Mutex Management

std::scoped_lock is designed as a direct, strictly superior replacement for std::lock_guard. It functions equally well with a single mutex:

std::scoped_lock lock(singleMutex); // Fully replaces std::lock_guard

When given a single mutex, std::scoped_lock avoids the overhead of the multi-lock deadlock avoidance algorithm and simply invokes lock() on the single object. By standardizing on std::scoped_lock, codebases can maintain a single, consistent locking pattern for both single-lock and multi-lock critical sections.