Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proxy object/reference getters vs setters?

When I am designing a generic class, I am often in dilemma between the following design choices:

template<class T>
class ClassWithSetter {
 public:
  T x() const; // getter/accessor for x
  void set_x(const T& x);
  ...
};
// vs
template<class T>
class ClassWithProxy {
  struct Proxy {
    Proxy(ClassWithProxy& c /*, (more args) */);
    Proxy& operator=(const T& x);  // allow conversion from T
    operator T() const;  // allow conversion to T
    // we disallow taking the address of the reference/proxy (see reasons below)
    T* operator&() = delete;
    T* operator&() const = delete;
    // more operators to delegate to T?
   private:
    ClassWithProxy& c_;
  };
 public:
  T x() const; // getter
  Proxy x();  // this is a generalization of: T& x();
  // no setter, since x() returns a reference through which x can be changed
  ...
}; 

Notes:

  • the reason why I return T instead of const T& in x() and operator T() is because a reference to x might not be available from within the class if x is stored only implicitly (e.g. suppose T = std::set<int> but x_ of type T is stored as std::vector<int>)
  • suppose caching of Proxy objects and/or x is not allowed

I am wondering what would be some scenarios in which one would prefer one approach versus the other, esp. in terms of:

  • extensibility / generality
  • efficiency
  • developer's effort
  • user's effort

?

You can assume that the compiler is smart enough to apply NRVO and fully inlines all the methods.

Current personal observations:

(This part is not relevant for answering the question; it just serves as a motivation and illustrates that sometimes one approach is better than the other.)

One particular scenario in which the setter approach is problematic is as follows. Suppose you're implementing a container class with the following semantics:

  • MyContainer<T>& (mutable, read-write) - allows modifying on both the container and its data implementation of the
  • MyContainer<const T>& (mutable, read-only) - allows modifying to the container but not its data
  • const MyContainer<T> (immutable, read-write) - allows modifying the data but not the container
  • const MyContainer<const T> (immutable, read-only) - no modifying to the container/data

where by "container modifications" I mean operations like adding/removing elements. If I implement this naively with a setter approach:

template<class T>
class MyContainer {
 public:
   void set(const T& value, size_t index) const {  // allow on const MyContainer&
     v_[index] = value;  // ooops,
     // what if the container is read-only (i.e., MyContainer<const T>)?
   }
   void add(const T& value);  // disallow on const MyContainer&
   ...
 private:
  mutable std::vector<T> v_;
};

The problem could be mitigated by introducing a lot of boilerplate code that relies on SFINAE (e.g. by deriving from a specialized template helper which implements both versions of set()). However, a bigger problem is that this brakes the common interface, as we need to either:

  • ensure that calling set() on an read-only container is a compile error
  • provide a different semantics for the set() method for read-only containers

On the other hand, while the Proxy-based approach works neatly:

template<class T>
class MyContainer {
   typedef T& Proxy;
 public:
   Proxy get(const T& value, size_t index) const {  // allow on const MyContainer&
     return v_[index];  // here we don't even need a const_cast, thanks to overloading
   }
   ...
};

and the common interface and semantics is not broken.

One difficulty I see with the proxy approach is supporting the Proxy::operator&() because there might be no object of type T stored / a reference to available (see notes above). For example, consider:

T* ptr = &x();

which cannot be supported unless x_ is actually stored somewhere (either in the class itself or accessible through a (chain of) methods called on member variables), e.g.:

template<class T>
T& ClassWithProxy::Proxy::operator&() {
  return &c_.get_ref_to_x();
}

Does that mean that the proxy object references are actually superior when T& is available (i.e. x_ is explicitly stored) as it allows for:

  • batching/delaying updates (e.g. imagine the changes are propagated from the proxy class destructor)
  • better control over caching ?

(In that case, the dilemma is between void set_x(const T& value) and T& x().)

Edit: I changed the typos in constness of setters/accessors

like image 257
eold Avatar asked May 30 '14 19:05

eold


1 Answers

Like most design dilemmas, I think this depends on the situation. Overall, I would prefer the getters and setters pattern, as it is simpler to code (No need for a proxy class for every field), simpler to understand by another person (looking at your code), and more explicit in certain circumstances. However, there are situations where proxy classes can simplify user experience and hide implementation details. A few examples:

If your container is some sort of associative array, you might overload operator[] for getting and setting the value for a particular key. However, if a key hasn't been defined, you might need a special operation for adding it. Here a proxy class would probably be the most convenient solution, as it can handle = assignment in different ways as necessary. However, this can mislead users: If this particular data structure has different times for adding vs setting, using a proxy makes this difficult to see, while using a set and put method set can make it clear the separate time used by each operation.

What if the container does some sort of compression on T and stores the compressed form? While you could use a proxy which did the compression/decompression whenever necessary, it would hide the cost associated with de/re compression from the user, and they might use it as if it were a simple assignment without heavy computation. By creating getter/setter methods with appropriate names, it can be made more apparent that they take significant computational effort.

Getters and setters also seem more extensible. Making a getter and setter for a new field is easy, while making a proxy which forwards the operations for every property would be an error-prone annoyance. What if you later need to extend your container class? With getters and setters, just make them virtual and override them in the subclass. For proxies, you might have to make a new proxy struct in each subclass. To avoid breaking encapsulation you probably should make your proxy struct use the superclasses's proxy struct to do some of the work, which could get quite confusing. With getters/setters, just call the super getter/setter.

Overall, getters and setters are easier to program, understand and change, and they can make visible the costs associated with an operation. So, in most situations, I would prefer them.

like image 154
Straw1239 Avatar answered Oct 19 '22 05:10

Straw1239