【问题标题】:Move Semantics in Golang在 Golang 中移动语义
【发布时间】:2014-01-17 22:49:14
【问题描述】:

这来自 Bjarne Stroustrup 的 C++ 编程语言,第四版 3.3.2。

我们真的不想要副本;我们只是想得到结果 一个函数:我们想要移动一个 Vector 而不是复制它。 幸运的是,我们可以说明这个意图:

class Vector {
     // ...

     Vector(const Vector& a);          // copy constructor
     Vector& operator=(const Vector& a);     // copy assignment

     Vector(Vector&& a);               // move constructor
     Vector& operator=(Vector&& a);          // move assignment
};

根据该定义,编译器将选择移动构造函数 实现函数返回值的传递。 这意味着 r=x+y+z 将不涉及向量的复制。反而, Vectors 只是被移动了。通常,Vector 的移动构造函数是 简单的定义...

我知道 Golang 支持传统的值传递和使用 Go 样式指针的引用传递。

Go 是否像上面 Stroustrup 所描述的那样支持 C++11 的“移动语义”,以避免无用的来回复制?如果是这样,这是自动的,还是需要我们在代码中做一些事情才能实现。


注意:已经发布了一些答案 - 我必须稍微消化一下,所以我还没有接受 - 谢谢。

【问题讨论】:

  • @wheaties - 为什么不呢?你认为继续来回复制东西更好吗?为什么会这样?
  • 移动语义为无复杂性的语言引入了更多复杂性。我宁愿保持简单,愚蠢,也不愿看着 Go 长成八角龟。
  • @wheaties - 比看着 Go 成长为一个八角的 turduckelephanizard 没有争论 - C++ 11 得到了改进,真的 - 缺点是现在有 100 种方法可以做一切,我们有200种方式!这就是我真正喜欢使用 Go 的原因。我真正想知道的是,如果这是自动处理的,以避免无用的来回复制。
  • Go 没有向量。它有自己的数组,并为您处理资源管理。因此,您只需要传递数组切片,而无需考虑如何实现任何内容。
  • @Vector,Go 的方式不是使用向量,而是使用切片。切片 value 是一个(打包的)结构,由一个指针和两个(平台大小的)整数组成——当你传递切片时,它会被复制,而且非常便宜。

标签: c++11 go move


【解决方案1】:

细分如下:

  1. Go 中的所有内容都是按值传递的。
  2. 但是有五种内置的“引用类型”也是按值传递的,但在内部它们保存对单独维护的数据结构的引用:映射、切片、通道、字符串和函数值(无法改变数据后两个参考)。

你自己的答案@Vector 是不正确的,Go 中的 nothing 是通过引用传递的。相反,有些类型具有引用语义。它们的值仍然按值传递(原文如此!)。

您的困惑可能源于您的大脑目前被 C++、Java 等所负担,而 Go 中的这些事情大多是“像 C 中一样”完成的。

以数组和切片为例。在 Go 中,数组按值传递的,但是切片是一个压缩结构,包含一个指针(指向底层数组)和两个平台大小的整数(切片的长度和容量),并且它是被复制的 this 结构的值——一个指针和两个整数——当它被分配或返回等时。如果你复制一个“裸”数组,它会被复制——连同它的所有元素。

这同样适用于通道和地图。您可以将定义通道和映射的类型视为声明如下:

type Map struct {
   impl *mapImplementation
}

type Slice struct {
   impl *sliceImplementation
}

(顺便说一句,如果你知道C++,你应该知道一些C++ 代码使用这个技巧来降低细节在头文件中的暴露。)

所以当你以后有

m := make(map[int]string)

您可以将其视为 m 具有 Map 类型,所以当您以后这样做时

x := m

m 被复制,但它只包含一个指针,因此 xm 现在都引用相同的底层数据结构。 m 是否通过引用复制(“移动语义”)?肯定不是! map 和 slice 和 channel 类型的值是否具有引用语义?是的!

请注意,这三种类型一点也不特别:通过在其中嵌入指向某些复杂数据结构的指针来实现自定义类型是一种相当常见的模式。

换句话说,Go 允许程序员决定他们想要的类型的语义。 Go 恰好有五个已经具有引用语义的内置类型(而所有其他内置类型都具有值语义)。选择一种语义而不影响另一种语义不会影响以任何方式按值复制所有内容的规则。例如,在 Go 中有指向任何类型值的指针并分配它们(只要它们具有兼容的类型)都很好——这些 指针 将按值复制。

从另一个角度来看,许多 Go 包(标准和第 3 方)更喜欢使用指向(复杂)值的指针。一个例子是os.Open()(它在文件系统上打开一个文件)返回一个*os.File类型的值。也就是说,它返回一个指针并且期望调用代码来传递这个指针。当然,Go 作者可能已经将 os.File 声明为包含单个指针的 struct,本质上使该值具有引用语义,但他们没有这样做。我认为这样做的原因是没有特殊的语法来处理这种类型的值,所以没有理由让它们作为映射、通道和切片工作。换句话说,KISS。


推荐阅读:

【讨论】:

  • 注意:已经发布了一些答案 - 我必须稍微消化一下,所以我还没有接受 - 谢谢。
【解决方案2】:

The Go Programming Language Specification

Calls

在函数调用中,函数值和参数在 通常的顺序。在评估它们之后,调用的参数 通过值传递给函数并且被调用的函数开始 执行。函数的返回参数按值传递 函数返回时返回调用函数。

在 Go 中,一切都是按值传递的。

Rob Pike

在 Go 中,一切都是按值传递的。一切。

有一些类型(指针、通道、映射、切片)具有 类似引用的属性,但在这些情况下,相关数据 结构(指针、通道指针、映射头、切片头)包含一个 指向底层共享对象的指针(指向事物、通道 描述符、哈希表、数组);数据结构本身是通过 价值。总是。

总是。

-抢劫

【讨论】:

  • 除非它是通过引用传递的?此处引用的部分忽略了列出上面给出的示例,我认为这是断章取义的。
  • @skinner:仔细阅读 Go 编程语言规范。正如 Rob Pike 多次指出的那样,你错了。
  • @peterSO - 我很想否决你的答案,因为对或错,它根本没有真正解决我的问题。请编辑您的答案并将其连接到我的问题。
  • @peterSO - 在 Go 中,一切都是按值传递的。一切。 - 我是 GoLang 中的“新手”,我知道 Rob Pike 是谁,但这听起来不太对劲。这里一定有更多的事情发生。该语言清楚地定义了指针和引用类型,我一直在翻阅的所有书籍(Summerfield、Chisnall、Balbaert)都以传统方式解释它们。
  • @Vector: 并且指针是按值传递的。如果我有一个变量var p *Foo,并调用bar(p)bar 不能改变p 的值。它可以改变*p,但不能让p指向别的东西。
【解决方案3】:

据我了解,Go 以及 Java 和 C# 从未有过 C++ 的过度复制成本,但没有解决向容器的所有权转移问题。因此,仍然涉及复制。随着 C++ 变得更像是一种值语义语言,引用/指针被降级为 i) 类内的智能指针托管对象和 ii) 依赖引用,移动语义解决了过度复制的问题。请注意,这与“按值传递”无关,现在每个人都通过引用 (&) 或常量引用 (const &) 在 C++ 中传递对象。 让我们看看这个(1):

BigObject BO(big,stuff,inside);
vector<BigObject> vo;
vo.reserve(1000000);
vo.push_back(BO);

或 (2)

vector<BigObject> vo;
vo.reserve(1000000);
vo.push_back(BigObject(big,stuff,inside));

虽然您通过引用传递向量 vo,但在 C++03 中,向量代码中有一个副本。 在第二种情况下,必须构造一个临时对象,然后将其复制到向量中。由于它只能通过vector访问,这是一个浪费的副本。

但是,在第一种情况下,我们的意图可能只是将 BO 的控制权交给向量本身。 C++17 允许这样做:

(1, C++17)

vector<BigObject> vo;
vo.reserve(1000000);
vo.emplace_back(big,stuff,inside);

或 (2, C++17)

vector<BigObject> vo;
vo.reserve(1000000);
vo.push_back(BigObject(big,stuff,inside));

根据我的阅读,尚不清楚 Java、C# 或 Go 是否可以免除 C++03 在容器情况下遭受的相同复制重复。

老式的 COW(写时复制)技术也有同样的问题,因为一旦向量内的对象被复制,资源就会被复制。

【讨论】:

    【解决方案4】:

    Stroustrup 正在谈论 C++,它允许您按值传递容器等 - 因此过度复制成为一个问题。

    在 Go 中(如 Delphi、Java 等),当您传递容器类型等时,它们始终是引用,因此这不是问题。无论如何,您不必在 GoLang 中处理或担心 - 编译器只会做它需要做的事情,并且从我目前看到的情况来看,它做对了。

    感谢@KerrekSB 让我走上正轨。

    @KerrekSB - 我希望这是正确的答案。如有错误,概不负责。:)

    【讨论】:

    • 移动语义不仅仅是为了避免不必要的复制。这也与所有权转让有关。 (编辑:嗯,OP 确实询问避免复制)
    • @DyP - 我不担心所有权转移,我认为这是隐含的。正如你所说,我正在解决 Stroustrup 所说的问题 - 不必要的复制。
    • 注意:已经发布了一些答案 - 我必须稍微消化一下,所以我还没有接受 - 谢谢。
    猜你喜欢
    • 1970-01-01
    • 2018-08-09
    • 2016-07-11
    • 2012-04-09
    • 2020-08-29
    • 2018-04-05
    • 2019-01-02
    • 2021-09-24
    • 2011-07-28
    相关资源
    最近更新 更多