如何在一个只有保护或私有构造函数的类上调用 ::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个回答

132

这个答案可能更好,我可能会接受它。但我也想出了一种更丑陋的方法,但它仍然可以让所有内容都保持一致,并且不需要派生类:

#include <memory>
#include <string>

class A {
 protected:
   struct this_is_private;

 public:
   explicit A(const this_is_private &) {}
   A(const this_is_private &, ::std::string, int) {}

   template <typename... T>
   static ::std::shared_ptr<A> create(T &&...args) {
      return ::std::make_shared<A>(this_is_private{0},
                                   ::std::forward<T>(args)...);
   }

 protected:
   struct this_is_private {
       explicit this_is_private(int) {}
   };

   A(const A &) = delete;
   const A &operator =(const A &) = delete;
};

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

::std::shared_ptr<A> bar()
{
   return A::create("George", 5);
}

::std::shared_ptr<A> errors()
{
   ::std::shared_ptr<A> retval;

   // Each of these assignments to retval properly generates errors.
   retval = A::create("George");
   retval = new A(A::this_is_private{0});
   return ::std::move(retval);
}

编辑于2017年1月6日: 我修改了这个内容,以明确这个想法可以轻松地扩展到需要参数的构造函数,因为其他人提供的答案沿着这些方向并似乎对此感到困惑。


17
实际上,我是那些仅用作“键”的毫无意义结构的狂热粉丝。我更喜欢这种做法而不是Luc的解决方案,但这可能是我的反对继承的偏见。 - Matthieu M.
3
@Berkus:那么将其改为protected而不是private。我所指的“它”是this_is_private类,如果这样的话可能应该将其重命名。在我的代码中,我通常将其称为constructor_access - dalle
4
Stefan,如果你给this_is_private一个私有构造函数,你就可以将A类设为友元。似乎可以堵住漏洞。 - Steven Kramer
2
这个问题已经有足够的备选答案了(尽管其中许多似乎是相同主题的重复变体)。我在其他领域使用PassKey习惯用语,并发现这里的两个得票最高的答案实际上结合得非常好。我添加了一个私有的make_shared_enabler类,它派生自我的主类,但是主类的构造函数需要一个make_shared_enabler类的PassKey。这使我可以通过将其保护来隐藏公共API中的构造函数,同时由于PassKey而禁止(额外)子类化。 - monkey0506
2
@Omnifarious:this_is_private 构造函数中的 int 有什么意义?为什么不直接使用 explicit this_is_private() =default; - plexando
显示剩余14条评论

97
观察20.7.2.2.6 shared_ptr creation [util.smartptr.shared.create]中std::make_shared的要求,第1段如下:

要求:表达式::new (pv) T(std::forward<Args>(args)...)必须是良好形式,其中pv类型为void*且指向适合容纳类型T对象的存储器。 A必须是分配器(17.6.3.5),而且A的复制构造函数和析构函数不能抛出异常。

由于该要求在条件上是无条件规定的,并且不考虑范围等因素,所以像友元这样的技巧都不可用。
一个简单的解决方案是从A派生。这不需要将A变成接口,甚至不需要将其变成多态类型。
// interface in header
std::shared_ptr<A> make_a();

// implementation in source
namespace {

struct concrete_A: public A {};

} // namespace

std::shared_ptr<A>
make_a()
{
    return std::make_shared<concrete_A>();
}

1
哦,那是一个非常聪明的答案,可能比我想到的另一个答案更好。 - Omnifarious
1
不过,有一个问题,shared_ptr 是否会删除 A 而不是 concrete_A,这样会不会引起问题? - Omnifarious
8
噢,这是因为shared_ptr在实例化时存储了一个删除器,如果你使用的是make_shared,那么删除器绝对必须使用正确的类型。 - Omnifarious
1
@LucDanton 这个问题不仅仅涉及接口,正如标题所示,他也在询问一个私有构造函数。而且,这也是我看到这个问题的原因。一些旧代码中有着类似于马基雅维利类的东西,其中包含一个私有构造函数和一个返回原始指针的创建方法,而我正在尝试将它们转换为智能指针。 - zahir
3
我喜欢这种方法(并且我自己也使用它), 但你确实需要一个虚析构函数。它可以很好地扩展到带参数的构造函数(只需提供一个传递构造函数)。如果你使用_protected_而不是_private_,你可以使其对头文件的用户完全不可见。 - Joe Steele
显示剩余9条评论

