C++11 - 所有权和访问器

13

我刚接触C++,但还不太理解所有权的概念,尤其是在使用getter时。以下是一些示例代码:

class GameObject {
public:
  Transform *transform();
private:
  Transform _transform;
};

我猜原始指针的使用是不安全的,因为当对象不再存在时,其他人仍然可以访问它?

  1. 因此,我考虑对transform成员使用unique_ptr,因为GameObject是唯一拥有transform的对象。但是我能从getter中返回它吗?但是反过来,为什么我要首先使用unique_ptr,而不是像上面那样将其添加为成员变量呢?

  2. 那为什么不能使用shared_ptr呢?这让我感到很奇怪,因为我不想共享所有权,GameObject才是owner,而其他人可能会访问它...

  3. 那它是什么?引用吗? 我猜shared_ptr似乎是最明智的选择, 因为其他人可以安全地保留对transform的引用,但如果包含的GameObject被销毁了,渲染transform就变得无用了。也许我的所有权思路有问题,但每种可能性似乎都不正确。谢谢您的帮助。


1
你可以通过shared_ptr存储_transform成员,并从getter函数返回weak_ptr。 - inkooboo
5个回答

18
很明显(或者应该明显)对于任何阅读您的类定义的人来说,GameObject拥有一个transform。您所说的“共享所有权”既不隐含也不需要。由于从GameObject中获取transform永远不可能失败,因此您不需要像指针(原始或其他方式)一样表达可能为null的情况,所以返回一个(可能的)引用const似乎是最惯用的方法。
您说原始指针是不安全的,但它并不比任何其他直接访问方法更不安全。无论您如何提供访问所拥有的transform对象的方式(而不是其副本),都会给客户端机会获取其地址并在拥有者GameObject的生命周期之外存储它。真正取决于客户端是否做出愚蠢的事情。无法绝对地预防这种情况,因此您应该使接口简单、清晰且难以意外地使用不正确。

1
+1 是为了设计解决方案,使得难以出现错误。这是 Boost 库的核心理念。 - Vite Falcon
大家的回答都很好,非常感谢! @Balog Pal:那么就会涉及到复制吗? - cboe
@cboe:是的,这就是重点。你避免了“泄漏内部细节”。除非获取的成员是一个集合,否则在大多数情况下几乎不会注意到复制。 - Balog Pal
@BalogPal:是的,按值返回肯定可以避免任何生命周期问题;我做出了隐含的假设,即转换更多地是实体而不是值,但这并不一定是正确的假设。 - CB Bailey
绝对是实体比值更重要,但我甚至没有考虑过 :) 谢谢 - cboe

15

我认为重要的是你的设计模型表明Transform对象被其GameObject所拥有。

如果不打算共享所有权(换句话说,如果Transform对象的生命周期严格绑定在拥有它的唯一GameObject的生命周期中),那么我会避免使用shared_ptr

在我看来,你已经做出了正确的选择:只需具有Transform类型的子对象。但是,返回引用而不是原始指针:

class GameObject {
public:
    Transform& transform();
private:
    Transform _transform;
};

这是表达您提供了访问但不提供所有权的 _transform 子对象的最清晰方式。

客户端不必担心诸如“我应该何时以何种方式删除此对象?而且我是否应该删除它?”这样的问题,因为您的接口已经清楚地表明:没有所有权,也没有责任。

另一方面,可能会有原因阻止您这样做。一个可能的原因是,GameObject 可能拥有或不拥有 Transform 对象。如果是这种情况,则可以使用 unique_ptr 代替(并返回一个空指针)。

使用 unique_ptr 的另一个原因可能是需要推迟构造 Transform 子对象,因为构造函数接受一些需要计算的值,不能在构造函数的初始化列表中提供。

总体而言,除非有充分的理由,否则请优先采用最简单的解决方案并嵌入类型为 Transform 的子对象,提供一个引用即可。


你能解释一下倒数第二段吗?如果使用unique pointer,返回类型应该是一个unique_ptr,它将被“移动”到客户端吗? - Koushik Shetty
@Koushik:如果Transform的构造函数必须使用需要一些非平凡计算的参数进行调用,并且这个计算不能在GameObject的构造函数的初始化列表中执行,或者最好放在构造函数的主体中,则无法初始化Transform子对象。当然,如果Transform有一个默认构造函数,那么我们可以让_transform子对象被默认构造,然后在主体中重新分配或修改它。 - Andy Prowl
@Koushik:甚至可以由客户端创建Transform对象,并通过unique_ptr传递给GameObject。无论如何,GameObject都不会通过返回unique_ptr来提供对所拥有的_transform指针的访问。返回unique_ptr将意味着转移所有权,这在这里是不希望的。GameObject将返回引用或原始指针(取决于是否始终嵌入了GameObjectTransform对象)。 - Andy Prowl
我理解那部分,但unique_ptr如何用于推迟transform的构建是我不理解的。 - Koushik Shetty

