如何在一个只有保护或私有构造函数的类上调用 ::std::make_shared 函数?

246

我有这段代码不起作用,但我认为意图很清楚:

testmakeshared.cpp

#include <memory>

class A {
 public:
   static ::std::shared_ptr<A> create() {
      return ::std::make_shared<A>();
   }

 protected:
   A() {}
   A(const A &) = delete;
   const A &operator =(const A &) = delete;
};

::std::shared_ptr<A> foo()
{
   return A::create();
}

但是当我编译它时,我会得到这个错误:

g++ -std=c++0x -march=native -mtune=native -O3 -Wall testmakeshared.cpp
In file included from /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:52:0,
                 from /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/memory:86,
                 from testmakeshared.cpp:1:
testmakeshared.cpp: In constructor ‘std::_Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp>::_Sp_counted_ptr_inplace(_Alloc) [with _Tp = A, _Alloc = std::allocator<A>, __gnu_cxx::_Lock_policy _Lp = (__gnu_cxx::_Lock_policy)2u]’:
/usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr_base.h:518:8:   instantiated from ‘std::__shared_count<_Lp>::__shared_count(std::_Sp_make_shared_tag, _Tp*, const _Alloc&, _Args&& ...) [with _Tp = A, _Alloc = std::allocator<A>, _Args = {}, __gnu_cxx::_Lock_policy _Lp = (__gnu_cxx::_Lock_policy)2u]’
/usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr_base.h:986:35:   instantiated from ‘std::__shared_ptr<_Tp, _Lp>::__shared_ptr(std::_Sp_make_shared_tag, const _Alloc&, _Args&& ...) [with _Alloc = std::allocator<A>, _Args = {}, _Tp = A, __gnu_cxx::_Lock_policy _Lp = (__gnu_cxx::_Lock_policy)2u]’
/usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:313:64:   instantiated from ‘std::shared_ptr<_Tp>::shared_ptr(std::_Sp_make_shared_tag, const _Alloc&, _Args&& ...) [with _Alloc = std::allocator<A>, _Args = {}, _Tp = A]’
/usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:531:39:   instantiated from ‘std::shared_ptr<_Tp> std::allocate_shared(const _Alloc&, _Args&& ...) [with _Tp = A, _Alloc = std::allocator<A>, _Args = {}]’
/usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:547:42:   instantiated from ‘std::shared_ptr<_Tp1> std::make_shared(_Args&& ...) [with _Tp = A, _Args = {}]’
testmakeshared.cpp:6:40:   instantiated from here
testmakeshared.cpp:10:8: error: ‘A::A()’ is protected
/usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr_base.h:400:2: error: within this context

Compilation exited abnormally with code 1 at Tue Nov 15 07:32:58

这条消息基本上是在说模板实例化堆栈中来自::std::make_shared的某个随机方法无法访问构造函数,因为它是受保护的。

但我真的想同时使用::std::make_shared和防止任何人创建一个没有由::std::shared_ptr指向的该类对象。有什么办法可以做到这一点吗?


你可以将需要构造函数的深层函数标记为友元,但这样做不具备可移植性。 - Daniel
@Dani:是的,拥有一个便携式解决方案会很不错。但那应该可行。 - Omnifarious
1
为什么不使用return std::shared_ptr<A>(new A()),而是使用return std::make_shared<A>() - Vassilis
1
因为make_shared仅为对象和引用计数执行1次分配。 - Trass3r
@Omnifarious,您认为这个解决方案怎么样?它是否符合您的需求?如果我漏掉了什么,请告诉我。 - John
@John:这是Vassilis提出的建议,如果你去掉"enable_shared_from_this"这部分的话。 - undefined
19个回答

