Skip to content

Latest commit

 

History

History
346 lines (226 loc) · 15.7 KB

18.10 — Dynamic casting.md

File metadata and controls

346 lines (226 loc) · 15.7 KB

18.10 — Dynamic casting

Way back in lesson 6.16 -- Explicit type conversion (casting) and static_cast, we examined the concept of casting, and the use of static_cast to convert variables from one type to another.

前面我们介绍了类型造型的概念,static_cast的用法(把变量从一种类型变成另一种类型)

In this lesson, we’ll continue by examining another type of cast: dynamic_cast.

在这一节我们要学另一个类型转换:dynamic_cast.

The need for dynamic_cast dynamicCast的需要

When dealing with polymorphism, you’ll often encounter cases where you have a pointer to a base class, but you want to access some information that exists only in a derived class.

在处理多态的时候,你将会遇到这样的情况:你有一个指向派生类的基类指针,但是你想访问更多的关于派生类部分的内容。

Consider the following (slightly contrived) program:

思考下面的例子:

#include <iostream>
#include <string>
 
class Base
{
protected:
	int m_value{};
 
public:
	Base(int value)
		: m_value{value}
	{
	}
	
	virtual ~Base() = default;
};
 
class Derived : public Base
{
protected:
	std::string m_name{};
 
public:
	Derived(int value, const std::string& name)
		: Base{value}, m_name{name}
	{
	}
 
	const std::string& getName() const { return m_name; }
};
 
Base* getObject(bool bReturnDerived)
{
	if (bReturnDerived)
		return new Derived{1, "Apple"};
	else
		return new Base{2};
}
 
int main()
{
	Base *b{ getObject(true) };
 
	// how do we print the Derived object's name here, having only a Base pointer?
 
	delete b;
 
	return 0;
}

In this program, function getObject() always returns a Base pointer, but that pointer may be pointing to either a Base or a Derived object. In the case where the pointer is pointing to a Derived object, how would we call Derived::getName()?

在这个程序中getObject总是返回一个基类指针,但是那个指针有可能指向一个派生类,也有可能指向一个基类。在基类指针指向派生类对象的情况下,我们怎么才能访问到派生类的getName函数呢?

One way would be to add a virtual function to Base called getName() (so we could call it with a Base pointer/reference, and have it dynamically resolve to Derived::getName()). But what would this function return if you called it with a Base pointer/reference that was actually pointing to a Base object? There isn’t really any value that makes sense. Furthermore, we would be polluting our Base class with things that really should only be the concern of the Derived class.

其中一个办法是给基类中增加一个虚函数,让函数解析往继承关系派生的方向下沉。但是如果这样做的话,一个指向基类对象的基类指针又应该在getName里面怎么返回呢?实质上返回任何值都不合适,因为这个是派生类应该操心的事情。更进一步的说,这样做还会把基类的内容弄乱。

We know that C++ will implicitly let you convert a Derived pointer into a Base pointer (in fact, getObject() does just that). This process is sometimes called upcasting. However, what if there was a way to convert a Base pointer back into a Derived pointer? Then we could call Derived::getName() directly using that pointer, and not have to worry about virtual function resolution at all.

我们知道C++允许你隐式的转换一个派生类指针为基类指针。(getObject就是那样干的)。这个过程有时候被叫做向上造型。然而,有没有一种办法可以向下造型呢?那样的话,我们就可以通过使用造型得到的派生类指针直接调用派生类对象的getName方法了。不用去考虑虚函数的解决方案了。

dynamic_cast

C++ provides a casting operator named dynamic_cast that can be used for just this purpose. Although dynamic casts have a few different capabilities, by far the most common use for dynamic casting is for converting base-class pointers into derived-class pointers. This process is called downcasting.

C++提供了一种造型运算符叫dynamic_cast,可以被用来向下造型。尽管 dynamic casts 有一些不同的能力,但是到目前为止最常用的能力就是把基类指针转换为派生类指针。这个过程叫做向下造型。

Using dynamic_cast works just like static_cast. Here’s our example main() from above, using a dynamic_cast to convert our Base pointer back into a Derived pointer:

使用dynamic_cast的方法跟static_cast是一样的,下面有一个例子,使用了一个向下造型把基类指针转换成了派生类指针。

int main()
{
	Base *b{ getObject(true) };
 
        Derived *d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer
 
        std::cout << "The name of the Derived is: " << d->getName() << '\n';
 
	delete b;
 
	return 0;
}

This prints:

The name of the Derived is: Apple

dynamic_cast failure

The above example works because b is actually pointing to a Derived object, so converting b into a Derived pointer is successful.

上面的例子没出错是因为b实际上指向了一个派生类对象,现在用派生类指针指向派生类对象也没什么不妥。

However, we’ve made quite a dangerous assumption: that b is pointing to a Derived object. What if b wasn’t pointing to a Derived object? This is easily tested by changing the argument to getObject() from true to false. In that case, getObject() will return a Base pointer to a Base object. When we try to dynamic_cast that to a Derived, it will fail, because the conversion can’t be made.

然而,我们已经做了一个危险的假设,我们假设了b是指向派生类对象的情形。如果b不是指向派生类对象,而是指向一个基类对象呢?当我们试图把一个指向基类对象的基类指针转换成一个派生类指针的时候,会失败。因为理应失败呀!

If a dynamic_cast fails, the result of the conversion will be a null pointer.

如果向下造型失败了,这个转换的结果会是一个空指针。

Because we haven’t checked for a null pointer result, we access d->getName(), which will try to dereference a null pointer, leading to undefined behavior (probably a crash).

因为我们没有检查一个空指针的结果,我们就直接调用了getName方法,所以这样的代码会尝试解引用一个空指针,导致一个未定义行为(可能会崩溃)。

In order to make this program safe, we need to ensure the result of the dynamic_cast actually succeeded:

为了让这个程序变得安全一点,我们需要确保向下造型是成功的,这样我们才能正常使用我们得到的造型结果的派生类指针。

int main()
{
	Base *b{ getObject(true) };
 
        Derived *d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer
 
        if (d) // make sure d is non-null
            std::cout << "The name of the Derived is: " << d->getName() << '\n';
 
	delete b;
 
	return 0;
}

Rule: Always ensure your dynamic casts actually succeeded by checking for a null pointer result.

**规则:**你应该一直检查你向下造型的结果指针,是不是为空指针。

Note that because dynamic_cast does some consistency checking at runtime (to ensure the conversion can be made), use of dynamic_cast does incur a performance penalty.

注意,因为动态造型在运行时做一些一致性检查(确保这种转换是可以进行的),所以使用向下造型是有性能上的代价的。

Also note that there are several cases where downcasting using dynamic_cast will not work:

也要注意以下几个向下造型会失败的情况

  1. With protected or private inheritance.

如果继承的类型是protected或者是private继承

  1. For classes that do not declare or inherit any virtual functions (and thus don’t have a virtual table).

对那些不包含虚函数的类,也没办法向下造型(没有虚表), 在父类有虚函数,而子类没有重写的情况下会怎么样呢?

  1. In certain cases involving virtual base classes (see this page for an example of some of these cases, and how to resolve them).

在包含虚基类的某些案例中,也会失败。

Downcasting with static_cast 用static_cast向下造型

It turns out that downcasting can also be done with static_cast. The main difference is that static_cast does no runtime type checking to ensure that what you’re doing makes sense. This makes using static_cast faster, but more dangerous. If you cast a Base* to a Derived*, it will “succeed” even if the Base pointer isn’t pointing to a Derived object. This will result in undefined behavior when you try to access the resulting Derived pointer (that is actually pointing to a Base object).

向下造型也可以用static_cast来完成,主要的区别在于static_cast没有运行时的类型检查来确保你做的事情是合理的。这使得用static_cast的话,会快一点,但是会更危险。如果你把一个基类指针转换成一个派生类指针,它总是会成功,不管这个基类指针指向的是不是派生类对象。在你用派生类指针访问一个基类对象的时候,这可能会导致未定义的行为。

If you’re absolutely sure that the pointer you’re downcasting will succeed, then using static_cast is acceptable. One way to ensure that you know what type of object you’re pointing to is to use a virtual function. Here’s one (not great because it uses a global variable) way to do that:

如果你非常确定你向下造型是肯定能成功的。那么使用static_cast就是可以的了。去确认你的指针指向的对象的办法是使用虚函数。

#include <iostream>
#include <string>
 
// Class identifier
enum class ClassID
{
	base,
	derived
	// Others can be added here later
};
 
class Base
{
protected:
	int m_value{};
 
public:
	Base(int value)
		: m_value{value}
	{
	}
 
	virtual ~Base() = default;
	virtual ClassID getClassID() const { return ClassID::base; }
};
 
class Derived : public Base
{
protected:
	std::string m_name{};
 
public:
	Derived(int value, const std::string& name)
		: Base{value}, m_name{name}
	{
	}
 
	const std::string& getName() const { return m_name; }
	virtual ClassID getClassID() const { return ClassID::derived; }
 
};
 
