views:

164

answers:

5

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).

A: 

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++.

AraK
Robert Mason
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.
AraK
A: 

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
Then what is the point of `const`? It is possible to get around it, but the point is to discourage it.
Zifre
@Zifre: The question specifically asked if this could be **guaranteed**, not how to discourage it. My answer is 100% correct.
BlueRaja - Danny Pflughoeft
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
Correct or not, your answer is worthless. Nothing is guaranteed in any language if you assume malicious users. So why bother?
Dennis Zickefoose
@Dennis: That is not true. See edit.
BlueRaja - Danny Pflughoeft
+1  A: 

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
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
+2  A: 

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
+1  A: 

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