4
基于CRTP的解决方案可以对多个类进行分解,易于启用,并适用于带有参数的构造函数。它要求构造函数为受保护状态(不是私有)。使用方式与enable_shared_from_this类似。它没有破坏protected关键字的缺点,即使用::make_unique的类将需要成为友元。灵感来自Mark Tolley的答案
实现:
template <typename ClassWithProtectedCtor>
class enable_protected_make_unique
{
protected: // important, if public then equivalent to having the constructor public which is what we want to avoid!
    template <typename... Args>
    static std::unique_ptr<ClassWithProtectedCtor> make_unique(Args &&... args)
    {
        class make_unique_enabler : public ClassWithProtectedCtor
        {
        public:
            // it's from this line that comes the need to have the constructor protected, not private:
            make_unique_enabler(Args &&... args) : ClassWithProtectedCtor(std::forward<Args>(args)...) {}
        };
        return std::make_unique<make_unique_enabler>(std::forward<Args>(args)...);
    }
};

使用示例:

class Factory;

class MyClassWithProtectedCtor : public enable_protected_make_unique<MyClassWithProtectedCtor>
{
friend Factory;
private:
    MyClassWithProtectedCtor(int a, double c) {};
}

class Factory
{
    std::unique_ptr<MyClassWithProtectedCtor> CreateMyClassWithProtectedCtor(int a, double c)
    {
        return MyClassWithProtectedCtor::make_unique(a, c);
    }
}

你可以用shared替换unique,或在同一个“enabler”类中组合两者。
免责声明:我没有在生产代码中测试过,可能存在缺点(例如当提到类型MyClassWithProtectedCtor时出现更长的错误消息)。

2
你可以通过在 make_unique_enabler 构造函数中添加 static_assert(!std::is_constructible_v<ClassWithProtectedCtor, Args...>); 来使这个解决方案更加完善。如果 ClassWithProtectedCtor 的构造函数是公共的,它将会失败。 - Roman
这种方法的一个缺点是它阻止了一个类被声明为final - Florian Winter

4

我知道这个帖子有点老了,但是我找到了一个答案,它不需要继承或者在构造函数中添加额外的参数,而且其他地方好像都没有提到过。不过它不是可移植的:

#include <memory>

#if defined(__cplusplus) && __cplusplus >= 201103L
#define ALLOW_MAKE_SHARED(x) friend void __gnu_cxx::new_allocator<test>::construct<test>(test*);
#elif defined(_WIN32) || defined(WIN32)
#if defined(_MSC_VER) && _MSC_VER >= 1800
#define ALLOW_MAKE_SHARED(x) friend class std::_Ref_count_obj;
#else
#error msc version does not suport c++11
#endif
#else
#error implement for platform
#endif

class test {
    test() {}
    ALLOW_MAKE_SHARED(test);
public:
    static std::shared_ptr<test> create() { return std::make_shared<test>(); }

};
int main() {
    std::shared_ptr<test> t(test::create());
}

我已经在Windows和Linux上进行了测试,但可能需要针对不同的平台进行微调。


1
我倾向于将其减一,因为它缺乏可移植性。其他答案(特别是“关键类”答案)相当优雅,而不可移植的答案非常丑陋。我想不出你会使用不可移植的答案的原因。它并不更快或者什么的。 - Omnifarious
@Omnifarious 的确不具备可移植性,我不建议使用,但我认为这实际上是语义上最正确的解决方案。在 我的答案 中,我链接了一个提案,该提案建议将 std::shared_ptr_access 添加到标准中,可以看作是以简单和可移植的方式允许执行上述操作。 - Boris Dalstein
现在你可以通过将std::construct_at作为好友来实现类似的功能。然而,这两种方法都会泄漏,因为客户端可以公开访问std::construct_at__gnu_cxx::new_allocator - Thomas Eding

4
如果您也想启用一个带参数的构造函数,这可能会有所帮助。
#include <memory>
#include <utility>

template<typename S>
struct enable_make : public S
{
    template<typename... T>
    enable_make(T&&... t)
        : S(std::forward<T>(t)...)
    {
    }
};

