Skip to content

Latest commit

 

History

History
355 lines (248 loc) · 13.1 KB

13.15 — Overloading the assignment operator.md

File metadata and controls

355 lines (248 loc) · 13.1 KB

13.15 — Overloading the assignment operator

The assignment operator (operator=) is used to copy values from one object to another already existing object.

赋值运算符是用来把一个值拷贝给另外一个的。

Assignment vs Copy constructor

The purpose of the copy constructor and the assignment operator are almost equivalent -- both copy one object to another. However, the copy constructor initializes new objects, whereas the assignment operator replaces the contents of existing objects.

拷贝构造函数和赋值运算符的目的几乎是一样的,都是把一个对象拷贝给另外一个对象。然而,拷贝构造函数初始化了一个新的对象,而赋值运算符却替换了原先存在的对象。

The difference between the copy constructor and the assignment operator causes a lot of confusion for new programmers, but it’s really not all that difficult. Summarizing:

这种差别给编程的新手带来了很多的困惑,但是它真的不难,很好总结:

  • If a new object has to be created before the copying can occur, the copy constructor is used (note: this includes passing or returning objects by value).

    如果一个对象创建在拷贝发生之前,拷贝构造函数被使用。

  • If a new object does not have to be created before the copying can occur, the assignment operator is used.

    如果在拷贝之前一个新对象不需要被创建,赋值运算符被使用。

Overloading the assignment operator

Overloading the assignment operator (operator=) is fairly straightforward, with one specific caveat that we’ll get to. The assignment operator must be overloaded as a member function.

重载赋值运算符是相当简单的。只需要注意赋值运算符重载函数必须被作为一个成员函数来重载。

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
	int m_numerator;
	int m_denominator;
 
public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
	// Copy constructor
	Fraction(const Fraction &copy) :
		m_numerator(copy.m_numerator), m_denominator(copy.m_denominator)
	{
		// no need to check for a denominator of 0 here since copy must already be a valid Fraction
		std::cout << "Copy constructor called\n"; // just to prove it works
	}
 
        // Overloaded assignment
        Fraction& operator= (const Fraction &fraction);
 
	friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
        
};
 
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
	out << f1.m_numerator << "/" << f1.m_denominator;
	return out;
}
 
// A simplistic implementation of operator= (see better implementation below)
Fraction& Fraction::operator= (const Fraction &fraction)
{
    // do the copy
    m_numerator = fraction.m_numerator;
    m_denominator = fraction.m_denominator;
 
    // return the existing object so we can chain this operator
    return *this;
}
 
int main()
{
    Fraction fiveThirds(5, 3);
    Fraction f;
    f = fiveThirds; // calls overloaded assignment
    std::cout << f;
 
    return 0;
}

This prints:

5/3

This should all be pretty straightforward by now. Our overloaded operator= returns *this, so that we can chain multiple assignments together:

这些东西现在看起来应该相当直白。我们的重载运算符返回自己(就像a=1的返回值是a一样),所以我们利用这一点来把多个赋值运算连在一起。

int main()
{
    Fraction f1(5,3);
    Fraction f2(7,2);
    Fraction f3(9,5);
 
    f1 = f2 = f3; // chained assignment
 
    return 0;
}

Issues due to self-assignment

Here’s where things start to get a little more interesting. C++ allows self-assignment:

到这里的时候,事情就变的有点有趣了,C++允许自己给自己赋值。

int main()
{
    Fraction f1(5,3);
    f1 = f1; // self assignment
 
    return 0;
}

This will call f1.operator=(f1), and under the simplistic implementation above, all of the members will be assigned to themselves. In this particular example, the self-assignment causes each member to be assigned to itself, which has no overall impact, other than wasting time. In most cases, a self-assignment doesn’t need to do anything at all!

However, in cases where an assignment operator needs to dynamically assign memory, self-assignment can actually be dangerous:

在上面简化的赋值运算符函数实现的情况下,所有的成员都会被自己给赋值。在这个特定的例子中,这个自我赋值除了浪费时间,一点用都没有。在大多数情况下,自己给自己的赋值不需要做任何事情。

然而,在赋值运算符需要动态分配内存的时候,自己给自己赋值是非常危险的,看下面的例子:

#include <iostream>
 
class MyString
{
private:
    char *m_data;
    int m_length;
 
public:
    MyString(const char *data="", int length=0) :
        m_length(length)
    {
        if (!length)
            m_data = nullptr;
        else 
            m_data = new char[length];
 
        for (int i=0; i < length; ++i)
            m_data[i] = data[i];
    }
 
    // Overloaded assignment
    MyString& operator= (const MyString &str);
 
    friend std::ostream& operator<<(std::ostream& out, const MyString &s);
};
 
std::ostream& operator<<(std::ostream& out, const MyString &s)
{
    out << s.m_data;
    return out;
}
 
// A simplistic implementation of operator= (do not use)
MyString& MyString::operator= (const MyString &str)
{
    // if data exists in the current string, delete it
    if (m_data) delete[] m_data;
 
    m_length = str.m_length;
 
    // copy the data from str to the implicit object
    m_data = new char[str.m_length];
 
    for (int i=0; i < str.m_length; ++i)
        m_data[i] = str.m_data[i];
 
    // return the existing object so we can chain this operator
    return *this;
}
 
int main()
{
    MyString alex("Alex", 5); // Meet Alex
    MyString employee;
    employee = alex; // Alex is our newest employee
    std::cout << employee; // Say your name, employee
 
    return 0;
}

First, run the program as it is. You’ll see that the program prints “Alex” as it should.

Now run the following program:

