当我设计一个通用类时,我经常处于以下设计选择之间的两难境地:
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
...
};
注意:
T
和const T&
中返回x()
而不是operator T()
的原因是因为在课堂上可能无法提及对x
的引用如果x
仅存储隐式(例如,假设T = std::set<int>
,x_
类型T
存储为std::vector<int>
)x
不允许 我想知道在某种情况下,人们更喜欢一种方法而不是另一种方式,尤其是。就:
您可以假设编译器足够智能以应用NRVO并完全内联所有方法。
目前的个人观察:
(这部分与回答问题无关;它只是作为一种动机,并说明有时一种方法比另一种更好。)
setter方法存在问题的一个特定情况如下。假设您正在实现具有以下语义的容器类:
MyContainer<T>&
(可变,读写) - 允许修改容器及其数据
MyContainer<const T>&
(可变,只读) - 允许修改容器但不允许修改数据const MyContainer<T>
(不可变,读写) - 允许修改数据而不是容器const MyContainer<const T>
(不可变,只读) - 不修改容器/数据通过&#34;容器修改&#34;我的意思是添加/删除元素等操作。如果我用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()
的两个版本的专用模板助手派生),可以减轻问题。但是,更大的问题是这个制动公共接口,因为我们需要:
另一方面,虽然基于代理的方法工作得很好:
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()
之间。)
编辑:我更改了setter / accessors的常量中的拼写错误
答案 0 :(得分:1)
与大多数设计困境一样,我认为这取决于具体情况。总的来说,我更喜欢getter和setter模式,因为它更简单的代码(不需要每个字段的代理类),更容易被另一个人理解(查看你的代码),在某些情况下更明确。但是,在某些情况下,代理类可以简化用户体验并隐藏实现细节。几个例子:
如果您的容器是某种关联数组,则可能会重载operator []以获取和设置特定键的值。但是,如果尚未定义某个键,则可能需要执行特殊操作才能添加该键。这里的代理类可能是最方便的解决方案,因为它可以根据需要以不同的方式处理=赋值。但是,这可能会误导用户:如果此特定数据结构具有不同的添加时间与设置,则使用代理会使这很难看到,而使用set和put方法设置可以清楚地表明每个操作使用的单独时间。 / p>
如果容器在T上进行某种压缩并存储压缩形式怎么办?虽然您可以使用在必要时执行压缩/解压缩的代理,但它会隐藏与用户进行压缩/解压缩相关的成本,并且他们可能会使用它,就像它是一个简单的分配而没有繁重的计算。通过使用适当的名称创建getter / setter方法,可以更明显地表明它们需要大量的计算工作。
Getters和setter似乎也更具可扩展性。为新字段制作getter和setter很容易,而制作代理可以转发每个属性的操作将是一个容易出错的烦恼。如果您以后需要扩展容器类怎么办?使用getter和setter,只需将它们设置为虚拟并在子类中覆盖它们。对于代理,您可能必须在每个子类中创建一个新的代理结构。为了避免破坏封装,你可能应该让你的代理结构使用超类的代理结构来完成一些工作,这可能会让人很困惑。使用getter / setter,只需调用super getter / setter。
总的来说,getter和setter更容易编程,理解和更改,并且可以显示与操作相关的成本。所以,在大多数情况下,我更喜欢它们。
答案 1 :(得分:0)
我认为你的ClassWithProxy接口正在混合包装器/代理和容器。对于容器,通常使用诸如
之类的访问器T& x();
const T& x() const;
就像标准容器一样,例如std::vector::at()
。但通常通过引用访问成员会破坏封装。对于容器来说,它是一种便利,也是设计的一部分。
但是你注意到T
的引用并不总是可用,所以这将减少ClassWithSetter接口的选项,这对于T
处理应该是包装器与存储类型的方式(当容器处理存储对象的方式时)。我会更改命名,要弄清楚,它可能不如普通的get / set那样有效。
T load() const;
void save(const T&);
或其他更多内容。现在显而易见的是,通过使用代理修改T
,再次打破了封装。
顺便说一下,没有理由不在容器内部使用包装器。
答案 2 :(得分:0)
我认为您的set
实现可能存在的部分问题是,您对const MyContainer<T>&
行为方式的看法与标准容器的行为方式不一致,因此可能会混淆未来的代码维护者。 “常量容器,可变元素”的常规容器类型为const MyContainer<T*>&
,您可以在其中添加间接级别以清楚地表明您对用户的意图。
这是标准容器的工作方式,如果您使用该机制,则不需要底层容器是可变的,set
函数也不需要const
。
所有这一切都说我稍微偏好set
/ get
方法,因为如果某个特定属性只需要get
,则根本不需要编写set
但是,我不希望直接访问成员(例如get / set或proxy),而是提供一个有意义的命名接口,客户端可以通过该接口访问类功能。在显示我的意义的一个简单示例中,而不是set_foo(1); set_bar(2); generate_report();
更喜欢直接接口,如generate_report(1, 2);
,并避免直接操作类属性。