Base* getObject(bool bReturnDerived)
{
	if (bReturnDerived)
		return new Derived{1, "Apple"};
	else
		return new Base{2};
}
 
int main()
{
	Base *b{ getObject(true) };
 
	if (b->getClassID() == ClassID::derived)
	{
		// We already proved b is pointing to a Derived object, so this should always succeed
		Derived *d{ static_cast<Derived*>(b) };
		std::cout << "The name of the Derived is: " << d->getName() << '\n';
	}
 
	delete b;
 
	return 0;
}

But if you’re going to go through all of the trouble to implement this (and pay the cost of calling a virtual function and processing the result), you might as well just use dynamic_cast.

但是你要搞这么多事情的话,解析虚函数也是有性能代价的。你用dynamic_cast也很好呀。

dynamic_cast and references

Although all of the above examples show dynamic casting of pointers (which is more common), dynamic_cast can also be used with references. This works analogously to how dynamic_cast works with pointers.

上面的所有例子都在展示dynamic_cast对指针的作用。dynamic_cast也可以对引用起作用。这个过程和对指针的作用是相似的。

#include <iostream>
#include <string>
 
class Base
{
protected:
	int m_value;
 
public:
	Base(int value)
		: m_value{value}
	{
	}
 
	virtual ~Base() = default; 
};
 
class Derived : public Base
{
protected:
	std::string m_name;
 
public:
	Derived(int value, const std::string& name)
		: Base{value}, m_name{name}
	{
	}
 
	const std::string& getName() const { return m_name; }
};
 
int main()
{
	Derived apple{1, "Apple"}; // create an apple
	Base &b{ apple }; // set base reference to object
	Derived &d{ dynamic_cast<Derived&>(b) }; // dynamic cast using a reference instead of a pointer
 
	std::cout << "The name of the Derived is: " << d.getName() << '\n'; // we can access Derived::getName through d
 
	return 0;
}

Because C++ does not have a “null reference”, dynamic_cast can’t return a null reference upon failure. Instead, if the dynamic_cast of a reference fails, an exception of type std::bad_cast is thrown. We talk about exceptions later in this tutorial.

因为C++没有空引用,所以在对引用向下造型的时候,bad_cast异常会被抛出,我们会在后面一点探讨异常的事情。

dynamic_cast vs static_cast

New programmers are sometimes confused about when to use static_cast vs dynamic_cast. The answer is quite simple: use static_cast unless you’re downcasting, in which case dynamic_cast is usually a better choice. However, you should also consider avoiding casting altogether and just using virtual functions.

新手可能会static_cast和dynamic_cast的选择感到困惑。答案很简单,除非你要向下造型,你就用static_cast就行。当然你也应该考虑不要做类型转换,只需要调用虚函数就行。

Downcasting vs virtual functions

There are some developers who believe dynamic_cast is evil and indicative of a bad class design. Instead, these programmers say you should use virtual functions.

有一些程序员认为dynamic_cast是邪恶的,意味着一种不好的类设计。这些程序员说你应该使用虚函数,而不是dynamic_cast

In general, using a virtual function should be preferred over downcasting. However, there are times when downcasting is the better choice:

通常来说,应该优先使用虚函数,再使用向下造型,但是有一些时候向下造型是更好的的选择:

  • When you can not modify the base class to add a virtual function (e.g. because the base class is part of the standard library)

    当你没办法修改基类,添加一个虚函数的时候。(比如那个类是标准库)

  • When you need access to something that is derived-class specific (e.g. an access function that only exists in the derived class)

    如果你需要访问一些派生类特有的。(比如一个只存在于派生类中的访问函数)

  • When adding a virtual function to your base class doesn’t make sense (e.g. there is no appropriate value for the base class to return). Using a pure virtual function may be an option here if you don’t need to instantiate the base class.

    当给你的基类添加一个虚函数没有意义的时候(比如在基类的时候没有合适的返回值)。如果你不需要实例化虚类的话,你就把他搞成纯虚函数。

A warning about dynamic_cast and RTTI 关于daynamic_cast和运行时类型信息

Run-time type information (RTTI) is a feature of C++ that exposes information about an object’s data type at runtime. This capability is leveraged by dynamic_cast. Because RTTI has a pretty significant space performance cost, some compilers allow you to turn RTTI off as an optimization. Needless to say, if you do this, dynamic_cast won’t function correctly.

RTTI是是一个C++在允许时暴露一个对象的数据类型的特性。这个功能被dynamic_cast利用了。因为RTTI有很好的性能,有一些编译器允许你为了性能把RTTI关掉,如果把这个关掉的话,dynamic_cast就会失效。