代理对象/参考getter vs setter?

时间:2014-05-30 19:28:24

标签: c++ proxy getter-setter proxy-classes

当我设计一个通用类时,我经常处于以下设计选择之间的两难境地:

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
  ...
}; 

注意:

  • 我在Tconst 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()的两个版本的专用模板助手派生),可以减轻问题。但是,更大的问题是这个制动公共接口,因为我们需要:

  • 确保在只读容器上调用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()之间。)

编辑:我更改了setter / accessors的常量中的拼写错误

3 个答案:

答案 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);,并避免直接操作类属性。