Skip to content

Latest commit

 

History

History
282 lines (189 loc) · 13 KB

18.9 — Object slicing.md

File metadata and controls

282 lines (189 loc) · 13 KB

18.9 — Object slicing

Let’s go back to an example we looked at previously:

回忆一下之前看到过的例子

class Base
{
protected:
    int m_value{};
 
public:
    Base(int value)
        : m_value{ value }
    {
    }
 
    virtual const char* getName() const { return "Base"; }
    int getValue() const { return m_value; }
};
 
class Derived: public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }
 
    virtual const char* getName() const { return "Derived"; }
};
 
int main()
{
    Derived derived{ 5 };
    std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
 
    Base &ref{ derived };
    std::cout << "ref is a " << ref.getName() << " and has value " << ref.getValue() << '\n';
 
    Base *ptr{ &derived };
    std::cout << "ptr is a " << ptr->getName() << " and has value " << ptr->getValue() << '\n';
 
    return 0;
}

In the above example, ref references and ptr points to derived, which has a Base part, and a Derived part. Because ref and ptr are of type Base, ref and ptr can only see the Base part of derived -- the Derived part of derived still exists, but simply can’t be seen through ref or ptr. However, through use of virtual functions, we can access the most-derived version of a function. Consequently, the above program prints:

在上面的例子中,引用和指针指向一个派生类对象,这个派生类对象有基类部分和派生类部分。因为ref和ptr是Base的类型。所以ref和ptr只能看到派生类的记在部分。派生类的派生类部分仍然存在,只是不能被ref和ptr看见。然而通过使用虚函数,我们可以访问最派生的那个函数版本,因此上面的函数就打印了下面的结果:

derived is a Derived and has value 5
ref is a Derived and has value 5
ptr is a Derived and has value 5

But what happens if instead of setting a Base reference or pointer to a Derived object, we simply assign a Derived object to a Base object?

但是如果我们我们不是设置指针或者设置引用,而是直接把一个派生类对象赋值给一个基类对象呢?

int main()
{
    Derived derived{ 5 };
    Base base{ derived }; // what happens here?
    std::cout << "base is a " << base.getName() << " and has value " << base.getValue() << '\n';
 
    return 0;
}

Remember that derived has a Base part and a Derived part. When we assign a Derived object to a Base object, only the Base portion of the Derived object is copied. The Derived portion is not. In the example above, base receives a copy of the Base portion of derived, but not the Derived portion. That Derived portion has effectively been “sliced off”. Consequently, the assigning of a Derived class object to a Base class object is called object slicing (or slicing for short).

派生类对象有基类部分和派生类部分,当我们赋值派生类对象给基类对象的时候,只有派生类对象的基类部分被拷贝。这个派生类的部分是没有被拷贝的。在上面的例子中,base收到了派生类的基类部分的拷贝,没有派生类部分。那个派生类的部分已经被切掉了。因此将派生类对象赋值给基类对象的过程叫做对象截断。(因为vptr这个东西不会再赋值过程中被拷贝,这个东西是编译器完成填写的,它指向了Base的虚表, 我猜)

Because variable base does not have a Derived part, base.getName() resolves to Base::getName().

因为基类对象没有派生类的部分,所以base.getName解析到了基类的getName函数版本。

The above example prints:

base is a Base and has value 5

Used conscientiously, slicing can be benign. However, used improperly, slicing can cause unexpected results in quite a few different ways. Let’s examine some of those cases.

好好使用的话,切片也可以是良性的。然而,不正当的使用,切片就会导致意料之外的许多结果。我们对那些结果做一些实验:

Slicing and functions

Now, you might think the above example is a bit silly. After all, why would you assign derived to base like that? You probably wouldn’t. However, slicing is much more likely to occur accidentally with functions.

现在你可能觉得上面的例子又有点傻了。你干嘛要赋值一个派生类对象给基类对象呢?你可能不想那样做。然而对象截断是有可能发生在和函数配合的时候的。

Consider the following function:

思考下面的函数

void printName(const Base base) // note: base passed by value, not reference
{
    std::cout << "I am a " << base.getName() << '\n';
}

This is a pretty simple function with a const base object parameter that is passed by value. If we call this function like such:

这是一个简单的接受基类对象作为参数的函数。

int main()
{
    Derived d{ 5 };
    printName(d); // oops, didn't realize this was pass by value on the calling end
 
    return 0;
}

When you wrote this program, you may not have noticed that base is a value parameter, not a reference. Therefore, when called as printName(d), we might have expected base.getName() to call virtualized function getName() and print “I am a Derived”, that is not what happens. Instead, Derived object d is sliced and only the Base portion is copied into the base parameter. When base.getName() executes, even though the getName() function is virtualized, there’s no Derived portion of the class for it to resolve to. Consequently, this program prints:

当你这样写程序的时候,你可能没有注意到基类是一个值类型的参数,不是引用类型,所以在调用这个函数的时候,我们可能期待base.GetName调用虚函数的getName,打印说自己是派生类。但那并不是事实,事实是,派生类对象被截断了,只有基类的部分被拷贝到了函数参数里面,当这个getName被执行的时候,尽管getName是虚函数,但是没有Derived部分可供他解析,所以这个函数就打印了下面的结果:

I am a Base

In this case, it’s pretty obvious what happened, but if your functions don’t actually print any identifying information like this, tracking down the error can be challenging.

在这个例子中,发生了什么非常明显,但是如果函数不是打印什么消息的话,跟踪这样的错误是很不容易的。

Of course, slicing here can all be easily avoided by making the function parameter a reference instead of a pass by value (yet another reason why passing classes by reference instead of value is a good idea).

