转换持有多态类型的模板时的未定义行为

undefined behavior when casting template holding a polymorphic type

出于类型擦除的原因,我有一个模板 A<T> 可以容纳任何数据类型。当 A 持有派生自 Base 的多态类型 Derived 并且我将其转换为 A<Base> 时,GCC 的未定义行为消毒程序报告 运行 时间错误:

#include <iostream>

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

template<typename T> 
struct A : public I
{
    explicit A(T&& value) : value(std::move(value)) {}
    T& get() { return value; }
private:
    T value;
};

struct Base
{
    virtual ~Base() = default;
    virtual void fun() 
    {
        std::cout << "Derived" << std::endl;
    }
};

struct Derived : Base
{
    void fun() override
    {
        std::cout << "Derived" << std::endl;
    }
};

int main()
{
    I* a_holding_derived = new A<Derived>(Derived());
    A<Base>* a_base = static_cast<A<Base>*>(a_holding_derived);
    Base& b = a_base->get();
    b.fun();
    return 0;
}

编译&运行

$ g++ -fsanitize=undefined -g -std=c++11 -O0 -fno-omit-frame-pointer && ./a.out

输出:

main.cpp:37:62: runtime error: downcast of address 0x000001902c20 which does not point to an object of type 'A'

0x000001902c20: note: object is of type 'A<Derived>'

 00 00 00 00  20 1e 40 00 00 00 00 00  40 1e 40 00 00 00 00 00  00 00 00 00 00 00 00 00  21 00 00 00

              ^~~~~~~~~~~~~~~~~~~~~~~

              vptr for 'A<Derived>'

    #0 0x400e96 in main /tmp/1450529422.93451/main.cpp:37

    #1 0x7f35cb1a176c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)

    #2 0x400be8  (/tmp/1450529422.93451/a.out+0x400be8)


main.cpp:38:27: runtime error: member call on address 0x000001902c20 which does not point to an object of type 'A'

0x000001902c20: note: object is of type 'A<Derived>'

 00 00 00 00  20 1e 40 00 00 00 00 00  40 1e 40 00 00 00 00 00  00 00 00 00 00 00 00 00  21 00 00 00

              ^~~~~~~~~~~~~~~~~~~~~~~

              vptr for 'A<Derived>'

    #0 0x400f5b in main /tmp/1450529422.93451/main.cpp:38

    #1 0x7f35cb1a176c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)

    #2 0x400be8  (/tmp/1450529422.93451/a.out+0x400be8)


Derived

live example on coliru

我有两个问题:

  1. 消毒剂的输出是否正确?
  2. 如果是,从 A<Derived>A<Base> 的有效转换会是什么样子?

问题是 A<Base>A<Derived> 彼此没有任何关系 完全 。它们的表示可能完全不同。对于您尝试执行的演员表,A<Base>A<Derived> 的基数 class 显然不是这种情况。

看来,您想创建类似于值类型的智能指针之类的东西。副手,我不确定是否可以创建支持所有必要转换的值类型。如果在需要支持转换的类型组中存在特定需求或已知的共同基础 class,则可以实施相应的 class。

我不确定您的设计目标,但为了正确看待讨论,这里有一个典型的类型擦除示例:一个 class Foo 公开了 class Foo 的擦除调用=14=]:

#include <memory>
#include <type_traits>
#include <utility>

class Foo
{
    struct ImplBase
    {
        virtual ~ImplBase() = default;
        virtual int bar(int, int) = 0;  // This line is the whole point!
    };

    std::unique_ptr<ImplBase> impl;

    template <typename T> struct Impl : ImplBase
    {
        Impl(T t) t_(std::move(t)) {}

        int bar(int a, int b) override { return t_.bar(a, b); }

        T t_;
    };

public:
    template <typename T>
    Foo(T && x)
    : impl(new Impl<typename std::decay<T>::type>(std::forward<T>(x)))
    {}

    int bar(int a, int b)  // Not virtual! Foo is a "value-like" class.
    {
        return impl->bar(a, b);
    }
};

这种方法的实用性在于,您现在可以拥有一个使用 Foo 的接口类型,并且可以使用 any 调用此接口在结构上满足 Impl 要求的类型(您当然会在不参考实施细节的情况下记录)。

例如,考虑以下函数:

void DoSomething(Foo a, int x, int y)
{
    UpdateCounter(a.bar(x, y));
}

这个函数可以在一个单独的翻译单元中定义和编译,永远不会再被触及。但是未来的用户,可能永远不会与 DoSomething 作者有因果联系,可以传递任意对象,这些对象公开了 bar 函数:

struct X { double bar(long int, int); };
struct Y { char bar(int, float, bool = false); };

DoSomething(Foo(X{}), 10, 20);
DoSomething(Foo(Y{}), 20, 10);

备注:

  • 类型擦除提供 临时多态性
  • 对客户端类型的要求是结构性的,与继承无关。想想 "duck typing" 或 "concept".
  • 类型擦除设计公开了功能,而不是层次相关性。
  • 如果您要求 Impl 可复制(这转化为对 T 的要求),您可以使 Foo 可复制。
  • 我们使用原始的new;没有分配器支持。类型擦除分配器支持已被证明非常具有挑战性,特别是如果类型擦除状态应该是可复制的。