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 ofconst T&
inx()
andoperator T()
is because a reference tox
might not be available from within the class ifx
is stored only implicitly (e.g. supposeT = std::set<int>
butx_
of typeT
is stored asstd::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 theMyContainer<const T>&
(mutable, read-only) - allows modifying to the container but not its dataconst MyContainer<T>
(immutable, read-write) - allows modifying the data but not the containerconst 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