Skip to content

Instantly share code, notes, and snippets.

@kristopherjohnson
Last active June 5, 2025 01:52
Show Gist options
  • Save kristopherjohnson/f4352bdf35458845bc5deb1265979687 to your computer and use it in GitHub Desktop.
Save kristopherjohnson/f4352bdf35458845bc5deb1265979687 to your computer and use it in GitHub Desktop.
C++ SynchronizedValue<T> class
#ifndef KJ_SYNCHRONIZEDVALUE_H
#define KJ_SYNCHRONIZEDVALUE_H
/*
Copyright 2025 Kristopher Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <functional>
#include <mutex>
#include <type_traits>
#include <utility>
namespace kj {
// Inspired by the C++ proposal for a synchronized_value<T> type
// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p0290r3.html,
// but compatible with C++11.
/// @brief Wrapper for a value that can be safely accessed from multiple
/// threads.
///
/// Use one of the `apply()` function templates to access the wrapped
/// value while a mutex is locked by the calling thread.
///
/// @tparam T Type of the value.
/// @tparam Mutex Type of the mutex to use for synchronization, which must be
/// compatible with `std::lock_guard`.
template <class T, class Mutex = std::mutex> class SynchronizedValue {
mutable Mutex mtx;
T value;
template <class R_, class F_, class T_, class M_>
friend R_ apply(F_ &&f, SynchronizedValue<T_, M_> &sv);
template <class R_, class F_, class T_, class M_>
friend R_ apply(F_ &&f, const SynchronizedValue<T_, M_> &sv);
template <class F_, class T_, class M_>
friend void apply(F_ &&f, SynchronizedValue<T_, M_> &sv);
template <class F_, class T_, class M_>
friend void apply(F_ &&f, const SynchronizedValue<T_, M_> &sv);
template <class F_, class T_, class U_, class M_, class N_>
friend void apply(F_ &&f, SynchronizedValue<T_, M_> &lhs,
SynchronizedValue<U_, N_> &rhs);
public:
using mutex_type = Mutex;
using value_type = T;
/// @brief Default constructor (only available if T is default constructible)
template <class U = T, class = class std::enable_if<
std::is_default_constructible<U>::value>::type>
SynchronizedValue() : value() {
}
/// @brief Constructs a synchronized value with a provided value.
/// @param desired Value to be copied into the synchronized value.
SynchronizedValue(const T &desired)
: value(desired) {
}
/// @brief Constructs a synchronized value with a provided value.
/// @param value Value to be moved into the synchronized value.
SynchronizedValue(T &&desired) : value(std::move(desired)) {}
~SynchronizedValue() = default;
SynchronizedValue(const SynchronizedValue &) = delete;
SynchronizedValue &operator=(const SynchronizedValue &) = delete;
/// @brief Apply a function to the synchronized value.
/// @param f
void apply(std::function<void(T &)> f) {
std::lock_guard<Mutex> lock(mtx);
f(value);
}
/// @brief Get a copy of the wrapped value.
/// @return A copy of the value.
T get() const {
std::lock_guard<Mutex> lock(mtx);
return value;
}
/// @brief Set the wrapped value from an lvalue reference.
/// @param desired New value to set.
void operator=(const T &desired) {
std::lock_guard<Mutex> lock(mtx);
value = desired;
}
/// @brief Set the wrapped value from an rvalue reference.
/// @param desired New value to set.
void operator=(T &&desired) {
std::lock_guard<Mutex> lock(mtx);
value = std::move(desired);
}
/// @brief Exchange values with another SynchronizedValue of the same type.
/// @param other The other instance to swap with.
void swap(SynchronizedValue &other) {
if (this == &other) {
return;
}
std::lock(mtx, other.mtx);
std::lock_guard<Mutex> lock_this(mtx, std::adopt_lock);
std::lock_guard<Mutex> lock_other(other.mtx, std::adopt_lock);
std::swap(value, other.value);
}
};
/// @brief Applies a function to the wrapped value of a SynchronizedValue.
/// @param f Function to apply, which takes a reference to the value type.
/// @param sv Synchronized value to apply the function to.
/// @return The result of the function application.
template <class R, class F, class T, class M>
R apply(F &&f, SynchronizedValue<T, M> &sv) {
std::lock_guard<M> lock(sv.mtx);
return f(sv.value);
}
/// @brief Applies a function to the wrapped value of a SynchronizedValue.
/// @param f Function to apply, which takes a reference to the value type.
/// @param sv Synchronized value to apply the function to.
/// @return The result of the function application.
template <class R, class F, class T, class M>
R apply(F &&f, const SynchronizedValue<T, M> &sv) {
std::lock_guard<M> lock(sv.mtx);
return f(sv.value);
}
/// @brief Applies a function to the wrapped value of a SynchronizedValue
/// with returning a result.
/// @param f Function to apply, which takes a reference to the value type.
/// @param sv Synchronized value to apply the function to.
/// @return The result of the function application.
template <class F, class T, class M>
void apply(F &&f, SynchronizedValue<T, M> &sv) {
std::lock_guard<M> lock(sv.mtx);
f(sv.value);
}
/// @brief Applies a function to the wrapped value of a SynchronizedValue
/// without returning a result.
/// @param f Function to apply, which takes a reference to the value type.
/// @param sv Synchronized value to apply the function to.
/// @return The result of the function application.
template <class F, class T, class M>
void apply(F &&f, const SynchronizedValue<T, M> &sv) {
std::lock_guard<M> lock(sv.mtx);
f(sv.value);
}
/// @brief Apply a function to the values of two SynchronizedValue objects.
/// @tparam F Function type, which must accept two references to the value
/// type
/// @tparam T Value type of first SynchronizedValue
/// @tparam U Value type of second SynchronizedValue
/// @tparam M Mutex type of first SynchronizedValue
/// @tparam N Mutex type of second SynchronizedValue
/// @param lhs First SynchronizedValue
/// @param rhs Second SynchronizedValue
template <class F, class T, class U, class M, class N>
void apply(F &&f, SynchronizedValue<T, M> &lhs, SynchronizedValue<U, N> &rhs) {
if ((void *)&lhs == (void *)&rhs) {
// same object, so only lock once
std::lock_guard<M> lock(lhs.mtx);
f(lhs.value, rhs.value);
} else {
std::lock(lhs.mtx, rhs.mtx);
std::lock_guard<M> lock_lhs(lhs.mtx, std::adopt_lock);
std::lock_guard<N> lock_rhs(rhs.mtx, std::adopt_lock);
f(lhs.value, rhs.value);
}
}
/// @brief Swaps the values of two SynchronizedValue objects.
/// @tparam T Value type
/// @tparam M Mutex type of first SynchronizedValue
/// @tparam N Mutex type of second SynchronizedValue
/// @param lhs First SynchronizedValue
/// @param rhs Second SynchronizedValue
template <class T, class M, class N>
void swap(SynchronizedValue<T, M> &lhs, SynchronizedValue<T, N> &rhs) {
if ((void *)&lhs == (void *)&rhs) {
return;
}
apply([](T &a, T &b) { std::swap(a, b); }, lhs, rhs);
}
} // namespace kj
#endif // KJ_SYNCHRONIZEDVALUE_H
@kristopherjohnson
Copy link
Author

kristopherjohnson commented May 30, 2025

Example usage:

#include <iostream>

#include "kj_synchronized_value.h"

using namespace kj;

int main() {
  SynchronizedValue<int> sv(42);

  std::cout << "Initial value: " << sv.get() << std::endl;

  // Example usage of apply to do something with the value.
  auto doubled = apply<int>([](int &value) { return value * 2; }, sv);
  std::cout << "Result of doubling: " << doubled << std::endl;

  // Mutate the value using apply
  apply([](int &value) { value += 10; }, sv); // or: sv.apply([](int &value) { value += 10; });
  std::cout << "Updated value after additions: " << sv.get() << std::endl;

  // Swap two synchronized values
  SynchronizedValue<std::string> hello("Hello, World!");
  SynchronizedValue<std::string> goodbye("Goodbye, World!");
  swap(hello, goodbye);
  std::cout << "Swapped Values: " << hello.get() << ", " << goodbye.get()
            << std::endl;

  return 0;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment