【问题标题】:Can upcasting a shared_ptr<T> to a shared_ptr<void> lead to undefined behaviour?将 shared_ptr<T> 向上转换为 shared_ptr<void> 会导致未定义的行为吗?
【发布时间】:2017-01-30 03:33:36
【问题描述】:

共享指针非常聪明。他们会记住最初构造时使用的类型,以便正确删除它们。以此为例:

struct A { virtual void test() = 0; };
struct B : A { void test() override {} };

void someFunc() {
    std::shared_ptr<A> ptr1;

    ptr1 = std::make_shared<B>();

    // Here at the end of the scope, B is deleted correctly
}

但是,void 指针似乎存在一个问题:要使 void 指针的向下转换有效,必须将其向下转换为最初向上转换的类型。

例如:

void* myB = new B;

// Okay, well defined
doStuff(static_cast<B*>(myB));

// uh oh, not good!
// For the same instance of a child object, a pointer to the base and
// a pointer to the child can be differrent.
doStuff(static_cast<A*>(myB));

使用std::shared_ptr,当您使用std::make_shared 时,删除器必须类似于此函数:[](B* ptr){ delete ptr; }。由于指针(在第一个示例中)在指向 A 的指针中持有 B 实例并正确删除它,因此它必须以某种方式向下转换它。

我的问题是:下面的代码 sn-p 调用未定义的行为吗?

void someFunc() {
    {
        std::shared_ptr<void> ptr = std::make_shared<B>();

        // Deleting the pointer seems okay to me,
        // the shared_ptr knows that a B was originally allocated with a B and
        // will send the void pointer to the deleter that's delete a B.
    }

    std::shared_ptr<void> vptr;

    {
        std::shared_ptr<A> ptr = std::make_shared<B>();

        // ptr is pointing to the base, which can be 
        // different address than the pointer to the child.

        // assigning the pointer to the base to the void pointer.
        // according to my current knowledge of void pointers, 
        // any future use of the pointer must cast it to a A* or end up in UB.
        vptr = ptr;
    }

    // is the pointer deleted correctly or it tries to
    // cast the void pointer to a B pointer without downcasting to a A* first?
    // Or is it well defined and std::shared_ptr uses some dark magic unknown to me?
}

【问题讨论】:

    标签: c++ pointers shared-ptr void-pointers


    【解决方案1】:

    代码是正确的。

    std::shared_ptr 内部保存了真正的指针和真正的删除器,因为它们在构造函数中,所以无论你如何向下转换它,只要向下转换有效,删除器就是正确的。

    shared_ptr 实际上并不包含指向对象的指针,而是指向包含实际对象、引用计数器和删除器的中间结构的指针。如果你转换shared_ptr 并不重要,中间结构不会改变。它无法更改,因为您的 vptrptr 尽管类型不同,但共享引用计数器(当然还有对象和删除器)。

    顺便说一句,该中间结构是make_shared 优化的原因:它将中间结构和对象本身分配在同一个内存块中,并避免了额外的分配。

    为了说明智能指针的作用,我编写了一个带有普通指针的程序,由于您的问题,该程序会崩溃(使用 GCC 6.2.1):

    #include <memory>
    #include <iostream>
    
    struct A
    {
        int a;
        A() :a(1) {}
        ~A()
        {
            std::cout << "~A " << a << std::endl;
        }
    };
    
    struct X
    {
        int x;
        X() :x(3) {}
        ~X()
        {
            std::cout << "~X " << x << std::endl;
        }
    };
    
    struct B : X, A
    {
        int b;
        B() : b(2) {}
        ~B()
        {
            std::cout << "~B " << b << std::endl;
        }
    };
    
    int main()
    {
        A* a = new B;
        void * v = a;
        delete (B*)v; //crash!
    
        return 0;    
    }
    

    实际上它打印了错误的整数值,这证明了 UB。

    ~B 0
    ~A 2
    ~X 1
    *** Error in `./a.out': free(): invalid pointer: 0x0000000001629c24 ***
    

    但是带有智能指针的版本可以正常工作:

    int main()
    {
        std::shared_ptr<void> vptr;
    
        {
            std::shared_ptr<A> ptr = std::make_shared<B>();
            vptr = ptr;
        }
        return 0;
    }
    

    按预期打印:

    ~B 2
    ~A 1
    ~X 3
    

    【讨论】:

    • 我将始终支持一个正确的答案,该答案传达了 shared_ptr 的设计完美。 +1 :)
    • @RichardHodges 我更喜欢std::unique_ptr,因为它们在开销方面是免费的;)。至于答案,干得好@rodrigo,它几乎涵盖了所有内容。
    • shared_ptr 确实持有一个指针,即从shared_ptr::get() 返回的指针。但这与传递给删除器的指针不同。
    • 感谢@BenVoigt 的澄清。我猜想删除器和要删除的指针必须保存在控制块中,并且直接在共享指针中返回shared_ptr::get()。我说的对吗?
    • @GuillaumeRacicot:是的。因为可以有许多不同的 shared_ptr 实例指向对象的不同部分(所以 get() 为每个返回不同的值),但是如果所有这些部分共享一个共同的生命周期,那么所有这些 shared_ptr 实例必须共享控制块。
    【解决方案2】:

    shared_ptr 总是将原始指针传递给删除器,而不是通过vptr.get() 获得的指针。这不仅是为了使这种情况有效,而且对于在构造函数重载shared_ptr&lt;T&gt;::shared_ptr(const shared_ptr&lt;T&gt;&amp;, element_type*) 中体现的成员子对象和拥有对象的指针也是必要的。

    所以这是安全的。

    【讨论】:

      猜你喜欢
      • 2012-07-22
      • 2014-11-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-12
      • 2010-11-24
      • 2019-10-20
      • 2023-04-05
      相关资源
      最近更新 更多