c ++带有嵌套多态模板的动态转换

c++ Dynamic cast with nested polymorphic template

我正在使用 PolyM message queue 提供消息

class Msg

以及带有模板负载的消息

template<typename PayloadType> class DataMsg: public Msg

直到我将 DataMsg 模板嵌套在另一个 DataMsg 中,这一直有效,就像这样...

DataMsg<DataMsg<int>>

并尝试提取嵌套的 DataMsg 以将其传递以进行进一步处理。为此,我像这样转换为 Msg 基本类型:

function(Msg &base) {
    auto &msg = dynamic_cast<DataMsg<Msg>&>(base).getPayload();
}

此转换失败并出现错误转换异常。使用 static_cast 似乎没有任何副作用。

从多态的角度来看,我看不出我的方法有什么问题。由于动态转换适用于非嵌套类型,它应该也适用于嵌套类型吗?

我已经在 PolyM GitHub issues page 上问过这个问题,但是没有得到正确的解释为什么转换失败。

这是一个显示问题的简单示例:

#include <memory>
#include <iostream>

using namespace std;

class Msg {
  public:
     virtual ~Msg() {}
};

template<typename PayloadType>
class DataMsg: public Msg {
  public:
     virtual ~DataMsg() {}

     PayloadType& getPayload() const
     {
       return *pl_;
     }

  private:
    PayloadType* pl_;
};

static void getInnerMsg(Msg &msgMsg) { 
    try { 
        auto &msg = dynamic_cast<DataMsg<Msg>&>(msgMsg).getPayload();
        std::cout << "cast OK" << endl;
    } catch ( std::bad_cast& bc ) {      
        std::cerr << "bad_cast caught: " << bc.what() << endl;
    }
}

和我的测试用例:

int main(int argc, char *argv[])
{    
     Msg                     msg1;
     DataMsg<int>            msg2;
     DataMsg<Msg>            msg3;
     DataMsg<DataMsg<int>>   msg4;

    cout << "expect bad cast (no nested message)" << endl;
    getInnerMsg(msg1);
    cout << "-------------" << endl;
    cout << "expect bad cast (no nested message)" << endl;
    getInnerMsg(msg2);
    cout << "-------------" << endl;
    cout << "expect successful cast (nested message base type)" << endl;
    getInnerMsg(msg3);
    cout << "-------------" << endl;
    cout << "expect successful cast (nested message child type)" << endl;
    getInnerMsg(msg4);

    return 0;
}

运行 与 "g++ test.cpp -o test.x && ./test.x"。 GitHub 问题包含更完整的用法示例。

正如 github 问题回复中(简要地)解释的那样:

function(Msg &base) {
    auto &msg = dynamic_cast<DataMsg<Msg>&>(base).getPayload();
}

这里您尝试从 Msg 转换为 DataMsg<Msg>。但是,您指出 base 的动态类型是 DataMsg<DataMsg<int>>DataMsg<Msg> 继承自 MsgDataMsg<DataMsg<int>> 继承自 Msg,但它们在其他方面不相关(无论模板参数之间的关系如何)。

更一般地说,MyClass<Derived> 不继承自 MyClass<Base> -(无论两者是否派生自相同的基础 class),因此动态转换一个 from/to另一个是非法的(through/from 是否有共同基础)。

From a polymorphic view point I can't see anything wrong with my approach. As dynamic casting works for the non-nested type, it should work for the nested one too?

就模板而言,A<B>A<C> 是截然不同的不相关类型,即使 BC 是相关的。使用通过 static_cast 获得的引用会导致未定义的行为。举例来说,如果我们要手写您得到的 class 层次结构,它将类似于此

struct base { ~virtual base() = default; };

struct foo : base {
    base *p;
};

struct bar : base {
    foo *p;
};

在上面的示例中,如果对象的动态类型是bar,则不能转换为foo&,这些类型之间不在继承链中,并且动态转换将失败。但是一个static_castbase&(指一个bar)到foo&都会成功。没有 运行 时间检查,编译器相信你所说的类型,但你没有说实话。如果您使用该引用,则会出现未定义的行为。遗憾的是 "working" 的出现是未定义行为的有效表现。

This works until I nest the DataMsg template inside another DataMsg ... try to extract the nested DataMsg

到目前为止,您的逻辑对于解包嵌套消息具有直观意义。

您有一个通用消息,其中包含一个包含某种特定类型的动态具体消息的通用 header,提取特定消息负载,其中包含另一条特定类型的消息,等等。

一切都很好。

您的概念性问题是您将嵌套模板参数视为嵌套消息 objects,它们根本不一样。

template<typename PayloadType> class DataMsg: public Msg
    // ...
  private:
    PayloadType* pl_;
};

表示 DataMsg 模板的每个实例化 is-a Msg 和它 has-a 指向包含的有效载荷的指针。

现在,DataMsg 的每个实例化都是一个新类型 DataMsg<X>,它与任何其他实例化 DataMsg<Y> 无关,即使 XY是相关的(除了它们都派生自 Msg)。所以:

DataMsg<DataMsg<int>>

is-a Msghas-a 指向 DataMsg<int> (它本身 is-a Msg), 而

DataMsg<Msg>

还有is-aMsg,以及has-a指向[=15=的指针],但它仍然是一种与 DataMsg<DataMsg<int>> 无关的新类型(除了具有共同的基础),即使有效负载指针类型是可转换的。

所以,您关于消息布局的想法很好,但您没有在类型系统中正确地建模它们。如果你想这样做,你可以明确专门针对嵌套消息:

using NestedMsg = DataMsg<Msg>;

template<typename NestedPayloadType>
class DataMsg<DataMsg<NestedPayloadType>>: public NestedMsg {
  public:
     NestedPayloadType const & getDerivedPayload() const
     {
       // convert nested Msg payload to derived DataMsg
       return dynamic_cast<DataMsg<NestedPayloadType> const &>(this->getPayload());
     }
};

现在DataMsg<DataMsg<int>>真的is-aDataMsg<Msg>。因此它继承了 Msg const& DataMsg<Msg>::getPayload() const,你仍然可以通过调用 `getDerivedPayload().

来获取 payload 的派生类型 (DataMsg<int>)

你的其他 getPayload 方法也应该 return 一个常量引用,顺便说一句。