94
可能是最简单的解决方案。基于 Mohit Aron 的先前 answer,并融合了 dlf 的建议。
#include <memory>

class A
{
public:
    static std::shared_ptr<A> create()
    {
        struct make_shared_enabler : public A {};

        return std::make_shared<make_shared_enabler>();
    }

private:
    A() {}  
};

10
如果A有非默认构造函数,您还需要公开它们:struct make_shared_enabler:public A {template <typename ... Args> make_shared_enabler(Args &&... args):A(std :: forward <Args>(args)...){}};。这将使A的所有私有构造函数作为make_shared_enabler构造函数可见。在这里使用构造函数继承特性(using A::A;)似乎并没有帮助,因为构造函数仍然是私有的。 - anton_rh
3
你无法为内部类添加模板参数。请参见这里 - bobbel
3
嗯...看起来你是对的。在我的情况中,struct不是本地的,而是一个私有的struct: class A { ... private: struct A_shared_enabler; }; class A::A_shared_enabler : public A { ... }。在这里可以看到http://cpp.sh/65qbr。 - anton_rh
这个很好用。有没有可能将其作为可继承属性,这样就不必多次重复此模式了?特别是那些暴露非默认构造函数的版本对我来说非常有趣。默认版本将“仅仅”需要一些语法结构,将A替换为继承该类的任何类。我不知道是否存在这样的东西,但如果了解到它存在,我也不会感到惊讶... - Kjeld Schmidt
1
@KjeldSchmidt 请查看我的答案,其中包含这个模式的分解,代价是使用受保护的构造函数而不是私有构造函数。 - Gabriel Devillers

46

这里有一个简洁的解决方案:

#include <memory>

class A {
   public:
     static shared_ptr<A> Create();

   private:
     A() {}

     struct MakeSharedEnabler;   
 };

struct A::MakeSharedEnabler : public A {
    MakeSharedEnabler() : A() {
    }
};

shared_ptr<A> A::Create() {
    return make_shared<MakeSharedEnabler>();
}

6
我喜欢这个。通过在A :: Create()内部定义“MakeSharedEnabler”,可以使其变得更简单。 - dlf
棒极了的想法,Mohit。它对我帮助很大。 - Jnana
@dlf 不,你不能对非模板类这样做,就像例子中的 A 类一样会不完整。 - Soup Endless
@dlf 我误读了,以为你是指在 A 中定义 MakeSharedEnabler。但是你也不能在 Create 中定义 MakeSharedEnabler,因为它不是封闭类 A,这会导致编译错误。你应该在 Create 之前定义 MakeSharedEnabler 才能使其正常工作。此外,你也不能在类 A 中定义 Create,因为你需要 MakeSharedEnabler 的定义才能将其转换为 A。你可以从 Create 返回 shared_ptr< MakeSharedEnabler>,但这对代码的客户端来说有点丑陋,并且会破坏封装性。 - Soup Endless
对于 template <class T> class A;,你可以在 A 内定义 MakeSharedEnablerCreate - Soup Endless
这应该是被接受的答案,如此简单。 - Conrad Jones

17
struct A {
public:
  template<typename ...Arg> std::shared_ptr<A> static create(Arg&&...arg) {
    struct EnableMakeShared : public A {
      EnableMakeShared(Arg&&...arg) :A(std::forward<Arg>(arg)...) {}
    };
    return std::make_shared<EnableMakeShared>(std::forward<Arg>(arg)...);
  }
  void dump() const {
    std::cout << a_ << std::endl;
  }
private:
  A(int a) : a_(a) {}
  A(int i, int j) : a_(i + j) {}
  A(std::string const& a) : a_(a.size()) {}
  int a_;
};

