当我设计一个通用类时,我经常陷入以下设计选择的两难境地:
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
...
};
注意:
- 在
x()
和operator T()
中返回T
而不是const T&
的原因是,如果x
仅以隐式方式存储(例如,假设T = std::set<int>
,但类型为T
的x_
存储为std::vector<int>
),则可能无法从类内部获得对x
的引用。 - 假设禁止缓存代理对象和/或
x
我想知道在哪些情况下会更倾向于一种方法而不是另一种方法,特别是在以下方面:
- 可扩展性/通用性
- 效率
- 开发人员投入的努力
- 用户投入的努力
?
您可以假设编译器足够智能,能够应用NRVO并完全内联所有方法。
当前个人观察:
(此部分与回答问题无关;它只作为动机,并说明有时一种方法比另一种方法更好。)
一个特定的场景,其中setter方法存在问题如下。假设您正在实现以下语义的容器类:
MyContainer<T>&
(可变,读写)- 允许在容器和其数据上进行修改MyContainer<const T>&
(可变,只读) - 允许对容器进行修改但不允许对其数据进行修改const MyContainer<T>
(不可变,读写)- 允许修改数据但不允许修改容器const MyContainer<const T>
(不可变,只读)- 不允许对容器/数据进行修改
其中“容器修改”是指添加/删除元素等操作。如果我使用setter方法来天真地实现它:
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_;
};
问题可以通过引入大量依赖于SFINAE的样板代码来缓解(例如,从实现
set()
的两个版本的特殊化模板帮助类中派生)。然而,更大的问题在于,这种方法"破坏了常见接口",因为我们需要做到以下两点之一:
- 确保在只读容器上调用
set()
是一个编译错误 - 为只读容器提供不同的
set()
方法语义
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
}
...
};
而且常见的接口和语义不会被破坏。
我发现代理方法的一个困难是如何支持Proxy::operator&()
,因为可能没有类型 T
的对象存储/可用的引用(参见上面的注释)。例如,请考虑以下情况:
T* ptr = &x();
除非x_
实际存储在某个地方(可以是类本身中或通过调用成员变量的(链式)方法访问),否则无法支持该操作,例如:
template<class T>
T& ClassWithProxy::Proxy::operator&() {
return &c_.get_ref_to_x();
}
这是否意味着当
T&
可用时(即 x_
显式地存储),代理对象引用实际上更为优越,因为它允许:
- 批处理/延迟更新(例如,假设更改是从代理类析构函数传播的)
- 更好地控制缓存?
void set_x(const T& value)
和 T& x()
。)编辑:我更正了设置器/访问器中 constness 的拼写错误。
std::vector<T>
意味着容器上的const
适用于其中的元素,因为容器"拥有"其元素。因此,如果您想要这些语义,将std::vector<T>
存储而不使用mutable
是错误的。此外,我认为您的语义存在错误:不应该以这种方式使用const
。const_container_class
和container_class
比const container_class
更好。其次,如果您无论如何都不想破坏封装,代理(proxy)应该通过设置/获取方法转发。 - Yakk - Adam Nevraumontvoid set_x(const T& x)
与T& get()
(或Proxy
作为返回类型)来封装对x
的修改的方式。 - eold