【问题标题】:How is it possible that std::shared_ptr<Derived> casts to std::shared_ptr<Base> with no compiler errors?std::shared_ptr<Derived> 怎么可能转换为 std::shared_ptr<Base> 而没有编译器错误?
【发布时间】:2021-05-14 03:37:30
【问题描述】:

我正在尝试实现自定义智能指针。所以我有这样的事情:

// Base class for every object
class Base {
public:
    int n_holders {};
};

class Derived : public Base {};

// Custom shared pointer
template<class T>
class Sptr {
public:
    T* obj;
    Sptr(T* obj_) : obj{obj_} {}
};

void SomeFunc(Sptr<Base> obj) {}

void SomeFunc2(std::shared_ptr<Base> obj) {}

int main()
{
    auto a = Sptr<Base>(new Base());
    SomeFunc(a); // OK

    auto b = Sptr<Derived>(new Derived());
    SomeFunc(b); // Error. No matching function call to SomeFunc

    auto c = std::shared_ptr<Base>(new Base());
    SomeFunc2(c); // OK

    auto d = std::shared_ptr<Derived>(new Derived());
    SomeFunc2(d); // OK !!!
}

我的问题是,如果使用std::shared_ptr 没有错误,为什么编译器不能自动从Sptr&lt;Derived&gt; 转换为Sptr&lt;Base&gt;?。如何让它成为可能?

【问题讨论】:

    标签: c++ templates casting derived-class


    【解决方案1】:

    std::shared_ptr 有一个构造函数(来自cppreference):

    template< class Y >
    shared_ptr( const shared_ptr<Y>& r ) noexcept;  (9)     
    

    这个重载...

    构造一个shared_ptr,它共享被管理对象的所有权 由 r.如果 r 不管理任何对象,this 也不管理任何对象。这 如果 Y 是,则模板重载不参与重载决议 不能隐式转换为(C++17 前)兼容(因为 C++17) T*。

    因此,从某种意义上说,棘手的部分不是转换共享指针,而是在指针类型不可隐式转换时阻止它。您可以使用 SFINAE 来实现。

    这是一个玩具示例,仅当 T1 继承自 T2 时才启用从 Bar&lt;T1&gt;Bar&lt;T2&gt; 的转换(但不是相反):

    #include <type_traits>
    
    template <typename T1>
    struct Bar {
        Bar() {}
    
        template <typename T2, typename std::enable_if_t<std::is_base_of_v<T1,T2>,int> = 0>
        Bar(Bar<T2>){} 
    
    };
    
    struct Foo {};
    struct Derived : Foo {};
    
    int main(){
        Bar<Derived> d;
        Bar<Foo> b;
        //d = b; // eror
        b = d;  // OK
    }
    

    Live Demo

    您可能希望它更通用,例如共享指针,以允许在 T2* 可以转换为 T1* 时进行此类转换,而不仅仅是在它们相互继承时(参见 std::is_convertible,我不得不承认,我不太了解 C++17 带来的变化,所以我只能猜测:在这种情况下可能是 std::is_layout_compatible)。因此,要模仿 C++17 之前的智能指针,您可以使用:

        template <typename T2, typename std::enable_if_t<std::is_convertible_v<T2*,T1*>,int> = 0>
        Bar(Bar<T2>){} 
    

    启用所有T2 的转换,其中T2* 可以转换为T1*

    【讨论】:

    • 或者如果可以使用 C++20 约束,template &lt;typename T2&gt; requires std::is_base_of_v&lt;T1, T2&gt; Bar(Bar&lt;T2&gt;); 或类似 is_convertible_v
    • 首选std::enable_if_t&lt;cond, int&gt; = 0 而不是typename = std::enable_if_t&lt;cond&gt;。前者允许重载,并且在一般情况下不能被劫持(构造函数是一个例外:-))
    • @largest_prime_is_463035818。所以我应该实现复制构造函数template &lt;typename T2, typename = typename std::enable_if&lt;std::is_base_of_v&lt;T1,T2&gt;&gt;::type&gt; Sptr(const Sptr&lt;T2&gt;&amp; otherSptr),对吧?
    • @ЕвгенийПавлов 不,这不是我写的。您应该实现一个转换构造函数,该构造函数只参与要启用的转换的重载解析。派生和基数之间的转换只是一种情况,我的代码只是为了粗略了解如何做到这一点。将条件替换为您想要的条件
    • @largest_prime_is_463035818 好的。我知道了。因此,如果我的所有类都派生自某个包含计数器的基类 Obj,那么我应该写 template &lt;typename T2, typename std::enable_if_t&lt;std::is_base_of_v&lt;Obj,T2&gt;,int&gt; = 0&gt;,对吧?
    猜你喜欢
    • 1970-01-01
    • 2017-08-06
    • 1970-01-01
    • 2015-07-23
    • 2010-11-24
    • 1970-01-01
    • 1970-01-01
    • 2021-06-21
    • 2021-02-04
    相关资源
    最近更新 更多