用于自动 getter-setter 方法的 C++ class 模板 - good/bad 实践?
C++ class template for automatic getter-setter methods - good/bad practice?
使用带有隐式 getters 和 setters 的模板 class 对象作为(几乎)POD classes 中的属性是好的做法吗?
考虑以下模板示例:
template<typename T>
class Attribute
{
protected:
T m_val;
public:
T * getAddress() { return &m_val; }
T get() { return m_val; }
void set(T v) { m_val = v; }
};
及其用法:
class A
{
public:
Attribute<float> floatAttr;
Attribute<int> intAttr;
Attribute<long> longAttr;
};
这样就可以封装数据,但实现开销更少。
这是好事还是坏事(为什么)?
编辑:陈述我在其中看到的优势。没有必要手工实现每一个gettersetter函数,但是这些函数还是有通常的优点:
- 数据被封装,客户端需要用到gettersetter的功能,以后还是可以换个方式实现。
- 内部用法仍然隐藏,可以更改。
- Getter 和 Setter 函数可以作为 lambda 函数传递。
在其他一些语言中,getters 和 setters 是一种防止实现细节逃逸到接口中的方法;一旦你直接公开了一个字段,你以后可能无法在不更改代码中访问该字段的所有站点的情况下重新实现为 属性(使用 getter 和 setter 函数) .
在 C++ 中,这并不(非常)适用。您可以将任何字段的类型更改为覆盖 operator=
的 class,并隐式转换为所需的类型(对于“get”端)。 (当然有一些不适用的用途;例如,如果在客户端代码中创建了对该字段的指针或引用 - 尽管我个人会避免这样做并认为这是可疑的做法)。
此外,由于 C++ 是静态类型的,工具(IDE 等)也更容易提供自动重构,如果您需要通过适当的调用将字段和相应的访问更改为 getter/setter 对.
作为证据,这里是对您的 Attribute
模板的更改,它允许您的“属性”就像一个直接公开的字段(除了 &
将 return属性的地址而不是隐藏字段):
template<typename T>
class Attribute
{
protected:
T m_val;
public:
operator T() { return m_val; }
T &operator=(const T &a) { m_val = a; return m_val; }
};
如果您真的想要,您也可以覆盖operator&
:
T *operator&() { return &m_val; }
...但是这样做在很大程度上破坏了封装(就此而言,您可以考虑将 operator=
的 return 类型更改为 T
或 void
同样的道理)。
如果您最初直接公开该字段,则可以将其定义替换为上述模板的实例,并且大多数使用不会受到影响。这说明了 getter/setter 模式在 C++ 中并不总是必需的原因之一。
您自己的解决方案虽然封装了 getter/setter 函数后面的原始字段,但实际上还公开了另一个字段:Attribute<T>
成员(您的示例中的 floatAttr
等)。为了将其用作封装方式,您依赖于用户不知道(或不关心)属性字段本身的类型;也就是说,您希望没有人这样做:
A a;
Attribute<float> & float_attr = a.floatAttr;
当然,如果他们不这样做并以您想要的方式访问字段,那么以后确实可以通过更改“属性”字段的类型来更改实现:
A a;
float f = a.floatAttr.get();
...所以从这个意义上说,你实现了一些封装;真正的问题是有更好的方法来做到这一点。 :)
最后,值得一提的是,您提出的 Attribute
模板以及我在上面显示的替代方案都将字段移动到 class(某些 T 为 Attribute<T>
)与原始父 class (A
) 分开。如果要更改实现,现在在某种程度上受此事实的限制;属性对象自然不会引用包含它的对象。例如,假设我有一个 class B
,它有一个属性 level
:
class B {
public:
Attribute<int> level;
};
现在假设我稍后添加一个“最低级别”字段,min_level
:
class B {
public:
Attribute<int> level;
Attribute<int> min_level;
};
此外,假设我现在想在赋值时将 level
限制为 min_level
的值。这不会是直截了当的!虽然我可以给 level
一个带有自定义实现的新类型,但它将无法访问包含对象的 min_level
值:
class LevelAttribute {
int m_val;
public:
T &operator=(const T &a) {
m_val = std::max(min_level, a); // error - min_level not defined
}
}
要让它工作,您需要将包含对象传递给 LevelAttribute
对象,这意味着要存储一个额外的指针。典型的老式 setter 直接在保存字段的 class 中声明为函数,避免了这个问题。
使用带有隐式 getters 和 setters 的模板 class 对象作为(几乎)POD classes 中的属性是好的做法吗?
考虑以下模板示例:
template<typename T>
class Attribute
{
protected:
T m_val;
public:
T * getAddress() { return &m_val; }
T get() { return m_val; }
void set(T v) { m_val = v; }
};
及其用法:
class A
{
public:
Attribute<float> floatAttr;
Attribute<int> intAttr;
Attribute<long> longAttr;
};
这样就可以封装数据,但实现开销更少。
这是好事还是坏事(为什么)?
编辑:陈述我在其中看到的优势。没有必要手工实现每一个gettersetter函数,但是这些函数还是有通常的优点:
- 数据被封装,客户端需要用到gettersetter的功能,以后还是可以换个方式实现。
- 内部用法仍然隐藏,可以更改。
- Getter 和 Setter 函数可以作为 lambda 函数传递。
在其他一些语言中,getters 和 setters 是一种防止实现细节逃逸到接口中的方法;一旦你直接公开了一个字段,你以后可能无法在不更改代码中访问该字段的所有站点的情况下重新实现为 属性(使用 getter 和 setter 函数) .
在 C++ 中,这并不(非常)适用。您可以将任何字段的类型更改为覆盖 operator=
的 class,并隐式转换为所需的类型(对于“get”端)。 (当然有一些不适用的用途;例如,如果在客户端代码中创建了对该字段的指针或引用 - 尽管我个人会避免这样做并认为这是可疑的做法)。
此外,由于 C++ 是静态类型的,工具(IDE 等)也更容易提供自动重构,如果您需要通过适当的调用将字段和相应的访问更改为 getter/setter 对.
作为证据,这里是对您的 Attribute
模板的更改,它允许您的“属性”就像一个直接公开的字段(除了 &
将 return属性的地址而不是隐藏字段):
template<typename T>
class Attribute
{
protected:
T m_val;
public:
operator T() { return m_val; }
T &operator=(const T &a) { m_val = a; return m_val; }
};
如果您真的想要,您也可以覆盖operator&
:
T *operator&() { return &m_val; }
...但是这样做在很大程度上破坏了封装(就此而言,您可以考虑将 operator=
的 return 类型更改为 T
或 void
同样的道理)。
如果您最初直接公开该字段,则可以将其定义替换为上述模板的实例,并且大多数使用不会受到影响。这说明了 getter/setter 模式在 C++ 中并不总是必需的原因之一。
您自己的解决方案虽然封装了 getter/setter 函数后面的原始字段,但实际上还公开了另一个字段:Attribute<T>
成员(您的示例中的 floatAttr
等)。为了将其用作封装方式,您依赖于用户不知道(或不关心)属性字段本身的类型;也就是说,您希望没有人这样做:
A a;
Attribute<float> & float_attr = a.floatAttr;
当然,如果他们不这样做并以您想要的方式访问字段,那么以后确实可以通过更改“属性”字段的类型来更改实现:
A a;
float f = a.floatAttr.get();
...所以从这个意义上说,你实现了一些封装;真正的问题是有更好的方法来做到这一点。 :)
最后,值得一提的是,您提出的 Attribute
模板以及我在上面显示的替代方案都将字段移动到 class(某些 T 为 Attribute<T>
)与原始父 class (A
) 分开。如果要更改实现,现在在某种程度上受此事实的限制;属性对象自然不会引用包含它的对象。例如,假设我有一个 class B
,它有一个属性 level
:
class B {
public:
Attribute<int> level;
};
现在假设我稍后添加一个“最低级别”字段,min_level
:
class B {
public:
Attribute<int> level;
Attribute<int> min_level;
};
此外,假设我现在想在赋值时将 level
限制为 min_level
的值。这不会是直截了当的!虽然我可以给 level
一个带有自定义实现的新类型,但它将无法访问包含对象的 min_level
值:
class LevelAttribute {
int m_val;
public:
T &operator=(const T &a) {
m_val = std::max(min_level, a); // error - min_level not defined
}
}
要让它工作,您需要将包含对象传递给 LevelAttribute
对象,这意味着要存储一个额外的指针。典型的老式 setter 直接在保存字段的 class 中声明为函数,避免了这个问题。