1

Is there a way to code a write-only reference to an object? For example, suppose there was a mutex class:

template <class T> class mutex {
protected:
   T _data;
public:
   mutex();
   void lock(); //locks the mutex
   void unlock(); //unlocks the mutex
   T& data(); //returns a reference to the data, or throws an exception if lock is unowned
};

Is there a way to guarantee that one couldn't do this:

mutex<type> foo;
type& ref;
foo.lock();
foo.data().do_stuff();
ref = foo.data();
foo.unlock();
//I have a unguarded reference to foo now

On the other hand, is it even worth it? I know that some people assume that programmers won't deliberately clobber the system, but then, why do we have private variables in the first place, eh? It'd be nice to just say it's "Undefined Behavior", but that just seems a little bit too insecure.

EDIT: OK, i understand the idea of a setter routine, but how would this be accomplished?

mutex<vector<int> > foo;
foo.lock();
for (int i=0; i < 10; i++) {
   foo.data().push_back(i);
}

foo.unlock(); Using a set routine would require a copy for each write:

mutex<vector<int> > foo;
foo.lock();
for (int i=0; i < 10; i++) {
   vector<int> copy = foo.read();
   copy.push_back(i);
   foo.write(copy);
}

though you could trivially optimize in this specific case, if, say, several different threads are all pushing elements, and maybe even erasing a few, this can become quite a bit of excess memory copying (i.e. one per critical section).

Robert Mason
  • 3,949
  • 3
  • 31
  • 44
  • Note that `type& ref;` won't compile. (References have to be bound to an object immediately upon creation. And that's no ugly artifact of the syntax, this is wanted. In C++, it's considered good style to introduce variables as they are needed and it's considered bad style to list all variables at the top of the function/block. There are good, objective arguments for this.) – sbi Apr 23 '10 at 06:24
  • Based on the responses I'm getting, it appears that I should just *trust* that users aren't going to purposely try to game the system, cause if they want to they will be able to do whatever they want anyway no matter how many guards I put up. Just say "do not create a copy of the reference returned by data". – Robert Mason Apr 23 '10 at 11:16
  • @Robert: You could create a proxy object that acts like a reference to the actual data becomes invalid on `unlock()`. I'll go and add that to my answer. – sbi Apr 24 '10 at 11:17
  • @Robert: On second thought, the `lock` class in the second part of my answer already does this. You have access to the data only through the lock, and the lock is released (`unlock()` called on the mutex) when it leaves the scope. After that, the lock is gone and so is the access. Of course, users can still store a reference to the data protected by the mutex/lock. But taking a reference guarded by a lock is less likely to happen accidentally. And C++' philosophy has always been to protect against Murphy (bad luck), not Machiavelli (bad intention). – sbi Apr 24 '10 at 11:24
  • "And C++' philosophy has always been to protect against Murphy (bad luck), not Machiavelli (bad intention)." Well said. I guess i've been doing too much GUI work recently- The cardinal rule of GUI development is to assume the user is an idiot :P – Robert Mason Apr 25 '10 at 01:04
  • 1
    @Robert: But GUI users _are_ idiots, we all know that. `:)` C++ programmers, on the other hand... - No no no, I'll keep my mouth well shut. – sbi Apr 26 '10 at 14:26

5 Answers5

2

Yes, you can create a wrapper class that becomes invalidated when unlock is called and return the wrapper, instead of returning the reference, and you can overload its assignment operator to assign to the reference. The trick is that you need to hang onto a reference to the wrapper's internal data, so that when unlock is called, prior to releasing the lock, you invalidate any wrappers that you have created.

Michael Aaron Safyan
  • 93,612
  • 16
  • 138
  • 200
2

The common way to differentiate between getters and setters is by the const-ness of the object:

template <class T> class mutex {
public:
   mutex();
   void lock();
   void unlock();
         T& data();       // cannot be invoked for const objects
   const T& data() const; // can be invoked for const objects
protected:
   T _data;
};

Now, if you want to have read-only access, make the mutex const:

