定义非常量“get”成员函数的原因?
Posted
技术标签:
【中文标题】定义非常量“get”成员函数的原因?【英文标题】:Reasons for defining non-const 'get' member functions? 【发布时间】:2019-05-10 00:25:48 【问题描述】:我正在通过 Stroustrup 的(使用 C++ 的编程原理和实践)一书学习 C++。在练习中,我们定义了一个简单的结构:
template<typename T>
struct S
explicit S(T v):valv ;
T& get();
const T& get() const;
void set(T v);
void read_val(T& v);
T& operator=(const T& t); // deep copy assignment
private:
T val;
;
然后要求我们定义一个 const 和一个非 const 成员函数来获取 val
。
我想知道:在任何情况下让非常量 get
函数返回 val
有意义吗?
在我看来,我们不能在这种情况下间接地改变价值。哪些用例需要 const 和非 const get
函数来返回成员变量?
【问题讨论】:
IMO 甚至更清洁的人根本不会是 getter 或 setter,而是让课程与行为有关。如果您有成员变量的 getter 和 setter,您不妨将其设为public
。
使用非常量 get 成员函数(或任何 getter 和 setter 反模式)的主要原因是避免进行正确的面向对象编程。
@Someprogrammerdude 不是真的。也许现在该字段只是一个普通值,但您知道当您要迭代开发新需求时,它必须是一个计算值,因此拥有一个 getter 可以避免更改访问该字段的所有代码。另外:拥有一个 getter,您可以轻松在其上添加断点,并在调试器中检查访问它的代码在哪里。如果您只有一个字段,则必须在访问该字段的每个地方都放置断点,这会比较麻烦。
一个 const getter 是有意义的private 允许仅通过构造函数和 setter 进行修改(这将保证不变量)。一般来说,根据我的经验,setter 和 getter,尤其是非常量 getter,通常是口头和书面传统的结果,而不是针对特定案例的特定想法的结论。但是,请注意,Stroustrup 的示例是一个通用案例,旨在简单地介绍这个想法。
@Bakuriu 我猜 Some 指的是其他访问私有字段的类首先是糟糕的设计。只是一个愚蠢的例子:糟糕的设计是有一个customer::getMoney()
和customer::setMoney(int)
,而实际上它应该是一个customer::pay(int)
。 pay
涵盖了您所有的问题,而且您不会破坏封装
【参考方案1】:
首先让我重新表述你的问题:
为什么要为成员设置一个非常量的 getter,而不仅仅是将成员公开?
几个可能的原因原因:
1。易于检测
谁说非常量 getter 必须是公正的:
T& get() return val;
?很可能是这样的:
T& get()
if (check_for_something_bad())
throw std::runtime_error
"Attempt to mutate val when bad things have happened");
return val;
然而,正如@BenVoigt 所建议的那样,等到调用者真正尝试通过引用改变值时再发出错误更合适。
2。文化大会/“老板是这么说的”
一些组织强制执行编码标准。这些编码标准有时是由可能过度防御的人编写的。因此,您可能会看到如下内容:
除非您的班级是 "plain old data" type,否则任何数据成员都不得公开。您可以根据需要对此类非公共成员使用 getter 方法。
然后,即使特定类只允许非常量访问是有意义的,它也不会发生。
3。也许val
不存在?
您已经给出了一个示例,其中val
实际上存在在类的一个实例中。但实际上 - 它不必! get()
方法可以返回某种proxy object,它在分配、变异等时执行一些计算(例如,在数据库中存储或检索数据;或者翻转一点,它本身不像对象需要的那样可寻址是)。
4。允许稍后更改类内部而不更改用户代码
现在,阅读上面的第 1. 或第 3 项,您可能会问“但我的 struct S
确实有val
!”或“我的get()
没有做任何有趣的事情!” - 嗯,真的,他们没有;但是您将来可能希望更改此行为。如果没有get()
,您班级的所有用户都需要更改他们的代码。有了get()
,只需要修改struct S
的实现即可。
现在,我不提倡这种设计方法,但有些程序员会这样做。
【讨论】:
“当坏事发生时尝试改变 val”是不正确的。const
或非const
成员函数的选择取决于this
对象的限定条件,而不是如何使用返回值。特别是,非const
getter 不能假设正在发生突变。
@BenVoigt:假设S
是NuclearMissilePosition
或PatientInfusionValueState
。这些是可变的,应该是可变的,但在某些情况下我可能不想要 .现在,你可以说“好的,然后只让友元函数改变它们”,或者“然后写一个 setter 函数”,但这也是一种方法。
但是当您无法区分 get 和 set 时,您的错误消息应该反映不确定性,而不是声称尝试了突变。
好吧,很公平。【参考方案2】:
非常量吸气剂?
Getter 和 setter 只是约定俗成的。有时使用的习惯用法不是提供 getter 和 setter,而是提供类似的东西
struct foo
int val() const return val_;
int& val() return val_;
private:
int val_;
;
这样,根据实例的常量,您可以获得引用或副本:
void bar(const foo& a, foo& b)
auto x = a.val(); // calls the const method returning an int
b.val() = x; // calls the non-const method returning an int&
;
这是否是一般的好风格是一个见仁见智的问题。在某些情况下,它会导致混淆,而在其他情况下,这种行为正是您所期望的(见下文)。
无论如何,更重要的是根据类应该做什么以及你想如何使用它来设计一个类的接口,而不是盲目地遵循关于 setter 和 getter 的约定(例如,你应该给方法一个有意义的名称,它表达了它的作用,而不仅仅是“假装被封装,现在让我通过 getter 访问你所有的内部结构”,这就是在任何地方使用 getter 的实际含义)。
具体例子
考虑到容器中的元素访问通常是这样实现的。作为一个玩具示例:
struct my_array
int operator[](unsigned i) const return data[i];
int& operator[](unsigned i) return data[i];
private:
int data[10];
;
向用户隐藏元素并不是容器的工作(即使data
也可以是公开的)。您不希望根据您是要读取还是写入元素来访问元素的不同方法,因此在这种情况下提供 const
和非常量重载非常有意义。
来自 get vs encapsulation 的非 const 引用
也许不是那么明显,但是提供 getter 和 setter 是否支持封装还是相反,这有点争议。虽然总的来说,这件事很大程度上是基于意见的,但对于返回非 const 引用的 getter 来说,这与意见无关。他们确实打破了封装。考虑
struct broken
void set(int x)
counter++;
val = x;
int& get() return x;
int get() const return x;
private:
int counter = 0;
int value = 0;
;
顾名思义,此类已损坏。客户可以简单地获取一个引用,并且该类没有机会计算该值被修改的次数(正如set
所建议的那样)。一旦您返回一个非常量引用,那么关于封装,将成员公开几乎没有什么区别。因此,这仅用于此类行为是自然的情况(例如容器)。
附言
请注意,您的示例返回 const T&
而不是值。这对于模板代码是合理的,因为您不知道副本的成本,而对于 int
,您不会通过返回 const int&
而不是 int
获得太多收益。为了清楚起见,我使用了非模板示例,但对于模板代码,您可能更愿意返回 const T&
。
【讨论】:
【参考方案3】:get()
可以被允许变异的非 const 对象调用,你可以这样做:
S r(0);
r.get() = 1;
但是如果您将r
const 设置为const S r(0)
,则r.get() = 1
行不再编译,甚至无法检索值,这就是为什么您需要一个const 版本const T& get() const
至少能够检索const 对象的值,这样做可以:
const S r(0)
int val = r.get()
成员函数的 const 版本尽量与被调用的对象的 constness 属性保持一致,即如果对象通过 const 是不可变的并且成员函数返回引用,它可以通过返回一个常量引用来反映调用者的常量,从而保留对象的不变性。
【讨论】:
OP 在问,我敢肯定,为什么要使用这种方法,而不是仅仅授予对成员本身的访问权限。 @einpoklum 他对使用两个不同版本的get
执行相同操作“return val”的原因和用例更感兴趣...查看他的最后 2 段。【参考方案4】:
没有。如果 getter 只是简单地返回对成员的非常量引用,如下所示:
private:
Object m_member;
public:
Object &getMember()
return m_member;
那么m_member
应该是公共的,并且不需要访问器。绝对没有必要将此成员设为私有,然后创建一个访问器,让 all 访问它。
如果你调用getMember()
,你可以将结果引用存储到一个指针/引用,然后,你可以对m_member
做任何你想做的事情,封闭类对此一无所知。就好像m_member
是公开的一样。
注意,如果getMember()
做了一些额外的任务(例如,它不只是简单地返回m_member
,而是懒惰地构造它),那么getMember()
可能有用:
Object &getMember()
if (!m_member) m_member = new Object;
return *m_member;
【讨论】:
【参考方案5】:这取决于S
的用途。如果它是某种薄包装器,则允许用户直接访问底层值可能是合适的。
现实生活中的一个例子是std::reference_wrapper
。
【讨论】:
这很可能是“正确”的答案。在为外部库等封装 C 类型时很有用。 在我看来,reference_wrapper
做了不同的事情。在 OP 的示例中,使用get()
,您可以修改成员的值。使用reference_wrapper::get
,不能修改它指向的对象,只能修改指向的对象。
@geza reference_wrapper 暴露了引用语义,所以它的“值”是被包装的对象。它被指针保存的事实只是一个实现细节。
这就是为什么它在这里不相关。在 OP 的示例中,get()
返回对包含在对象中的成员的引用。 reference_wrapper
不这样做。它的 get()
函数做其他事情。 “在任何情况下,让非常量 get 函数返回 val 是有意义的吗?”。 reference_wrapper
返回指向的值,而不是值本身。以上是关于定义非常量“get”成员函数的原因?的主要内容,如果未能解决你的问题,请参考以下文章