强制转换包含多态类型的模板时的未定义行为



由于类型擦除的原因,我有一个可以容纳任何数据类型的模板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

coliru上的实时示例

我有两个问题:

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

问题是A<Base>A<Derived>彼此之间根本没有任何关系。他们的表现可能完全不同。对于您尝试执行的强制转换,A<Base>必须是A<Derived>的基类,但事实显然并非如此。

看起来,你想要创建一个类似智能指针的东西,它的行为就像一个值类型。顺便说一下,我不确定是否可以创建一个支持所有必要转换的值类型。如果在需要支持转换的类型组中存在特定需求或已知的公共基类,则可以实现相应的类。

我不确定您的设计目标,但为了深入讨论,这里有一个典型的类型擦除示例:一个单独的类Foo,它公开了对bar:的擦除调用

#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的单个接口类型,并且您可以用任何类型调用该接口,该类型在结构上满足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);

注:

  • 类型擦除提供自组织多态性
  • 对客户端类型的要求是结构化的,而不是与继承相关的。想想"鸭子打字"或"概念"
  • 类型擦除的设计暴露了功能,而不是层次关系
  • 如果您要求Impl是可复制的(这转化为对T的要求),则可以使Foo可复制
  • 我们使用原始CCD_ 21;没有分配器支持。事实证明,类型擦除分配器的支持非常具有挑战性,尤其是在类型擦除状态应该是可复制的情况下

最新更新