void read_data(const mutex< std::vector<int> >& data)
{
   // only const member functions can be called here
}

You can bind a non-const object to a const reference:

// ...
mutex< std::vector<int> > data;
data.lock();
read_data(data);
data.unlock();
// ...

Note that the lock() and unlock() functions are inherently unsafe in the face of exceptions:

void f(const mutex< std::vector<int> >& data)
{
  data.lock();
  data.data().push_back(42); // might throw exception
  data.unlock(); // will never be reached in push_back() throws
}

The usual way to solve this is RAII (resource acquisition is initialization):

template <class T> class lock;

template <class T> class mutex {
public:
   mutex();
protected:
   T _data;
private:
   friend class lock<T>;
   T& data();
   void lock();
   void unlock();
};

template <class T> class lock {
public:
  template <class T> {
  lock(mutex<T>& m) m_(m) {m_.lock();}
  ~lock()                 {m_.unlock();}

         T& data()        {return m_.data();}
   const T& data() const  {return m_.data()}
private:
  mutex<T>& m_;
};

Note that I have also moved the accessor functions to the lock class, so that there is no way to access unlocked data.

You can use this like this:

void f(const mutex< std::vector<int> >& data)
{
  {
    lock< std::vector<int> > lock_1(data);
    std::cout << lock1.data()[0]; // fine, too
    lock1.data().push_back(42);   // fine
  }
  {
    const lock< std::vector<int> > lock_2(data); // note the const
    std::cout << lock1.data()[0];  // fine, too
    // lock1.data().push_back(42); // compiler error
  }
}
sbi
  • 219,715
  • 46
  • 258
  • 445
1

You could encapsulate the data as private and expose a write routine. Within that routine you could lock your mutex, giving you similar behavior to what you are shooting for.

fbrereto
  • 35,429
  • 19
  • 126
  • 178
  • Bingo. You lock inside the write method. If you don't want a caller to block you can (with pthreads) use `pthread_mutex_trylock()` – Brian Roach Apr 23 '10 at 00:10
0

You can use a member function as the following:

void set_data(const T& var);

This is how write-only access is applied in C++.

Khaled Alshaya
  • 94,250
  • 39
  • 176
  • 234
  • Yes, that works, but doesn't it seem so much more intuitive and flexible to use an assignment operator instead of a setter function? Perhaps overloading operator=(T&)? – Robert Mason Apr 22 '10 at 23:58
  • I think when you overload the assignment operator the right-hand side of the assignment should be of the same type. This is what I do usually. – Khaled Alshaya Apr 23 '10 at 00:16
-2

No. There is no way to guarantee anything about reading and writing memory in unsafe languages like C++, where all the memory is treated like one big array.


[Edit] Not sure why all the downvotes; this is correct and relevant.

In safe languages, such as Java or C#, you can certainly guarantee that, for instance, properly-implemented immutable types will stay immutable. Such a guarantee can never be made in C++.

The fear is not so much malicious users as it is accidental invalid-pointers; I have worked on C++ projects where immutable types have been mutated due to an invalid pointer in completely unrelated code, causing bugs that are extremely difficult to track down. This guarantee - which only safe languages can make - is both useful and important.

BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283
  • 2
    Then what is the point of `const`? It is possible to get around it, but the point is to discourage it. – Zifre Apr 23 '10 at 00:00
  • 2
    @Zifre: The question specifically asked if this could be **guaranteed**, not how to discourage it. My answer is 100% correct. – BlueRaja - Danny Pflughoeft Apr 23 '10 at 00:08
  • The closest you'll get to a guarantee is private data and OS-level synchronization primitives. If the `T` is small enough, you could even do atomic writes, but I'm guessing this won't solve the problem if you're talking about multiple threads writing to some object. (FWIW, if you want explicit client locking, make the lock object a member class. Then you keep scope along with any references you take.) – dash-tom-bang Apr 23 '10 at 00:47
  • 3
    Correct or not, your answer is worthless. Nothing is guaranteed in any language if you assume malicious users. So why bother? – Dennis Zickefoose Apr 23 '10 at 00:57