如何在不生成副本的情况下使用getter和setter?

7
我想知道如何为一个占用大量内存的成员变量使用getter和setter。通常我会按以下步骤进行操作:
class A
{
private:
  BigObject object;
public:
  BigObject getObject() const
  {
    return object;
  }

  void setObject(const BigObject& object)
  {
    this->object = object;
  }
};

然而我相信这个getter和setter会复制BigObject,而这不是我想要的。有更好的方法吗?

我考虑过这样做,但我在网上读到说这不是一个好主意,因为它可能会导致分段错误(segmentation fault), 如果使用不当:

BigObject& getObject()
{
  return object
}

1
一个 setter 怎么不做拷贝就能工作? - David Schwartz
7
你在哪里网上看到会导致段错误的信息?通常通过引用返回类成员来实现这个功能,所以那个建议非常令人惊讶。 - NathanOliver
1
获取器和设置器会破坏封装性,因为它们暴露了对象的内部类型。我更愿意在类上实现一个操作方法,而不是让别人获取内部状态并将其放回去。 - Martin York
2
如果使用不当,任何东西都可能导致段错误。 - Mirko
1
为什么使用(或修改或“foos”或“bars”...)A :: object数据的代码不在A中?这个功能不是封装目标的一部分吗?请参阅有关“告诉,不要问”的文章。在我看来,getter和setter浪费你的时间(也浪费我们的时间)。 - 2785528
显示剩余2条评论
4个回答

8

如果您在这种情况下不关心封装,也就是说A::object成员应该可以被任何人修改而没有限制,请查看SergeyA的答案

通过const引用返回以避免复制并仍然保持封装(这意味着调用者不能错误地修改成员):

const BigObject& getObject() const
{
    return object;
}

如果调用者确实需要一份副本,他们可以很容易地自己完成。如果你想要防止悬空引用(你提到的segfault)在 getter 被用于临时对象时发生,你只能在 getter 确实被用于临时对象时返回一份副本:
BigObject getObject() const &&
{
    return object;
}

const BigObject& getObject() const &
{
    return object;
}

在调用临时对象上的getObject()方法时,将返回一份副本。或者,您可以通过删除该特定重载来完全防止在临时对象上调用getter:

BigObject getObject() const && = delete;

const BigObject& getObject() const &
{
    return object;
}

请记住,这不是一种保证安全的措施。它可以防止某些错误,但并非全部。函数的调用方仍应注意对象生存期。

顺便说一下,您也可以改进您的setter。现在,它将始终复制对象,而不管调用方如何传递参数。您应该通过值进行传递,并将其移动到成员中:

void setObject(BigObject object)
{
    this->object = std::move(object);
}

这要求BigObject是可移动的。如果它不是可移动的,那么这比以前更糟糕。

请注意,虽然使用rv ref限定符重载getter是避免某些悬空引用情况的便捷方式,但并不能防止所有这种情况。函数的用户仍必须遵守生命周期规则。 - eerorika
我不确定你的setObject函数定义如何比原来的更好。在原始定义中,参数是通过引用传递的,当将“object”赋值给“this->object”时会进行复制(尽管原始函数错误地使用了“object = object”而不是“this->object = object”)。而在你的定义中,由于对象是按值传递的,仍然存在一份副本。 - jjramsey
@jjramsey 这是一个改进。调用者可以移动构造参数,这样就不会有任何复制构造了。但是当他们需要时,也可以复制构造参数。 - eerorika
@jjramsey 当按值传递然后移动时,这允许调用者直接构造一个对象到setter的参数中,从而完全避免复制:setObject(BigObject()); 如果setter通过引用获取它,那么该临时对象将始终被不必要地复制。正如eeronika所提到的,这允许调用者使用setObject(std::move(big_obj));将已经存在的对象移动到A中。同样,不执行任何复制。 - Nikos C.
我犯了一个错误,我的版本中应该删除const。你的版本使用复制构造函数,而我的版本使用移动构造函数(在我的版本中,getObject可以重命名为takeObject)。 - Jarod42
显示剩余2条评论

4
最佳解决方案:让你的类的代码完全如下所示:
struct A
{
  BigObject object;
};

解释 - 避免使用琐碎的setter和getter。如果你发现自己在类中使用这些东西,请直接公开成员并完成它。

永远不要听信那些会说“但是如果将来我们添加了非琐碎逻辑怎么办”的人。我见过太多琐碎的setter和getter,已经存在了几十年,从未被替换为非琐碎的东西。


请注意,这种方法与返回引用一样存在封装不足的问题。但是,当不需要封装时,这确实是最方便的解决方案。 - eerorika
同意SergeyA关于平凡的getter和setter的看法。毕竟这是C++,而不是Java或C#!当需要检查有效性和/或在过程中修改值时,应使用setter。 - Adrian Mole
我选择将Nikos C.的评论设为答案,因为他的回答非常完整。但是我想实现你的解决方案,因为我想能够编辑BigObject。我在想是否可以使用一个具有公共变量BigObject的类,或者这样做很糟糕? - Le Grand Spectacle
1
@ThatGuyAgain 我认为这没有什么不好的。结构体作为对象的捆绑使用情况是存在的,其中不需要封装,因此公共数据成员是最好的选择。 - SergeyA

2
常见做法是:

class A
{
public:
  // this method is const
  const& BigObject getObject() const
  {
    return object;
  }

  // this method is not const
  void setObject(const BigObject& object)
  {
    object = object;
  }
private:
  BigObject object;
};

如果您只需要获取一个只读对象,那么完全没问题。否则,请考虑更改架构。
另一种选择是存储一个std::shared_ptr并返回一个std::shared_ptr或std::weak_ptr。

1

不需要返回成员的副本,可以返回对其的引用。这样就不需要复制该成员。

我曾经考虑过这种方式,但是在网上看到说如果使用不当会导致段错误,所以我觉得这不是一个好主意

解决方法是不要使用不当。

返回成员的引用是一个常见的模式,通常不被反对。尽管对于快速复制的类型,在没有必要引用成员本身时,返回副本通常更优。

有一种避免复制和破坏封装的解决方案:使用共享指针,并从getter返回该共享指针的副本。然而,这种方法有运行时成本并需要动态分配,因此并不适用于所有用例。


如果是setter,您可以使用移动赋值而不是复制赋值,对于某些类型来说,移动赋值比复制赋值更有效率。


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