【问题标题】:undefined behavior when casting template holding a polymorphic type转换包含多态类型的模板时的未定义行为
【发布时间】:2015-12-19 13:00:02
【问题描述】:

出于类型擦除的原因,我有一个模板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&lt;Derived&gt;A&lt;Base&gt; 的有效转换是什么样的?

【问题讨论】:

  • I*X 的静态转换具有未定义的行为,因为它实际上指向Y 的子对象,而XY 是不同的类型(分别为@987654338 @ 和 A&lt;Derived&gt;)。
  • @KerrekSB 我确实有另一种环绕A 的类型;我没有在这里展示它以保持示例最小化。
  • OK - 在这种情况下,您必须使用 I 专门作为您的“类型擦除句柄”。公共 API 的每个语义方面都必须通过 I 实现。
  • @KerrekSB 我可以使用两个嵌套的static_casts,但是我需要知道原始类型(以某种方式破坏了类型擦除的目的):example code;有什么办法吗?
  • 我不确定问题是否明确。类型擦除不会给你神奇的力量。它解决了需要在接口中编码的非常特定的问题。例如,any 是类型擦除类的最简单示例,其唯一接口是“类型检查”。 std::function 是一个不同的类型擦除类,其接口是“函数调用运算符”。您需要记住一个要以类型擦除的方式提供的接口。由于您的I 是空的,因此您目前没有完成任何事情。

标签: c++ gcc polymorphism undefined-behavior type-erasure


【解决方案1】:

问题在于A&lt;Base&gt;A&lt;Derived&gt; 彼此之间没有任何关系。它们的表示方式可能完全不同。对于您尝试执行的演员表,A&lt;Base&gt; 必须是 A&lt;Derived&gt; 的基类,但显然不是这样。

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

【讨论】:

    【解决方案2】:

    我不确定您的设计目标,但为了让讨论更直观,这里有一个类型擦除的典型示例:一个单独的类 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 的接口类型,并且您可以使用结构上满足要求的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);
    

    注意事项:

    • 类型擦除提供 ad-hoc 多态性
    • 对客户端类型的要求是结构性的,与继承无关。想想“鸭子打字”或“概念”。
    • 类型擦除设计公开功能,而不是层次相关性。
    • 如果您要求 Impl 可复制(转换为对 T 的要求),您可以使 Foo 可复制。
    • 我们使用原始new;没有分配器支持。事实证明,类型擦除分配器支持非常具有挑战性,尤其是在类型擦除状态应该是可复制的情况下。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-04-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-27
      • 2013-01-23
      相关资源
      最近更新 更多