13

我对推荐的编码技术有疑问。我有一个模型分析工具,有时我需要传递大量数据(从工厂类到包含多个异构块的类)。

我的问题是对于我是否应该使用指针或移动所有权是否有一些共识(我需要尽可能避免复制,因为数据块的大小可能高达 1 GB)。

指针版本如下所示:

class FactoryClass {
...
public:
   static Data * createData() {
      Data * data = new Data;
      ...
      return data;
   }
};

class StorageClass {
   unique_ptr<Data> data_ptr;
...
public:
   void setData(Data * _data_ptr) {
      data_ptr.reset(_data_ptr);
   }
};

void pass() {
   Data * data = FactoryClass::createData();
   ...
   StorageClass storage;
   storage.setData(data);
}

而移动版本是这样的:

class FactoryClass {
...
public:
   static Data createData() {
      Data data;
      ...
      return data;
   }
};

class StorageClass {
   Data data;
...
public:
   void setData(Data _data) {
      data = move(_data);
   }
};

void pass() {
   Data data = FactoryClass::createData();
   ...
   StorageClass storage;
   storage.setData(move(data));
}

我更喜欢移动版本 - 是的,我需要在主代码中添加移动命令,但最后我只有存储中的对象,我不必再关心指针语义了。

但是,当使用我不了解的移动语义时,我并不太放松。(不过我不关心 C++11 的要求,因为代码已经只能编译 Gcc4.7+)。

有人会有支持任一版本的参考吗?还是有其他一些如何传递数据的首选版本?

我无法搜索任何内容,因为关键字通常会导致其他主题。

谢谢。

编辑注意:第二个示例被重构以包含评论中的建议,语义保持不变。

4

1 回答 1

11

当您将对象传递给函数时,传递的内容部分取决于该函数将如何使用它。函数可以通过以下三种通用方式之一使用对象:

  1. 它可以在函数调用期间简单地引用该对象,而调用函数(或调用堆栈的最终父级)维护该对象的所有权。这种情况下的引用可以是常量引用或可修改的引用。该函数不会长期存储该对象。

  2. 它可以直接复制对象。它没有获得原件的所有权,但它确实获得了原件的副本,以便存储、修改或按照它的意愿处理副本。请注意,#1 和 this 之间的区别在于副本在参数列表中是显式的。例如,std::string取值。但这也可以像int取值一样简单。

  3. 它可以获得对象的某种形式的所有权。然后该函数对对象的销毁负有一些责任。这也允许函数长期存储对象。

我对这些范例的参数类型的一般建议如下:

  1. 尽可能通过明确的语言参考来获取对象。如果这不可能,请尝试std::reference_wrapper. 如果这不起作用,并且没有其他解决方案似乎合理,那么使用指针。指针将用于诸如可选参数之类的东西(尽管 C++14 的 std::optional 会使它变得不那么有用。尽管指针仍然有用途),语言数组(尽管如此,我们有涵盖这些大部分用途的对象) 等等。

  2. 按值取对象。那个是没有商量余地的。

  3. 通过值移动(即:将其移动到按值参数中)或通过指向对象的智能指针(也将按值获取,因为无论如何您都要复制/移动它)来获取对象. 您的代码的问题是您正在通过指针转移所有权,但使用的是原始指针。原始指针没有所有权语义。分配任何指针的那一刻,您应该立即将其包装在某种智能指针中。所以你的工厂函数应该返回一个unique_ptr.

您的案例似乎是#3。您在值移动和智能指针之间使用哪个完全取决于您。如果您Data出于某种原因必须进行堆分配,那么选择非常适合您。如果Data可以分配堆栈,那么您有一些选择。

我通常会根据对Data的内部大小的估计来做到这一点。如果在内部,它只是几个指针/整数(“很少”,我的意思是像 3-4),那么把它放在堆栈上就可以了。

事实上,它可以更好,因为你将有更少的机会双缓存未命中。如果您的Data函数经常只是从另一个指针访问数据,如果您Data按指针存储,那么对它的每个函数调用都必须取消引用您存储的指针以获取内部指针,然后取消引用内部指针。这是两个潜在的缓存未命中,因为没有一个指针与StorageClass.

如果Data按值存储,则Data内部指针很可能已经在缓存中。StorageClass它与的其他成员具有更好的局部性;如果您之前访问过一些StorageClass,您已经为缓存未命中支付了费用,因此您很可能已经Data在缓存中。

但运动不是免费的。它比完整副本便宜,但它不是免费的。您仍在复制内部数据(并且可能会清除原始数据上的任何指针)。但是话又说回来,在堆上分配内存也不是免费的。也没有释放它。

但是话又说回来,如果您不经常移动它(您移动它以使其到达最终位置,但之后再多一点),即使移动更大的物体也可以。如果您使用它而不是移动它,那么对象存储的缓存位置可能会胜过移动成本。

最终没有很多技术原因可以选择其中一个。我会说在合理的情况下默认移动。

于 2013-07-29T20:13:49.120 回答