int main()
{
    MyString alex("Alex", 5); // Meet Alex
    alex = alex; // Alex is himself
    std::cout << alex; // Say your name, Alex
 
    return 0;
}

You’ll probably get garbage output. What happened?

你可能会得到垃圾的输出。

Consider what happens in the overloaded operator= when the implicit object AND the passed in parameter (str) are both variable alex. In this case, m_data is the same as str.m_data. The first thing that happens is that the function checks to see if the implicit object already has a string. If so, it needs to delete it, so we don’t end up with a memory leak. In this case, m_data is allocated, so the function deletes m_data. But because str is the same as *this, the string that we wanted to copy has been deleted and m_data (and str.m_data) are dangling.

考虑一下当隐藏的this指针指向的对象和参数里面传入的右值的对象是同一个对象的情况。在这种情况下,mdata就和str的mdata是同一个。函数检查如果有数据的话,就把数据释放避免内存泄漏。在这个例子中,有数据所以delete掉了。但是因为赋值号左右的两个对象是同一个。我们想要赋值的对象已经被我们删掉了,现在的mdata指针已经被悬挂起来了(像一个钩子没有勾到任何东西)

Later on, we allocate new memory to m_data (and str.m_data). So when we subsequently copy the data from str.m_data into m_data, we’re copying garbage, because str.m_data was never initialized.

然后,我们分配来新的内存空间。所以我们就紧接着拷贝数据。我们拷贝一堆没有初始化过的垃圾数据。

Detecting and handling self-assignment

Fortunately, we can detect when self-assignment occurs. Here’s an updated implementation of our overloaded operator= for the MyString class:

幸运的是我们可以检测到这件事情。这里有一份更新了的赋值运算符函数的实现。

MyString& MyString::operator= (const MyString& str)
{
	// self-assignment check
	if (this == &str)
		return *this;
 
	// if data exists in the current string, delete it
	if (m_data) delete[] m_data;
 
	m_length = str.m_length;
 
	// copy the data from str to the implicit object
	m_data = new char[str.m_length];
 
	for (int i = 0; i < str.m_length; ++i)
		m_data[i] = str.m_data[i];
 
	// return the existing object so we can chain this operator
	return *this;
}

By checking if the address of our implicit object is the same as the address of the object being passed in as a parameter, we can have our assignment operator just return immediately without doing any other work.

通过检查赋值号左右操作数的地址是不是一样,我们可以让我们的赋值运算符函数什么都不干,直接返回this指针指向的对象(这里不能把右值返回出去,因为Fraction&不能指向一个const的Fraction&)。

Because this is just a pointer comparison, it should be fast, and does not require operator== to be overloaded.

因为这里仅仅是一个地址计算,所以会非常快。而且我们也不需要重载==关系运算符的函数。

When not to handle self-assignment

First, there is no need to check for self-assignment in a copy-constructor. This is because the copy constructor is only called when new objects are being constructed, and there is no way to assign a newly created object to itself in a way that calls to copy constructor.

首先没有必要在拷贝构造函数里面检查指针。因为拷贝构造函数只在新的对象被构建时调用,不能通过使用拷贝构造函数的方式把一个新的对象赋值给它自己。

Second, the self-assignment check may be omitted in classes that can naturally handle self-assignment. Consider this Fraction class assignment operator that has a self-assignment guard:

其次是这种检查也可能在本身就可以检查自我赋值的类中被略去。

// A better implementation of operator=
Fraction& Fraction::operator= (const Fraction &fraction)
{
    // self-assignment guard
    if (this == &fraction)
        return *this;
 
    // do the copy
    m_numerator = fraction.m_numerator; // can handle self-assignment
    m_denominator = fraction.m_denominator; // can handle self-assignment
 
    // return the existing object so we can chain this operator
    return *this;
}

If the self-assignment guard did not exist, this function would still operate correctly during a self-assignment (because all of the operations done by the function can handle self-assignment properly).

即便没有自我赋值的检查,这个函数依然可以正常的运行

Because self-assignment is a rare event, some prominent C++ gurus recommend omitting the self-assignment guard even in classes that would benefit from it. We do not recommend this, as we believe it’s a better practice to code defensively and then selectively optimize later.

因为自我赋值的情况相当少见,有一些接触的C++专家建议略去自我赋值的检查代码,即便是在有一些类可以从中获益的情况下。在这里,我们不建议这样做,因为我们相信先把代码写的防卫性强一些,然后再选择性的做优化会更好。

Default assignment operator

Unlike other operators, the compiler will provide a default public assignment operator for your class if you do not provide one. This assignment operator does memberwise assignment (which is essentially the same as the memberwise initialization that default copy constructors do).

不像别的运算符编译器不给你提供,对于赋值运算符,编译器会给你提供一个默认的public赋值运算符函数,如果你自己没有写的话。这个运算符也是和拷贝构造函数里面的函数那样在做成员拷贝。

Just like other constructors and operators, you can prevent assignments from being made by making your assignment operator private or using the delete keyword:

就像别的构造函数和运算符重载函数一样,你通过將函数设置为private或者把它用delete删掉的方式,阻止赋值运算。

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
	int m_numerator;
	int m_denominator;
 
public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
	// Copy constructor
	Fraction(const Fraction &copy) = delete;
 
	// Overloaded assignment
	Fraction& operator= (const Fraction &fraction) = delete; // no copies through assignment!
 
	friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
        
};
 
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
	out << f1.m_numerator << "/" << f1.m_denominator;
	return out;
}
 
int main()
{
    Fraction fiveThirds(5, 3);
    Fraction f;
    f = fiveThirds; // compile error, operator= has been deleted
    std::cout << f;
 
    return 0;
}