When I develop a general class, I often encounter a dilemma between the following design options:
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 I return
T instead of const T& in x() and operator T() is because the reference to x may not be available from the class if x is only stored implicitly (for example, suppose T = std::set<int> , but x_ type T stored as std::vector<int> ) - Assume caching of proxy and / or
x objects is not allowed
I am wondering what some scenarios will be in which one approach may be preferred over another, especially. in terms:
- extensibility / generality
- efficiency
- developer efforts
- custom effort
?
We can assume that the compiler is smart enough to use NRVO and fully embeds all methods.
Current personal observations:
(This part is not relevant for answering the question; it simply serves as a motivation and illustrates that sometimes one approach is better than another.)
One specific scenario in which the setter approach is problematic is as follows. Suppose you implement a container class with the following semantics:
MyContainer<T>& (mutable, read-write) - allows you to change both the container and its data implementationMyContainer<const T>& (mutable, read-only) - allows you to modify the container, but not its dataconst MyContainer<T> (immutable, read-write) - allows you to modify data, but not the containerconst MyContainer<const T> (immutable, read-only) - does not change for container / data
where by “container modifications” I mean operations such as adding / removing elements. If I naively implement this using the setter approach:
template<class T> class MyContainer { public: void set(const T& value, size_t index) const {
The problem can be reduced by introducing a lot of template code that relies on SFINAE (for example, by getting a specialized template helper that implements both versions of set() ). However, the big problem is that it slows down the general interface, since we need either:
- make sure calling set () in a read-only container is a compilation error
- provide different semantics for the set () method for read-only containers.
On the other hand, although the proxy-based approach works neatly:
template<class T> class MyContainer { typedef T& Proxy; public: Proxy get(const T& value, size_t index) const {
and the general interface and semantics are not violated.
One of the difficulties that I see with the proxy approach is Proxy::operator&() because there cannot be an object of type T that is stored / referenced to an accessible one (see notes above). For example, consider:
T* ptr = &x();
which cannot be supported if x_ is not actually stored anywhere (either in the class itself or through methods (chains) called in member variables), for example:
template<class T> T& ClassWithProxy::Proxy::operator&() { return &c_.get_ref_to_x(); }
Does this mean that references to proxy objects are really superior when T& is available (i.e. x_ explicitly stored), since it allows:
- batch / update delays (for example, imagine changes propagate from a proxy class destructor)
- better caching control?
(In this case, the dilemma is between void set_x(const T& value) and T& x() .)
Edit: I changed typos in constness seters / accessors