通过使这个函数的参数成为一个引用,截断可以被很容易的避免。(又是一个传递引用比传递值好的例证)

void printName(const Base &base) // note: base now passed by reference
{
    std::cout << "I am a " << base.getName() << '\n';
}
 
int main()
{
    Derived d{ 5 };
    printName(d);
 
    return 0;
}

This prints:

I am a Derived

Slicing vectors

Yet another area where new programmers run into trouble with slicing is trying to implement polymorphism with std::vector. Consider the following program:

没错,另一个新手可能出错的地方是在和vector配合的时候,试图实现多态。思考一下下面的程序。

#include <vector>
 
int main()
{
	std::vector<Base> v{};
	v.push_back(Base{ 5 }); // add a Base object to our vector
	v.push_back(Derived{ 6 }); // add a Derived object to our vector
 
        // Print out all of the elements in our vector
	for (const auto& element : v)
		std::cout << "I am a " << element.getName() << " with value " << element.getValue() << '\n';
 
	return 0;
}

This program compiles just fine. But when run, it prints:

这个程序可以通过编译,但是当他运行的时候,它产生的结果如下

I am a Base with value 5
I am a Base with value 6

Similar to the previous examples, because the std::vector was declared to be a vector of type Base, when Derived(6) was added to the vector, it was sliced.

跟之前的例子是相似的。

Fixing this is a little more difficult. Many new programmers try creating a std::vector of references to an object, like this:

修复这个问题是优点麻烦的,新手可能会直接把类型参数改成Base的引用类型。

std::vector<Base&> v{};

Unfortunately, this won’t compile. The elements of std::vector must be assignable, whereas references can’t be reassigned (only initialized).

不幸运的是,vector的元素必须是可以被赋值的,而引用只能在初始化的时候被赋值。

One way to address this is to make a vector of pointers:

解决这个问题的一个办法是使用指针:

#include <iostream>
#include <vector>
 
int main()
{
	std::vector<Base*> v{};
	
	Base b{ 5 }; // b and d can't be anonymous objects
	Derived d{ 6 };
 
	v.push_back(&b); // add a Base object to our vector
	v.push_back(&d); // add a Derived object to our vector
 
	// Print out all of the elements in our vector
	for (const auto* element : v)
		std::cout << "I am a " << element->getName() << " with value " << element->getValue() << '\n';
 
	return 0;
}

This prints:

I am a Base with value 5
I am a Derived with value 6

which works! A few comments about this. First, nullptr is now a valid option, which may or may not be desirable. Second, you now have to deal with pointer semantics, which can be awkward. But on the upside, this also allows the possibility of dynamic memory allocation, which is useful if your objects might otherwise go out of scope.

这样管用,但还是要说一下。首先,空指针现在变成了一个合法的选项了,你可以把空指针放到数组里面,这可能是不想要的结果。第二,你现在要处理指针的事情了,而处理指针可能不是那么轻松。但是从好的方面来说,这样也允许了动态分配内存的可能,如果你的对象想跑出作用域范围,这可能很有用。

The Frankenobject

In the above examples, we’ve seen cases where slicing lead to the wrong result because the derived class had been sliced off. Now let’s take a look at another dangerous case where the derived object still exists!

在上面的例子中,我们已经看到截断导致了一些错误的结果,因为派生类的部分被切掉了。现在让我们看一下另一个危险的例子。

Consider the following code:

int main()
{
    Derived d1{ 5 };
    Derived d2{ 6 };
    Base &b{ d2 };
 
    b = d1; // this line is problematic
 
    return 0;
}

The first three lines in the function are pretty straightforward. Create two Derived objects, and set a Base reference to the second one.

前面三行是很简单的,创建两个派生类对象,然后设置一个基类引用到第二个对象身上。

The fourth line is where things go astray. Since b points at d2, and we’re assigning d1 to b, you might think that the result would be that d1 would get copied into d2 -- and it would, if b were a Derived. But b is a Base, and the operator= that C++ provides for classes isn’t virtual by default. Consequently, only the Base portion of d1 is copied into d2.

第四行让人很晕。因为b指向d2,我们正在赋值d1给b,你可能认为结果可能是d1会拷贝到d2里面。如果b是一个派生类的引用的话,他就会。但现在b是基类的引用,C++提供的默认的赋值运算符重载函数并不是虚函数。因此只有Base的部分被赋值给了d1.

As a result, you’ll discover that d2 now has the Base portion of d1 and the Derived portion of d2. In this particular example, that’s not a problem (because the Derived class has no data of its own), but in most cases, you’ll have just created a Frankenobject -- composed of parts of multiple objects. Worse, there’s no easy way to prevent this from happening (other than avoiding assignments like this as much as possible).

结果是你会发现d2现在有了d1的基类部分,d2的派生类部分。在这个特定例子中这不是什么问题。但是在大部分情况下,你会创造一个科学怪人。从两个对象组合起来的对象。更坏的事情是,没有阻止这件事情的容易的办法。(要么你就只能避免赋值运算了)

Conclusion

Although C++ supports assigning derived objects to base objects via object slicing, in general, this is likely to cause nothing but headaches, and you should generally try to avoid slicing. Make sure your function parameters are references (or pointers) and try to avoid any kind of pass-by-value when it comes to derived classes.

尽管C++允许你赋值派生类对象给基类对象通过对象截断。通常来说,这样做没什么好书,但是会让你很头疼。你应该尽量避免截断。确保你的函数参数是引用或者指针,而且尽力避免任何发生在派生类型上的传值发生。