polymorphic_cast

优质
小牛编辑
134浏览
2023-12-01

头文件: "boost/cast.hpp"

C++中的多态转型是用 dynamic_cast来实现的。dynamic_cast有一个有时会导致错误代码的特性,那就是它对于所使用的不同类型会有不同的行为。在用于一个引用类型时,如果转型失败,dynamic_cast 会抛出一个std::bad_cast异常。这样做的原因很简单,因为C++里不允许有空的引用,所以要么转型成功,要么转型失败而你获得一个异常。当然,在 dynamic_cast 用于一个指针类型时,失败时将返回空指针。

dynamic_cast的这种对指针和引用类型的不同行为以前被认为是一个有用 的特性,因为它允许程序员表达他们的意图。典型地,如果转型失败不是一种逻辑错误,就使用指针转型,如果它确是一种错误,就使用引用转型。不幸的是,两种 方法之间的区别仅在于一个*号和一个&号,这种细微的差别是不自然的。如果想把指针转型失败作为错误处理,该怎么办?为了通过自动抛出异常来清楚 地表达这一点,也为了让代码更一致,Boost提供了polymorphic_cast. 它在转型失败时总是抛出一个 std::bad_cast 异常。

在《The C++ Programming Language 3rd Edition》中,Stroustrup对于指针类型的dynamic_cast说了以下一段话,事实是它可以返回空指针:

"偶尔可能会不小心忘了测试指针是否为空。如果这困扰了你,你可以写一转型函数在转型失败时抛出异常。"

polymorphic_cast 正是这样一个转型函数。

用法

polymorphic_cast 的用法类似于 dynamic_cast, 除了 (正是它的意图) 在转型失败时总是抛出一个 std::bad_cast 异常。polymorphic_cast 的另一个特点是它是一个函数,必要时可以被重载。作为对我们的C++词汇表的一个自然扩展,它使得代码更清晰,类型转换也更少错误。要使用它,就要包含头文件"boost/cast.hpp". 这个函数泛化了要转换的类型,并接受一个要进行转型的参数。

template <class Target, class Source>
  polymorphic_cast(Source* p);

要注意的是,polymorphic_cast 没有针对引用类型的版本。原因是那是dynamic_cast已经实现了的,没有必须让 polymorphic_cast 重复C++语言中已有的功能。以下例子示范了与 dynamic_cast类似的语法。

向下转型和交叉转型

使用dynamic_castpolymorphic_cast可能有两种典型的情况:从基类向派生类的向下转型,或者交叉转型,即从一个基类到另一个基类。以下例子示范了使用polymorphic_cast的两类转型。这里有两个基类,base1base2, 以及一个从两个基类公有派生而来的类 derived

#include <iostream>
#include <string>
#include "boost/cast.hpp"

class base1 {
public:
  virtual void print() {
    std::cout << "base1::print()\n";
  }

  virtual ~base1() {}
};

class base2 {
public:

  void only_base2() {
    std::cout << "only_base2()\n";
  }

  virtual ~base2() {}
};

class derived : public base1, public base2 {
public:

  void print() {
    std::cout << "derived::print()\n";
  }

  void only_here() {
    std::cout << "derived::only_here()\n";
  }
  void only_base2() {
    std::cout << "Oops, here too!\n";
  }
};

int main() {
  base1* p1=new derived;

 p1->print();

  try {
    derived* pD=boost::polymorphic_cast<derived*>(p1);
    pD->only_here();
    pD->only_base2();

    base2* pB=boost::polymorphic_cast<base2*>(p1);
    pB->only_base2();

  }
  catch(std::bad_cast& e) {
    std::cout << e.what() << '\n';
  }

  delete p1;
}

我们来看看 polymorphic_cast 是如何工作的,首先我们创建一个 derived 的实例,然后通过不同的基类指针以及派生类指针来操作它。对p1使用的第一个函数是print, 它是base1derived的一个虚拟函数。我们还使用了向下转型,以便可以调用 only_here, 它仅在 derived中可用:

derived* pD=boost::polymorphic_cast<derived*>(p1);
pD->only_here();

注意,如果 polymorphic_cast 失败了,将抛出一个 std::bad_cast 异常,因此这段代码被保护在一个 try/catch 块中。这种做法与使用引用类型的dynamic_cast正好是一样的。指针 pD 随后被用来调用函数 only_base2. 这个函数是base2中的非虚拟函数,但是在derived中也提供了,因此隐藏了base2中的版本。因而我们需要执行一个交叉转型来获得一个base2指针,才可以调用到 base2::only_base2 而不是 derived::only_base2.

base2* pB=boost::polymorphic_cast<base2*>(p1);
pB->only_base2();

再一次,如果转型失败,将会抛出异常。这个例子示范了如果转型失败被认为是错误的话,使用polymorphic_cast可以多容易地进行错误处理。不需要测试空指针,也不会把错误传播到函数以外。正如我们即将看到的,dynamic_cast 有时会为这类代码增加不必要的复杂性;它还可能导致未定义行为。

dynamic_cast 对 polymorphic_cast

为了看一下这两种转型方法之间的不同,[3] 我们把它们放在一起来比较一下复杂性。我们将重用前面例子中的类 base1, base2, 和 derived。你会发现在对指针类型使用dynamic_cast时,测试指针的有效性是一种既乏味又反复的事情,这使得测试很容易被紧张的程序员所忽略掉。

[3] 技术上,dynamic_cast 是转型操作符,而 polymorphic_cast 是函数模板。

void polymorphic_cast_example(base1* p) {
  derived* pD=boost::polymorphic_cast<derived*>(p);
  pD->print();

  base2* pB=boost::polymorphic_cast<base2*>(p);
  pB->only_base2();
}

void dynamic_cast_example(base1* p) {
  derived* pD=dynamic_cast<derived*>(p);
  if (!pD)
    throw std::bad_cast();
  pD->print();

  base2* pB=dynamic_cast<base2*>(p);
  if (!pB)
    throw std::bad_cast();

  pB->only_base2();

}

int main() {
  base1* p=new derived;
  try {
    polymorphic_cast_example(p);
    dynamic_cast_example(p);
  }
  catch(std::bad_cast& e) {
    std::cout << e.what() << '\n';
  }
  delete p;
}

这两个函数,polymorphic_cast_exampledynamic_cast_example, 使用不同的方法完成相同的工作。差别在于无论何时对指针使用 dynamic_cast ,我们都要记住测试返回的指针是否为空。在我们的例子里,这种情况被认为是错误的,因此要抛出一个类型为 bad_cast 的异常。[4] 如果使用 polymorphic_cast, 错误的处理被局限在std::bad_cast的异常处理例程中, 这意味着我们不需要为测试转型的返回值而操心。在这个简单的例子中,不难记住要测试返回指针的有效性,但还是要比使用polymorphic_cast做更多的工作。如果是几百行的代码,再加上两三个程序员来维护这个函数的话,忘记测试或者抛出了错误的异常的风险就会大大增加。

[4] 当然,返回指针无论如何都必须被检查,除非你绝对肯定转型不会失败。

polymorphic_cast 不总是正确的选择

如果说失败的指针转型不应被视为错误,你就应该使用 dynamic_cast 而不是 polymorphic_cast. 例如,一种常见的情形是使用 dynamic_cast 来进行类型确定测试。使用异常处理来进行几种类型的转换测试是低效的,代码也很难看。这种情形下 dynamic_cast 就很有用了。当我们同时使用 polymorphic_castdynamic_cast时,你应该非常清楚你自己的意图。即使没有 polymorphic_cast, 如果人们知道使用dynamic_cast的方法,他仍然可以达到相同的安全性,如下例所示。

void failure_is_error(base1* p) {

  try {
    some_other_class& soc=dynamic_cast<some_other_class&>(*p);
    // 使用 soc
   }
  catch(std::bad_cast& e) {
    std::cout << e.what() << '\n';
  }
}

void failure_is_ok(base1* p) {
  if (some_other_class* psoc=
    dynamic_cast<some_other_class*>(p)) {
    // 使用 psoc
  }
}

在这个例子中,指针 p 被解引用[5] 并被转型为 some_other_class的引用。这调用了dynamic_cast的异常抛出版本。例子中的第二部分使用了不会抛出异常的版本来转型到指针类型。你是否认为这是清晰、简明的代码,答案取决于你的经验。经验丰富的C++程序员会非常明白这段程序。是不是所有看到这段代码的人都十分熟悉dynamic_cast呢,或者他们不知道dynamic_cast的 行为要取决于进行转型的是指针还是引用呢?你或者一个维护程序员是否总能记得对空指针进行测试?维护代码的程序员是否知道要对指针进行解引用才可以在转型 失败时获得异常?你真的想在每次你需要这样的行为时都写相同的逻辑吗?抱歉说了这么多,这只是想表明,如果转型失败应该要抛出异常,那么 polymorphic_cast 要比 dynamic_cast 更坚固也更清晰。它要么成功,产生一个有效的指针,要么失败,抛出一个异常。简单的规则总是更容易被记住。

[5] 如果指针 p 为空,该例将导致未定义行为,因为它解引用了一个空指针。

我们还没有看到如何通过重载 polymorphic_cast 来解决一些不常见的转型需求,但你应该知道这是可能的。何时你会想改变多态转型的缺省行为呢?有一种情形是句柄/实体类(handle/body-classes), 向下转型的规则可能会与缺省的不同,或者是根本不允许。

总结

必须记住,其它人将要维护我们写的代码。这意味着我们必须确保代码以及它的意图是清晰并且易懂的。这一点可以通过注释部分地解决,但对于任何人,更容易的方法是不需加以说明的代码。当(指针)转型失败被认为是异常时,polymorphic_castdynamic_cast更能清晰地表明代码的意图,它也导致更短的代码。如果转型失败不应被认为是错误,则应该使用dynamic_cast,这使得dynamic_cast的使用更为清楚。仅仅使用 dynamic_cast 来表明两种不同的意图很容易出错,而不够清楚。抛出异常与不抛出异常这两个不同的版本对于大多数程序员而言太微妙了。

何时使用 polymorphic_castdynamic_cast:

  • 当一个多态转型的失败是预期的时候,使用 dynamic_cast&lt;T*&gt;. 它清楚地表明转型失败不是一种错误。

  • 当一个多态转型必须成功以确保逻辑的正确性时,使用 polymorphic_cast&lt;T*&gt;. 它清楚地表明转型失败是一种错误。

  • 对引用类型执行多态转型时,使用 dynamic_cast.