这与Luc Danton的答案基本相同,虽然将其转换为本地类是一个不错的选择。附带代码说明可以使这个答案更好。 - user743382
通常情况下,我想将这样的小函数写在头文件中而不是cc文件中。其次,在实践中,我使用一个宏,看起来像 #define SharedPtrCreate(T) template<typename ...Arg>..... - alpha
好的回答。我甚至会将其放入一个名为IMPLEMENT_CREATE_SHARED(ClassName)的宏中。 - ivan.ukr
其中一个问题是现在更加混乱的是哪些参数实际上被create方法所接受,任何错误都会导致更加混乱的模板消息。这只是在实际的create方法中定义helper类所做出的一个权衡。 - Matias Grioni

17

这个怎么样?

static std::shared_ptr<A> create()
{
    std::shared_ptr<A> pA(new A());
    return pA;
}

23
这很好用。但是,::std::make_shared的功能不仅仅是创建一个指向某个对象的shared_ptr,它还会分配引用计数和对象,使它们靠近彼此。我非常、非常想要使用::std::make_shared - Omnifarious
删除的赋值和复制运算符禁止此操作。 - Daniel
10
这确实是最直接的方法,虽然并不完全符合问题的要求。make_shared有一些很好的特性,我尽可能地使用它,但在这种情况下,似乎运行时性能优势并不超过实际需要使用它所需的额外代码复杂度和仪式感。如果你真的需要make_shared的性能,请放手去用,但不要忽视只使用shared_ptr构造函数的简单性。 - Kevin
1
要小心内存泄漏... 参见此问题 https://dev59.com/wWUq5IYBdhLWcg3wC8Hx#14837300 - dgmz
@Omnifarious 这个在哪里有文档记录? - smac89
2
@smac89 - https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared#Notes - 请查看注释部分,第一个项目。这是标准中建议的实现。 - Omnifarious

13

我认为理想的解决方案需要对C++标准进行补充。Andrew Schepler提出了以下建议:

(整个讨论可以在这里找到。)

我们可以借鉴boost::iterator_core_access的思想。我建议引入一个新类std::shared_ptr_access,该类没有公共或受保护成员,并指定对于std::make_shared(args...)和std::alloc_shared(a, args...),在std::shared_ptr_access的上下文中,表达式::new(pv) T(forward(args)...)和ptr->~T()必须是良好定义的。

std::shared_ptr_access的实现可能如下所示:

namespace std {
    class shared_ptr_access
    {
        template <typename _T, typename ... _Args>
        static _T* __construct(void* __pv, _Args&& ... __args)
        { return ::new(__pv) _T(forward<_Args>(__args)...); }

        template <typename _T>
        static void __destroy(_T* __ptr) { __ptr->~_T(); }

        template <typename _T, typename _A>
        friend class __shared_ptr_storage;
    };
}

使用方法

如果/当以上内容被添加到标准中时,我们只需执行以下操作:

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

 protected:
   friend class std::shared_ptr_access;
   A() {}
   A(const A &) = delete;
   const A &operator =(const A &) = delete;
};

如果您认为这也是标准的重要补充,欢迎在链接到的isocpp谷歌小组中发表您的意见。

9

我遇到了同样的问题,但是现有的答案都无法令我满意,因为我需要传递参数到受保护的构造函数中。此外,我需要针对多个不同参数的类执行此操作。

基于几个现有答案并使用相似的方法,为此我提供了这个小技巧:

template < typename Object, typename... Args >
inline std::shared_ptr< Object >
protected_make_shared( Args&&... args )
{
  struct helper : public Object
  {
    helper( Args&&... args )
      : Object{ std::forward< Args >( args )... }
    {}
  };

  return std::make_shared< helper >( std::forward< Args >( args )... );
}

2
这很酷,因为它不会使类变得混乱。谢谢。如果我将protected_make_shared作为全局命名空间中的公共函数,则必须像这样将其添加为类的友元:template<typename Object, typename... Args> friend shared_ptr<Object> protected_make_shared(Args&&... args);。但是,如果只有一个类使用它,您可以将其作为(公共)类成员添加为static inline - some guy
2
这种解决方案的缺点是protected_make_shared可以从任何地方调用,包括不与Object友好的类。 - Gabriel Devillers