5

经验法则:只有在绝对不能避免的情况下才需要考虑指针和所有权。C++具有良好的值语义(通过const ref扩展),因此在需要指针、智能指针或者裸指针之前,可以做得很远。

在您的例子中,_transform作为直接成员非常好。如果您需要getter,则可以创建它。

Transform transform() const {return _transform; }

在一些特殊情况下,可能需要

const Transform& transform() const {return _transform; }

如果有正当理由,请附上终身文档。

而且,显然最好的方法是根本不使用getter。面向对象编程是关于行为的,让你的类完成工作,而不是将其用作数据转储,不断从外部进行查询。


1
当需要所有权转移时,就需要一个指针。这听起来比你的经验法则更易理解。 - aggsol
我看不出任何矛盾。有一些好的线程列出了指针使用是不可避免的情况,你的情况在列表中。 - Balog Pal

3

所有权和裸指针?

智能指针通常具有所有权语义,因此在不想给予/共享所有权时使用它们是一个坏主意。

在我自己的代码中,我决定以下约定:

裸指针表示没有所有权转移/共享

这意味着如果某些函数接收裸指针作为参数,则它没有所有权(因此,不应尝试删除它)。同样,返回裸指针的函数不会传递/共享所有权(因此,调用者不应尝试删除它)。

因此,在您的情况下,您可以返回指针而不是智能指针。

指针还是引用?

这意味着指针和引用之间的选择仅取决于以下规则解决的一个因素:

  • 如果nullptr/NULL值有意义,则使用指针。
  • 如果nullptr/NULL值没有意义和/或不希望,则使用引用。

常量性和成员变量?

我看到了一些关于常量性的答案,所以我加上了我的两分钱:

将非const值返回给内部变量是一种设计选择。你必须在以下getter之间进行选择:

const Transform * getTransform() const ;
Transform * getTransform() ;

我(不当地)称上述访问器为“属性”,非const版本使用户可以随意修改内部Transform对象,而无需先询问您的类。请注意,如果GameObject是const,则无法修改其内部Transform对象(唯一可访问的getter是const版本)。此外,请注意,您不能更改GameObject内部Transform对象的地址,只能通过该地址更改数据指针(这与将成员变量公开不同)。
const Transform *& getTransform() const ;
Transform *& getTransform() ;

这就像上面的情况,但由于引用的存在,用户直接访问指针地址,这意味着非const访问器更或多或少地类似于将成员变量公开。

const Transform * getTransform() const ;
void setTransform(Transform * p_transform) ;

我将上面的访问器滥用地称为“getter/setter”:如果用户想修改变换值,则必须使用setTransform。这样你就可以更好地控制你正在做的事情(请注意,在我的代码中,你可以通过传递unique_ptr或auto_ptr来表达对p_transform对象的全部所有权的获取。你会注意到,如果GameObject是const,就没有办法修改其内部的Transform对象(setter是非const的)。

Transform * getTransform() const ;

我有时会滥用称呼上述访问器为“下标”,比如在指针数组中,该数组可以是const类型,所以内部地址是const的,但是由该地址指向的数据是non-const的,因此它可以被修改。这通常意味着Transform对象并不真正被你的GameObject对象“拥有”。实际上,很可能是GameObject仅引用某个外部对象,以方便使用。


2
其他人已经回答了关于所有权的问题,但我还想补充一点。将非const指针或引用传递给私有成员通常是一个非常糟糕的主意:这基本上等同于将该私有成员公开。换句话说,如果您编写一个返回非const引用或指向非const成员的指针的getter,则您的代码基本上等同于以下内容:
class GameObject {
public:
  Transform _transform; // and you don't have to write a getter
};

但这是一个不好的想法。

如果你真的必须编写getter函数,那么按照Balog Pal的建议,给出私有成员的const引用。如果需要非const引用,那么你应该重新考虑你的设计。


听起来不错,但将其设为只读是否是一种有效的方法? - cboe
@cboe 是的,绝对没问题。 - Ali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接