class foo
{
public:
    static std::unique_ptr<foo> create(std::unique_ptr<int> u, char const* s)
    {
        return std::make_unique<enable_make<foo>>(std::move(u), s);
    }
protected:
    foo(std::unique_ptr<int> u, char const* s)
    {
    }
};

void test()
{
    auto fp = foo::create(std::make_unique<int>(3), "asdf");
}

3

[编辑] 我已经阅读了上述关于标准化std::shared_ptr_access<>提案的帖子。其中有一篇回复指出了对std::allocate_shared<>的修复,并给出了其使用示例。我将其改编为下面的工厂模板,并在gcc C++11/14/17下进行了测试。它也可以与std::enable_shared_from_this<>一起使用,因此显然比我在这个答案中提供的原始解决方案更可取。以下是代码...

#include <iostream>
#include <memory>

class Factory final {
public:
    template<typename T, typename... A>
    static std::shared_ptr<T> make_shared(A&&... args) {
        return std::allocate_shared<T>(Alloc<T>(), std::forward<A>(args)...);
    }
private:
    template<typename T>
    struct Alloc : std::allocator<T> {
        template<typename U, typename... A>
        void construct(U* ptr, A&&... args) {
            new(ptr) U(std::forward<A>(args)...);
        }
        template<typename U>
        void destroy(U* ptr) {
            ptr->~U();
        }
    };  
};

class X final : public std::enable_shared_from_this<X> {
    friend class Factory;
private:
    X()      { std::cout << "X() addr=" << this << "\n"; }
    X(int i) { std::cout << "X(int) addr=" << this << " i=" << i << "\n"; }
    ~X()     { std::cout << "~X()\n"; }
};

int main() {
    auto p1 = Factory::make_shared<X>(42);
    auto p2 = p1->shared_from_this();
    std::cout << "p1=" << p1 << "\n"
              << "p2=" << p2 << "\n"
              << "count=" << p1.use_count() << "\n";
}

[Trans] 我找到了一种使用共享指针别名构造函数的解决方案。它使得构造和析构函数都可以被设为私有,并且还可以使用final修饰符。

#include <iostream>
#include <memory>

class Factory final {
public:
    template<typename T, typename... A>
    static std::shared_ptr<T> make_shared(A&&... args) {
        auto ptr = std::make_shared<Type<T>>(std::forward<A>(args)...);
        return std::shared_ptr<T>(ptr, &ptr->type);
    }
private:
    template<typename T>
    struct Type final {
        template<typename... A>
        Type(A&&... args) : type(std::forward<A>(args)...) { std::cout << "Type(...) addr=" << this << "\n"; }
        ~Type() { std::cout << "~Type()\n"; }
        T type;
    };
};

class X final {
    friend struct Factory::Type<X>;  // factory access
private:
    X()      { std::cout << "X() addr=" << this << "\n"; }
    X(int i) { std::cout << "X(...) addr=" << this << " i=" << i << "\n"; }
    ~X()     { std::cout << "~X()\n"; }
};

int main() {
    auto ptr1 = Factory::make_shared<X>();
    auto ptr2 = Factory::make_shared<X>(42);
}

注意,上述方法与std::enable_shared_from_this<>不兼容,因为初始的std::shared_ptr<>是针对包装器而不是类型本身。我们可以通过一个相当的类来解决这个问题,并且该类与工厂相兼容...
#include <iostream>
#include <memory>

template<typename T>
class EnableShared {
    friend class Factory;  // factory access
public:
    std::shared_ptr<T> shared_from_this() { return weak.lock(); }
protected:
    EnableShared() = default;
    virtual ~EnableShared() = default;
    EnableShared<T>& operator=(const EnableShared<T>&) { return *this; }  // no slicing
private:
    std::weak_ptr<T> weak;
};

