| Document Number: | P0290R3 | 
| Date: | 2023-01-06 | 
| Author: | Anthony Williams | 
| Audience: | SG1 | 
apply() for synchronized_value<T>This paper is a followup to P0290R2, based on feedback from the Kona 2022 meeting.
The basic idea is that synchronized_value<T> stores a value of
      typeT and a mutex.The apply() function then provides a means of
      accessing the stored value with the mutex locked, automatically unlocking the mutex
      afterwards.
The apply() function is variadic, so you can operate on a set
      of synchronized_value<T> objects. All the mutexes are locked prior to
      invoking the supplied callable object, and then they are all released
      afterwards.
The name is chosen to fit in with std::apply for tuples,
      since the operation is conceptually similar.  Rather than expanding
      a std::tuple to supply the arguments to the function, the
      values wrapped by the synchronized_values are extracted to
      supply the arguments to the function.
This provides an easy way for developers to ensure that all accesses to a given object are done with the relevant mutex locked, whilst also allowing for operations that require locks on multiple objects.
In order to avoid simple mistakes when using the synchronized_value<T>
      objects, there are no public member functions or operations other than construction.
The actual implementation may use an alternative synchronization mechanism instead of a mutex, provided that the synchronization requirements are met.
Simple accesses can be done with simple lambdas:
synchronized_value<std::string> s;
std::string read_value(){
    return apply([](auto& x){return x;},s);
}
void set_value(std::string const& new_val){
    apply([&](auto& x){x=new_val;},s);
}
    
    More complex processing can be done with a more complex lambda, or a separate function or callable object:
synchronized_value<std::queue<message_type>> queue;
      
void process_message(){
    std::optional<message_type> local_message;
    apply([&](std::queue<message_type>& q){
        if(!q.empty()){
            local_message.emplace(std::move(q.front()));
            q.pop_front();
        }
    },queue);
    if(local_message)
        do_processing(local_message.value());
}
    
    The variadic nature of apply() means that writing code that accesses
      multiple synchronized_value<T> objects is straightforward. It uses the same
      mechanism as std::lock() to ensure that the requisite mutexes are locked without
      deadlock.
The ubiquitous example of transferring money between accounts can then be simply written as a follows:
void transfer_money(
    synchronized_value<account>& from_,
    synchronized_value<account>& to_,
    money_value amount){
    apply([=](auto& from,auto& to){
        from.withdraw(amount);
        to.deposit(amount);
    },from_,to_);
}
    
    Add a new section to chapter 30 as follows.
30.x Synchronized Values
This section describes a class template to provide locked access to a value in order to facilitate the construction of race-free programs.
Header <synchronized_value> synopsis
namespace std { template<class T> class synchronized_value; template<class F,class FirstValue,class ... OtherValues> decltype(auto) apply( F&& f,synchronized_value<FirstValue>& first_value,synchronized_value<ValueTypes>&... other_values); }30.x.1 Class template
synchronized_valuenamespace std { template<class T> class synchronized_value { public: synchronized_value(synchronized_value const&) = delete; synchronized_value& operator=(synchronized_value const&) = delete; template<class ... Args> synchronized_value(Args&& ... args); ~synchronized_value(); private: T __value; // exposition only std::mutex __mut; // exposition only }; }An object of type
synchronized_value<T>wraps an object of typeT. The wrapped object can be accessed by passing a callable object or function toapply. All such accesses are done with a lock held to ensure that only one thread may be accessing the wrapped object for a givensynchronized_valueat a time.
template<class ... Args> synchronized_value(Args&& ... args);
- Constraints:
is_constructible_v<T,Args...>istrue- Effects:
- Direct-non-list-initializes the contained value with
std::forward<Args>(args)....- Throws:
- Any exceptions thrown by the selected constructor of
T.
std::system_errorif any necessary resources cannot be acquired.
~synchronized_value();
- Effects:
- Destroys the contained object of type
Tand*this.30.x.2
applyfunction
template<class F,class FirstValue,class ... OtherValues> decltype(auto) apply( F&& f,synchronized_value<FirstValue>& first_value,synchronized_value<ValueTypes>&... other_values);
- Effects:
- Equivalent to:
scoped_lock lock(first_value.__mut,other_values.__mut...); return INVOKE(std::forward<F>(f),first_value.__value,other_values.__value...);- Returns:
- The return value of the invocation of
f.- Throws:
std::system_errorif there was an error acquiring any of the locks. Any exceptions thrown by the invocation off.- Synchronization:
- Multiple threads may call
apply()concurrently without external synchronization. If multiple threads callapply()concurrently passing the same instance(s) ofsynchronized_valuethen the behaviour is as-if they each made their call in some unspecified order. The completion of the full expression associated with one invocation ofapplysynchronizes-with a subsequent invocation ofapplywhere the same instance ofsynchronized_valueis passed to both invocations ofapply.- Requires:
- A single instance of
synchronized_valueshall not be passed more than once to the same invocation ofapply. [Example:synchronized_value<int> sv; void f(int,int); apply(f,sv,sv); // undefined behaviour, sv passed more than once to same call—End Example]
The invocation offshall not callapplydirectly or indirectly passing any offirst_value, other_values....
Following discussion in LWG in Kona, the following changes have been made:
Following discussion in LEWG in Kona, the following changes have been made:
std::apply for
      tuples, and is a better match when a synchronized_value is supplied.Following discussion in SG1 in Oulu, the following changes have been made:
apply with the same synchronized_value more than once in
      the same argument list is explicitly disallowed;apply with an overlapping set
      of synchronized_value objects is explicitly disallowed; andsynchronized_value may throw std::system_error.