9
由于我不喜欢已提供的答案,我决定继续搜索,并找到了一个解决方案,它不像之前的答案那样通用,但我更喜欢它。回顾一下,它并不比Omnifarius提供的解决方案好多少,但可能还有其他人也喜欢它:)
这不是我发明的,而是Jonathan Wakely(GCC开发人员)的想法。
不幸的是,它不能与所有编译器一起使用,因为它依赖于std::allocate_shared实现中的小改动。但是,这个改动现在是标准库的拟议更新,所以它可能会得到所有编译器的支持。它适用于GCC 4.7。
C++标准库工作组的更改请求在此处: http://lwg.github.com/issues/lwg-active.html#2070 带有示例用法的GCC补丁在此处: http://old.nabble.com/Re%3A--v3--Implement-pointer_traits-and-allocator_traits-p31723738.html 该解决方案的思路是使用std::allocate_shared(而不是std::make_shared)和一个自定义分配器,该分配器被声明为类的私有构造函数的友元。
OP的示例将如下所示:
#include <memory>

template<typename Private>
struct MyAlloc : std::allocator<Private>
{
    void construct(void* p) { ::new(p) Private(); }
};

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

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

        friend struct MyAlloc<A>;
};

int main() {
    auto p = A::create();
    return 0;
}
以下是与我正在处理的实用程序相关的更复杂的示例。使用这个,我不能使用Luc的解决方案。但Omnifarius的解决方案可以被改编。请注意,在上一个示例中,每个人都可以使用MyAlloc创建A对象,但在这个示例中,除了create()方法之外,没有其他方法可以创建A或B对象。
#include <memory>

template<typename T>
class safe_enable_shared_from_this : public std::enable_shared_from_this<T>
{
    public:
    template<typename... _Args>
        static ::std::shared_ptr<T> create(_Args&&... p_args) {
            return ::std::allocate_shared<T>(Alloc(), std::forward<_Args>(p_args)...);
        }

    protected:
    struct Alloc : std::allocator<T>
    {  
        template<typename _Up, typename... _Args>
        void construct(_Up* __p, _Args&&... __args)
        { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    };
    safe_enable_shared_from_this(const safe_enable_shared_from_this&) = delete;
    safe_enable_shared_from_this& operator=(const safe_enable_shared_from_this&) = delete;
};

class A : public safe_enable_shared_from_this<A> {
    private:
        A() {}
        friend struct safe_enable_shared_from_this<A>::Alloc;
};

class B : public safe_enable_shared_from_this<B> {
    private:
        B(int v) {}
        friend struct safe_enable_shared_from_this<B>::Alloc;
};

int main() {
    auto a = A::create();
    auto b = B::create(5);
    return 0;
}

5

当你有两个紧密相关的类A和B一起工作时,会出现一个更棘手而有趣的问题。

假设A是“主类”,B是其“从属类”。如果你想将B的实例化限制为仅在A中进行,你可以将B的构造函数设置为私有,并像这样将B设为A的友元

class B
{
public:
    // B your methods...

private:
    B();
    friend class A;
};

很不幸,从A的方法中调用std::make_shared<B>()会使编译器抱怨B::B()是私有的。

我的解决方案是在B内创建一个公共的Pass虚类(就像nullptr_t一样),它有私有构造函数并且是A的友元,并将B的构造函数设为公共的,并添加Pass到其参数中,如下所示。

class B
{
public:
  class Pass
  {
    Pass() {}
    friend class A;
  };

  B(Pass, int someArgument)
  {
  }
};

class A
{
public:
  A()
  {
    // This is valid
    auto ptr = std::make_shared<B>(B::Pass(), 42);
  }
};

class C
{
public:
  C()
  {
    // This is not
    auto ptr = std::make_shared<B>(B::Pass(), 42);
  }
};

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