class Factory final {
public:
    template<typename T, typename... A>
    static std::shared_ptr<T> make_shared(A&&... args) {
        auto ptr = std::make_shared<Type<T>>(std::forward<A>(args)...);
        auto alt = std::shared_ptr<T>(ptr, &ptr->type);
        assign(std::is_base_of<EnableShared<T>, T>(), alt);
        return alt;
    }
private:
    template<typename T>
    struct Type final {
        template<typename... A>
        Type(A&&... args) : type(std::forward<A>(args)...) { std::cout << "Type(...) addr=" << this << "\n"; }
        ~Type() { std::cout << "~Type()\n"; }
        T type;
    };
    template<typename T>
    static void assign(std::true_type, const std::shared_ptr<T>& ptr) {
        ptr->weak = ptr;
    }
    template<typename T>
    static void assign(std::false_type, const std::shared_ptr<T>&) {}
};

class X final : public EnableShared<X> {
    friend struct Factory::Type<X>;  // factory access
private:
    X()      { std::cout << "X() addr=" << this << "\n"; }
    X(int i) { std::cout << "X(...) addr=" << this << " i=" << i << "\n"; }
    ~X()     { std::cout << "~X()\n"; }
};

int main() {
    auto ptr1 = Factory::make_shared<X>();
    auto ptr2 = ptr1->shared_from_this();
    std::cout << "ptr1=" << ptr1.get() << "\nptr2=" << ptr2.get() << "\n";
}

最后,有人说当Factory::Type作为友元使用时,clang会抱怨它是私有的,因此如果情况如此,请将其公开。 公开它不会造成任何损害。

1
问题的根源在于,如果您的友元函数或类对您的构造函数进行了较低级别的调用,则它们也必须成为友元。std::make_shared并不是实际调用您的构造函数的函数,因此将其设置为友元没有任何影响。
class A;
typedef std::shared_ptr<A> APtr;
class A
{
    template<class T>
    friend class std::_Ref_count_obj;
public:
    APtr create()
    {
        return std::make_shared<A>();
    }
private:
    A()
    {}
};

std::_Ref_count_obj实际上会调用你的构造函数,所以它需要成为友元。因为这有点模糊,所以我使用了一个宏。

#define SHARED_PTR_DECL(T) \
class T; \
typedef std::shared_ptr<T> ##T##Ptr;

#define FRIEND_STD_MAKE_SHARED \
template<class T> \
friend class std::_Ref_count_obj;

那么你的类声明看起来相当简单。如果你愿意,你可以为声明指针和类制作一个单一的宏。

SHARED_PTR_DECL(B);
class B
{
    FRIEND_STD_MAKE_SHARED
public:
    BPtr create()
    {
        return std::make_shared<B>();
    }
private:
    B()
    {}
};

这实际上是一个重要的问题。为了编写易于维护和移植的代码,您需要尽可能隐藏实现细节。
typedef std::shared_ptr<A> APtr;

隐藏了你如何处理智能指针的细节,你必须确保使用你的typedef。但是,如果你总是使用make_shared来创建智能指针,那就失去了其意义。
上面的例子强制使用你的类的代码使用你的智能指针构造函数,这意味着如果你切换到新的智能指针类型,你需要改变你的类声明,而且有很大的可能性会出现问题。不要假设你的下一个老板或项目将使用stl、boost等,计划将来更改它。
我已经做了近30年这样的工作,为修复之前错误的代码所付出的时间、痛苦和副作用代价巨大。

3
std::_Ref_count_obj是一个实现细节。这意味着,虽然这个解决方案在你的平台上现在可能有效,但它可能对其他人无效,并且随着你的编译器更新或者仅仅是改变编译标志,它可能随时停止工作。 - François Andrieux

0
如果可能的话,您可以创建一个公共移动构造函数,如下所示:
class A {
 public:
   A(A&&) = default;
   static ::std::shared_ptr<A> create() {
      return ::std::make_shared<A>(std::move<A>(A{}));
   }

 protected:
   A() {}
   A(const A &) = delete;
   const A &operator =(const A &) = delete;
};

::std::shared_ptr<A> foo()
{
   return A::create();
}

这只能起作用,因为移动构造函数是公共的,并且该类型是可移动的,这可能是不希望或不可能的。 - Florian Winter

