web-dev-qa-db-fra.com

Comment appeler :: std :: make_shared sur une classe avec uniquement des constructeurs protégés ou privés?

J'ai ce code qui ne fonctionne pas, mais je pense que l'intention est claire:

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();
}

Mais je reçois cette erreur quand je la compile:

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

Ce message indique en gros qu'une méthode aléatoire dans la pile d'instanciation de modèles de ::std::make_shared ne peut pas accéder au constructeur car il est protégé.

Mais je veux vraiment utiliser les deux ::std::make_shared et empêcher quiconque de créer un objet de cette classe qui n'est pas pointé par un ::std::shared_ptr. Est-il possible d'accomplir cela?

142
Omnifarious

Cette réponse est probablement meilleure, et celle que j'accepterai probablement. Mais j'ai aussi mis au point une méthode plus laide, mais qui permet de tout laisser en ligne et ne nécessite pas de classe dérivée:

#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);
}

Edit 2017-01-06: J'ai changé cela pour préciser que cette idée est clairement et simplement extensible aux constructeurs qui prennent des arguments parce que d'autres personnes ont fourni des réponses dans ce sens et semblaient confuses à ce sujet.

97
Omnifarious

En regardant les exigences pour std::make_shared dans 20.7.2.2.6 shared_ptr creation [util.smartptr.shared.create], paragraphe 1:

Requiert: L'expression ::new (pv) T(std::forward<Args>(args)...), où pv a le type void* et désigne un stockage approprié pour contenir un objet de type T, doit être correctement formée. A doit être un allocateur (17.6.3.5). Le constructeur de copie et le destructeur de A ne doivent pas lancer d'exceptions.

Puisque l'exigence est inconditionnellement spécifiée en termes d'expression et que des éléments tels que la portée ne sont pas pris en compte, je pense que des astuces telles que l'amitié ont raison.

Une solution simple consiste à dériver de A. Cela ne nécessite pas de rendre A une interface ou même un type polymorphe.

// 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>();
}
64
Luc Danton

Peut-être la solution la plus simple. Sur la base de answer de Mohit Aron et en incorporant la suggestion de 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() {}  
};
56
Mark Tolley

Voici une solution intéressante pour cela:

#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>();
}
23
Mohit Aron

Que dis-tu de ça?

static std::shared_ptr<A> create()
{
    std::shared_ptr<A> pA(new A());
    return pA;
}
10
Sean
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_;
};
10
alpha

Comme je n’aimais pas les réponses déjà fournies, j’ai décidé de chercher une solution qui n’est pas aussi générique que les réponses précédentes mais qui me plait mieux (tm). Rétrospectivement, il n’est pas beaucoup plus agréable que celui fourni par Omnifarius, mais d’autres personnes pourraient l’aimer aussi :)

Ceci n’a pas été inventé par moi, mais c’est l’idée de Jonathan Wakely (développeur de GCC). 

Malheureusement, cela ne fonctionne pas avec tous les compilateurs car il repose sur une petite modification de l'implémentation std :: allocate_shared. Mais cette modification est maintenant une proposition de mise à jour pour les bibliothèques standard. Elle pourrait donc être prise en charge par tous les compilateurs à l’avenir. Cela fonctionne sur GCC 4.7.

La demande de modification du groupe de travail de la bibliothèque standard C++ est la suivante: http://lwg.github.com/issues/lwg-active.html#2070

Le correctif GCC avec un exemple d'utilisation est ici: http://old.nabble.com/Re%3A--v3--Implement-pointer_traits-and-allocator_traits-p31723738.html

La solution fonctionne sur l'idée d'utiliser std :: allocate_shared (au lieu de std :: make_shared) avec un allocateur personnalisé déclaré ami de la classe avec le constructeur privé.

L'exemple du PO ressemblerait à ceci:

#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;
}

Un exemple plus complexe basé sur l'utilitaire sur lequel je travaille. Avec cela, je ne pouvais pas utiliser la solution de Luc. Mais celui d'Omnifarius pourrait être adapté. Bien que, dans l'exemple précédent, tout le monde puisse créer un objet A à l'aide de MyAlloc dans celui-ci, il n'existe aucun moyen de créer A ou B en dehors de la méthode create ().

#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;
}
8
Zsolt Rizsányi

Je réalise que ce fil est plutôt ancien, mais j'ai trouvé une réponse qui n'exige pas d'héritage ni d'arguments supplémentaires du constructeur que je ne pourrais voir ailleurs. Ce n'est pas portable si:

#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());
}

J'ai testé sur Windows et Linux, il peut avoir besoin de peaufiner pour différentes plates-formes.

3
ashleysmithgpu

Idéalement, je pense que la solution parfaite nécessiterait des ajouts à la norme C++. Andrew Schepler propose ce qui suit:

