简体   繁体   中英

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 (eg 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 (eg 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), eg:

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 (ie x_ is explicitly stored) as it allows for:

  • batching/delaying updates (eg 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 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.

I think your ClassWithProxy interface is mixing wrappers/proxys and containers. For containers it is common to use accessors like

T& x();
const T& x() const;

just like the standard containers do, eg std::vector::at() . But normally access to members by reference breaks encapsulation. For containers it's a convinience and part of the design.

But you noted that a reference to T is not always available, so this will reduce the options to your ClassWithSetter interface, which should be a wrapper for T dealing with the way you store your type (while containers are dealing with the way you store objects). I would change the naming, to make clear, it might not be as efficient as a plain get/set.

T load() const;
void save(const T&);

or something more in context. Now it should be obvious, modifying T by using a proxy, again breaks encapsulation.

By the way, there is no reason not to use the wrapper inside of a container.

I think that possibly part of the problem with your set implementation is that your idea of how a const MyContainer<T>& would behave is inconsistent with how standard containers behave and therefore would likely confuse future code maintainers. The normal container type for "constant container, mutable elements" is const MyContainer<T*>& where you add a level of indirection to clearly indicate your intention to users.

This is how the standard containers work, and if you utilize that mechanism you don't need the underlying container to be mutable nor the set function to be const .

All that said I slightly prefer the set / get approach because if a particular attribute only needs a get you don't have to write a set at all.

However I prefer not writing any direct access to members (like get/set or proxy) but instead providing a meaningfully named interface through which clients can access the class functionality. In a trivial example to show my meaning, instead of set_foo(1); set_bar(2); generate_report(); set_foo(1); set_bar(2); generate_report(); prefer a direct interface like generate_report(1, 2); and avoid directly manipulating class attributes.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM