C++ 右值引用和移动语义

13

C++03存在隐式造成不必要的拷贝的问题。因此,C++11引入了rvalue引用移动语义。现在我的问题是,这种不必要的拷贝问题是否也存在于诸如C#和Java之类的语言中,还是只是C++的问题?换句话说,rvalue引用是否使C++11比C#或Java更加高效?

就C#而言(它允许运算符重载),假设我们有一个数学向量类,并且我们像这样使用它。

vector_a = vector_b + vector_c;
编译器将把vector_b + vector_c转换为一个临时对象(我们称之为vector_tmp)。
我认为 C# 无法区分像vector_tmp这样的临时 rvalue 和像vector_b这样的 lvalue,因此我们仍然需要将数据复制到vector_a中。在 C++11 中,可以通过使用rvalue 引用移动语义轻松避免这种情况。

4
好的,我会尽力为您进行翻译。请注意,原始文本中已经是中文,因此无需进行翻译。如果您有需要翻译成其他语言的内容,请告知我需要翻译的语言和具体内容。另外,我会严格按照您的要求,尽可能准确地翻译给您。 - Sebastian Mach
6个回答

7
C#和Java中的类引用具有一些C++中shared_ptr的特性。然而,rvalue引用和移动语义更多地涉及临时值类型,但是与C++值类型相比,C#值类型非常不灵活,根据我自己的C#经验,大多数情况下你将得到类而不是结构体。
因此,我的假设是Java和C#都不会从这些新的C++功能中获益太多,它让代码能够安全地假定某个东西是临时的,并在没有复制的情况下直接窃取内容。

5

在C#和Java中存在不必要的复制操作。

那么,与C#或Java相比,rvalue引用使C++11更加高效吗?

答案是肯定的。:)


3
您指的是Java中的什么时候发生?请提供一个例子。它可能会出现在调用clone等特定实现中,但那似乎不是重点。Java没有默认的复制构造函数和隐式复制,因为它默认(实际上按值)传递对象的引用。 - Sebastian Olsson
Sebastian Olsson:显然,右值引用与复制构造函数无关。它与临时右值有关。C#如何处理这个问题?我的意思是,它如何处理从临时对象复制的问题。例如,您有一个带有重载操作的类,并且您有此表达式a = b + c + d。C++和C#如何不同地处理它。假设C++实现使用引用。 - MetallicPriest
这是关于一个不必要的析构和构造。我的观点是Java不像C++那样推断对象创建。至于C#中的运算符重载,我不敢回答,因为这不是我的专业领域。 - Sebastian Olsson
Sebastian:我明白你的意思。你是说由于Java没有复制构造函数,所以在Java中不存在这个问题。 - MetallicPriest
3
嗯,几乎是这样。我的意思是,Java在传递对象时不会使用复制构造函数自动进行对象拷贝,而是按值引用对象,因此不会发生这种情况。如果Java在传递对象时自动调用深层clone(),它将面临同样的问题。在Java中,部分问题可能出现在特定的实现中,例如使用不可变方法模拟运算符重载时,如a = a.add(b.add(c)),如果add创建一个新对象,则会出现问题,但Java没有这种默认行为。 - Sebastian Olsson
谢谢Sebastian,这真的很有帮助。也许你可以把它写成一个答案。 - MetallicPriest

4

由于Java和C#中的类使用引用语义,因此在这些语言中永远不会有任何隐式对象副本。移动语义解决的问题在Java和C#中不存在。


1
C++ 也可以使用引用语义。实际上,复制构造函数几乎总是编写为采用常量引用的形式。因此,问题似乎与引用无关。 - MetallicPriest
10
“传递引用”和“引用语义”是两个不同的概念。 - fredoverflow
在C#中,结构体具有值语义,而类具有引用语义。此外,在C#中,如果您想按引用传递,则使用ref限定符。没有ref限定符,您将获得按值调用。这两个概念完全正交:您可以通过值或引用传递结构体类型,也可以通过值或引用传递类类型。 - fredoverflow
FredOverflow - 假设你有一个基于运算符重载的数学向量类,并且在C#中像这样使用它,“vector_a = vector_b + vector_c”。那么C#如何在不复制的情况下实现呢?编译器肯定会将vector_b + vector_c转换为某个临时变量(我们称之为vector_tmp),然后将其分配给vector_a,对吧?所以,那里肯定会涉及一些复制操作?C++11在这里的做法是通过右值引用,允许编译器将vector_tmp作为引用访问,而这在C++03中是不可能的,因为它不能引用rvalue。 - MetallicPriest
@MetallicPriest 很遗憾,我确实了解rvals,但是新标准中没有太多相关内容。回到正题:vector_b + vector_c只是一种语法糖,类似于vector add(vector x, vector y)。在最简单的情况下,我们创建一个新对象并返回对它的引用。你可以把这个返回的对象引用看作是你的vector_tmp。之后唯一发生的事情就是我们将引用赋值给一个局部变量。在Java或C#中(至少对于编译器来说),不存在复制构造函数或赋值运算符的概念。 - Voo
显示剩余7条评论

1

我认为这可能发生在Java中。请参见下面的add和add_to操作。 add创建一个结果对象来保存矩阵加法运算的结果,而add_to仅将rhs添加到此结果对象中。

class Matrix   {
   public static final int w = 2;
   public static final int h = 2;

   public float [] data;

   Matrix(float v)
   {
       data = new float[w*h];
       for(int i=0; i<w*h; ++i)
           { data[i] = v; }
   }

   // Creates a new Matrix by adding this and rhs 
   public Matrix add(Matrix rhs)
   {
       Main result = new Main(0.0f);
       for(int i=0; i<w*h; ++i)
           { result.data[i] = this.data[i] + rhs.data[i]; }

       return result;
   }

   // Just adds the values in rhs to this
   public Main add_to(Main rhs)
   {
       for(int i=0; i<w*h; ++i)
           { this.data[i] += rhs.data[i]; }
       return this;    
   }

    public static void main(String [] args)
    {
       Matrix m = new Matrix(0.0f);
       Matrix n = new Matrix(1.0f);
       Matrix o = new Matrix(1.0f);

       // Chaining these ops would modify m
       // Matrix result = m.add_to(n).subtract_from(o);      
       m.add_to(n);         // Adds n to m
       m.subtract_from(o);  // Subtract o from n

       // Can chain ops without modifying m,
       // but temps created to hold results from each step
       Matrix result = m.add(n).subtract(o);
    }
}

因此,我认为这取决于你的类为用户提供了哪种功能。

1
这个问题经常出现。有人想要保留一个独特的对象副本,没有其他人可以修改它。该怎么办?
  1. 制作任何人给我的对象的深层副本?那样做可以解决问题,但效率不高。
  2. 要求人们给我一个新对象,并且不保留副本?如果你很勇敢,那会更快。但错误可能来自完全无关的代码片段几小时后修改了该对象。
  3. C++风格:将所有项从输入移动到我的新对象中。如果调用者意外地再次尝试使用对象,则会立即看到问题。
  4. 有时候C#只读集合也有帮助。但根据我的经验,通常最多也只是麻烦。

以下是我的讨论内容:

class LongLivedObject
{
    private Dictionary <string, string> _settings;
    public LongLivedObject(Dictionary <string, string> settings)
    {   // In C# this always duplicates the data structure and takes O(n) time.
        // C++ will automatically try to decide if it could do a swap instead.
        // C++ always lets you explicitly say you want to do the swap.
        _settings = new Dictionary <string, string>(settings);
    }
}

这个问题是Clojure和其他函数式语言的核心!总的来说,我经常希望在C#中拥有类似于C++11风格的数据结构和操作。

0

你可以尝试模拟移动语义。例如,在Trade-Ideas Philip的示例中,你可以传递自定义的MovableDictionary而不是Dictionary

public class MovableDictionary<K, V> // : IDictionary<K, V>, IReadOnlyDictionary<K, V>...
{
    private Dictionary<K, V> _map;

    // Implement all Dictionary<T>'s methods by calling Map's ones.

    public Dictionary<K, V> Move()
    {
        var result = Map;
        _map = null;
        return result;
    }

    private Dictionary<K, V> Map
    {
        get
        {
            if (_map == null)
                _map = new Dictionary<K, V>();

            return _map;
        }
    }
}

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