(Allez ici pour le fil entier)

nous pouvons emprunter une idée à boost :: iterator_core_access. Je propose une nouvelle classe std::shared_ptr_access sans public ou membres protégés, et de le spécifier pour std :: make_shared (args ...) et std :: alloc_shared (a, args ...), le fichier expressions :: new (pv) T(forward(args)...) et ptr-> ~ T () doivent être bien formé dans le contexte de std :: shared_ptr_access.

Une implémentation de std :: shared_ptr_access pourrait ressembler à ceci:

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;
    };
}

Usage

Si/quand ce qui précède est ajouté à la norme, nous ferions simplement:

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;
};

Si cela vous semble également un ajout important à la norme, n'hésitez pas à ajouter vos 2 centimes au groupe Google lié à l'isocpp.

3
Boris

Si vous souhaitez également activer un constructeur prenant des arguments, cela peut aider un peu.

#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");
}
2
demo

[Modifier] J'ai lu le fil mentionné ci-dessus dans une proposition std::shared_ptr_access<> normalisée. Une réponse indiquant un correctif à std::allocate_shared<> et un exemple de son utilisation figurait dans la réponse. Je l'ai adapté à un modèle d'usine ci-dessous et l'ai testé sous gcc C++ 11/14/17. Cela fonctionne aussi avec std::enable_shared_from_this<>, il serait donc évidemment préférable à ma solution originale dans cette réponse. C'est ici...

#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";
}

[Orig] J'ai trouvé une solution en utilisant le constructeur d'alias de pointeur partagé. Il permet à la fois au ctor et au dtor d'être privés, ainsi qu'à l'utilisation du spécificateur 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);
}

Notez que l'approche ci-dessus ne fonctionne pas bien avec std::enable_shared_from_this<> car le std::shared_ptr<> initial est destiné au wrapper et non au type lui-même. Nous pouvons résoudre ce problème avec une classe équivalente compatible avec l'usine ...

#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";
}

Enfin, quelqu'un a dit que Clang s'était plaint du fait que Factory :: Type était une personne privée lorsqu'elle était utilisée comme un ami. Rendez-le public si c'est le cas. L'exposer ne fait pas de mal.

2
user1715587

Il existe un problème plus complexe et intéressant qui se produit lorsque deux classes strictement liées, A et B, fonctionnent ensemble.

Disons que A est la "classe de maître" et B son "esclave". Si vous voulez limiter l'instanciation de B uniquement à A, vous devez rendre le constructeur de B privé, et l'ami B à A comme ceci

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

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

Malheureusement, appeler std::make_shared<B>() à partir d'une méthode de A amènera le compilateur à se plaindre du fait que B::B() soit privé.

Ma solution consiste à créer une classe publique Pass (tout comme nullptr_t) dans B qui a un constructeur privé et est amie avec A et rend public le constructeur de B et ajouter Pass à ses arguments, comme ceci.

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);
  }
};
1
keebus

J'avais le même problème, mais aucune des réponses existantes n'était vraiment satisfaisante car je devais passer des arguments au constructeur protégé. De plus, je dois le faire pour plusieurs classes, chacune prenant des arguments différents.

À cet effet, et en me basant sur plusieurs des réponses existantes qui utilisent toutes des méthodes similaires, je présente cette petite pépite:

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 )... );
}
0
Matthew

La racine du problème est que si la fonction ou la classe que vous avez amie passe des appels de niveau inférieur à votre constructeur, elles doivent également être traitées. std :: make_shared n'est pas la fonction qui appelle votre constructeur, ce qui en fait ne fait aucune différence.

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 appelle votre constructeur, il doit donc être un ami. Comme c'est un peu obscur, j'utilise une macro

#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;

Ensuite, votre déclaration de classe semble assez simple. Vous pouvez créer une seule macro pour déclarer le ptr et la classe si vous préférez.

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

C’est en fait un problème important… .. Pour créer un code portable maintenable, vous devez masquer autant que possible l’implémentation.

typedef std::shared_ptr<A> APtr;

cache un peu votre façon de manipuler votre pointeur intelligent, vous devez être sûr d'utiliser votre typedef. Mais si vous devez toujours en créer un en utilisant make_shared, cela ira à l'encontre du but recherché.

L'exemple ci-dessus force le code utilisant votre classe à utiliser votre constructeur de pointeur intelligent, ce qui signifie que si vous passez à une nouvelle version du pointeur intelligent, vous modifiez votre déclaration de classe et vous avez une chance décente d'être terminé. NE supposez PAS que votre prochain patron ou projet utilisera stl, boost, etc., pour le changer un jour.

En faisant cela depuis presque 30 ans, j’ai payé un lourd tribut en temps, en douleur et en effets secondaires pour réparer cela quand il s’est mal passé il ya quelques années.

0
brtip