-1
这个解决方案怎么样,它很简单,也能实现目标。 以下是代码片段:
#include <iostream>
#include <memory>
 
class Foo : public std::enable_shared_from_this<Foo> {
private:     //the user should not construct an instance through the constructor below.                    
    Foo(int num):num_(num) { std::cout << "Foo::Foo\n"; }
public:
    Foo(const Foo&) = delete;
    Foo(Foo&&) = default;
    Foo& operator=(const Foo&) = delete;
    Foo& operator=(Foo&&) = default;

public:
    ~Foo() { std::cout << "Foo::~Foo\n"; } 

    int DoSth(){std::cout << "hello world" << std::endl; return 0;}

    std::shared_ptr<Foo> getPtr() { return shared_from_this();}

    static std::shared_ptr<Foo> Create() {
        Foo* foo = new Foo(5);
        return std::shared_ptr<Foo>(foo);
    }

private:
    int num_;

};

int main()
{
    auto sp = Foo::Create();
    sp->DoSth();

    Foo& foo = *sp.get();
    auto sp1 = foo.getPtr();

    std::cout << sp.use_count() << std::endl;
}

1
这种解决方案的缺点在于它相当于将构造函数公开:非友元类可以通过Create()创建新实例,那么为什么不在这种情况下直接将构造函数公开呢?另一种解决方案则没有这个问题。 - Gabriel Devillers
1
我想使用make_shared而不是shared_from_this的原因是它避免了为引用计数分配内存。此外,引用计数最终被存储在所引用的对象附近。 - Omnifarious

-1
class A  {
public:
 static std::shared_ptr<A> getA() {
    return std::shared_ptr<A> a(new A());
 }

private:
  A() {}
};

由于std::make_shared无法调用我们手动创建实例的A类中的私有构造函数。因此,我们使用new手动创建一个A对象的实例。然后在其构造函数中将shared_ptr设置为指向新创建的A对象。您不必担心内存泄漏,因为shared_ptr会自动为您删除A


虽然这可能会提供一个很好的答案,但鼓励留下一些描述,说明它为什么回答了这个问题。谢谢。 - user438383
1
这并没有使用make_shared,因此不是一个答案。使用make_shared的特定原因是它仅为对象和引用计数执行一次分配。它还将对引用计数的访问局限于紧邻对象内存的内存中,提高了处理器缓存的效率,并使引用和对象极有可能在同一页虚拟内存上。 - Omnifarious
这种方法只为对象和引用计数分配一次内存。共享指针类似于new将东西存储在堆上。我认为你做出了错误的假设。使用new分配内存的内存位置没有比使用make_shared更低效的理由。 - Max Gómez
@Omnifarious 是正确的。表达式 new A() 无法知道在堆上分配的对象将与 shared_ptr 一起使用,因此它无法为引用计数器、互斥锁和其他 shared_ptr 实现的部分分配内存。它只在堆上分配了一个 A,返回一个原始指针,然后 shared_ptr 构造函数必须再次在堆上分配引用计数器的内存。 - Florian Winter

-3
#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<A>
{
private:
    A(){}
    explicit A(int a):m_a(a){}
public:
    template <typename... Args>
    static std::shared_ptr<A> create(Args &&... args)
    {
        class make_shared_enabler : public A
        {
        public:
            make_shared_enabler(Args &&... args):A(std::forward<Args>(args)...){}
        };
        return std::make_shared<make_shared_enabler>(std::forward<Args>(args)...);
    }

    int val() const
    {
        return m_a;
    }
private:
    int m_a=0;
};

int main(int, char **)
{
    std::shared_ptr<A> a0=A::create();
    std::shared_ptr<A> a1=A::create(10);
    std::cout << a0->val() << " " << a1->val() << std::endl;
    return 0;
}

这只是这个答案的复制:https://dev59.com/mmsz5IYBdhLWcg3wCDl2#27832765 - Omnifarious

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接