56 votes

Comment fonctionne le deleter personnalisé de std :: unique_ptr?

Selon N3290 std::unique_ptr accepte un deleter argument dans son constructeur.

Cependant, je ne peux pas obtenir que cela fonctionne avec Visual C++ 10.0 ou MinGW g++ 4.4.1 dans Windows, ni avec g++ 4.6.1 dans Ubuntu.

J'ai donc peur que ma compréhension de celui-ci est incomplet ou erroné, je ne vois pas le point de deleter argument qui est apparemment pas pris en compte, quelqu'un peut-il fournir un exemple?

De préférence j'aimerais voir aussi comment ça fonctionne,

unique_ptr<Base> p = unique_ptr<Derived>( new Derived )

Peut-être avec quelques libellé de la norme pour sauvegarder l'exemple, c'est à dire, qu'avec ce compilateur que vous utilisez, il fait effectivement ce qu'il est sensé faire?


MODIFIER

: Certaines personnes ont commenté et a même voté, de la flamme-guerre, que cette question est impossible de répondre. Je n'ai pas modifié le texte original ci-dessus. Citant les trois derniers alinéas de l', j'ai été en demandant "un exemple de travail?" d'une coutume pour la deleter unique_ptr, et "de préférence" comment ça marche pour upcasts, et "peut-être avec une formulation de la norme".

Cela s'est avéré être si simple de répondre que la bonne réponse a été donnée après un très court laps de temps, et, curieusement, la voix que c'était impossible, et les revendications qu'il a été impossible de répondre, est venu après que la réponse a été donnée; ils sont venus après que la réponse a été sélectionné en tant que "solution".

En partie, je suis en train d'écrire ce montage pour faire réfléchir les gens un peu plus. Pour ne pas se comporter comme des idiots, le vote de fermer sans réponse une question qui est (1) c'est très simple et direct, et (2) a déjà été répondu, pour l'amour de Dieu.

EDIT 2: les autres partielle en raison de l'écriture au-dessus de la modifier, est désormais couvert par l'amendement de Howard réponse.

46voto

Benjamin Lindley Points 51005

Cela fonctionne pour moi dans MSVC10

 int x = 5;
auto del = [](int * p) { std::cout << "Deleting x, value is : " << *p; };
std::unique_ptr<int, decltype(del)> px(&x, del);
 

Et sur gcc 4.5, ici

Je vais passer à la norme, à moins que vous ne pensiez que cet exemple donne exactement ce que vous attendez de lui.

27voto

Pour compléter les réponses précédentes, il y a un moyen d'avoir un custom deleter, sans avoir à "polluer" le unique_ptr signature par le fait d'avoir un pointeur de fonction ou quelque chose d'équivalent dans comme ceci:

std::unique_ptr< MyType, myTypeDeleter > // not pretty

Ceci est possible en fournissant une spécialisation pour le std::default_delete modèle de classe, comme ceci:

namespace std
{
template<>
class default_delete< MyType >
{
public:
  void operator()(MyType *ptr)
  {
    delete ptr;
  }
};
}

Et maintenant, tous std::unique_ptr< MyType > qui "voit" cette spécialisation partielle sera supprimé avec elle. Juste être conscient qu'il pourrait ne pas être ce que vous voulez pour tous std::unique_ptr< MyType >, donc, choisi avec soin votre solution.

23voto

Howard Hinnant Points 59526

Remarque: la réponse est en dessous de la ligne de séparation.

Avec la permission de Howard I (Alf) suis modifiant cette réponse en montrant pourquoi le code a un Comportement Indéfini, et un don de séparateurs en béton par exemple.

Depuis cela invalide la plupart de l'original de la réponse, je suis en plaçant ce texte à l'avant, pour éviter que les gens ne lire que le début de la réponse originale à cette question et d'être induit en erreur. Cependant, enterré dans l'original était-ce important d'info:

L' sizeof unique_ptr est le même que pour auto_ptr et T* si vous êtes en défaut la deleter, ou spécifier un apatride deleter. Si vous spécifiez un “stateful” deleter (comme un pointeur de fonction), puis de la mémoire supplémentaire est pile-allouée pour stocker l'état de votre deleter.

Cela dit, la question est

  • comment créer un unique_ptr<T> personnalisé deleter, qui sera correctement détruire l'arbitre, même après l' unique_ptr<T> a été déplacé à un unique_ptr<BaseOfT>.

C'est une des principales raison de ma question, par exemple, il se rapporte à l'utilisation d' unique_ptr pour le PIMPL idiome.

Notons d'abord que l'adresse d'un objet dépend du type qu'il est consulté en tant que:

#include <iostream>
using namespace std;

struct Base { int x; };
struct Derived: Base { virtual ~Derived() {} };

int main()
{
    Derived*    p1  = new Derived;
    Base*       p2  = p1;

    cout << "As Derived at " << p1 << ", as Base at " << p2 << ".\n";
}

Les tests qu'avec Visual C++ 10.0 et MinGW g++ 4.4.1:

d:\dev\test\hh> cl changing_address.cpp /Fe"b"
changing_address.cpp

d:\dev\test\hh> b
Comme Dérivés à l'00721A50, comme Base à 00721A54.

d:\dev\test\hh> g++ changing_address.cpp

d:\dev\test\hh> un
Comme Dérivés à l'0x351728, comme Base à 0x35172c.

d:\dev\test\hh> _

Ce type d'adresse spécifique est parfois surprenant, même pour chevronné C++ professionnels.

Le changement d'adresse se produit parce que l'adresse Base objet doit être l'adresse du premier octet de l' Base sous-objet en Derived. Et les deux compilateurs utilisés placé au-dessus de l' Base sous-objet ailleurs que, dès le début de l' Derived objet. Apparemment, ils ont choisi de placer une vtable pointeur au début, suivie par l' Base sous-objet; de toute façon, avec l' Base sous-objet n'est pas au tout début de l' Derived de l'objet, et donc avec une adresse différente.

En raison de son changement d'adresse, lorsqu'un unique_ptr<Base> passe sa propriété pointeur (de type Base*) pour un deleter fonction prenant void* argument, la deleter fonction peut recevoir une autre adresse, un autre void* de la valeur, que pour un original Derived* la valeur du pointeur.

L'effet d'une fonte d' Derived* peut alors être différente de l'originale Derived* pointeur, et toute tentative d'utilisation de la visée de l'objet, comme par delete, puis a un Comportement Indéfini.

Par exemple, le code ci-dessous, à l'aide de la réponse originale à cette question apparemment simple deleter fonction de régime, les accidents:

#include <iostream>
#include <memory>       // std::unique_ptr
#include <utility>      // std::forward

struct Base { int x; };
struct Derived: Base { virtual ~Derived() {} };

template< class Type >
void deleteFuncImpl( void* p )
{
    delete static_cast< Type* >( p );
}

#if defined( USE_CPP11_ARGUMENT_FORWARDING )
template< class T, class ...Args >
std::unique_ptr< T, void(*)(void*) >
    make_ptr( Args&& ...args )
{
    return std::unique_ptr< T, void(*)(void*) >(
        new T( std::forward<Args>( args )... ),
        &deleteFuncImpl< T >
        );
}
#endif

template< class T >
std::unique_ptr< T, void(*)(void*) > make_ptr()
{
    return std::unique_ptr< T, void(*)(void*) >(
        new T(),
        &deleteFuncImpl< T >
        );
}

int main()
{
    std::unique_ptr< Base, void(*)(void*) >  p = make_ptr< Derived >();
}

Le crash:

d:\dev\test\hh> g++ hh_02.cpp -std=c++0x -o hh_02

d:\dev\test\hh> hh_02

d:\dev\test\hh> _

Windows 7 "stopped working" dialog

J'ai donné une solution simple dans mon commentaire comme réponse, cette réponse originale à cette question à partir de Howard était une réponse.


D'origine, y compris mise à jour:

A votre question contenait votre réponse, ou du code qui y au moins tenté pour votre réponse, je (ou quelqu'un d'autre) pourrait avoir offert le code ci-dessous comme beaucoup plus simple, plus claire et moins d'obfuscation alternative:

// Your Base and Derived
// ..

template< class Type >
void deleteFuncImpl( void* p )
{
    delete static_cast< Type* >( p );
}

int main()
{
    unique_ptr< Base, void(*)(void*) >  p( new Derived, deleteFuncImpl<Derived> );
    cout << p->message() << endl;
}

Base:<init>
Derived::<init>
Message from Derived!
Derived::<destroy>
Base::<destroy>

Parmi ceux qui connaissent unique_ptr, il est bien connu que l'on peut obtenir de nombreux avantages d'une dynamique deleter à l'aide d'un pointeur de fonction comme la statique de la deleter type, pas besoin que ce soit pour envelopper le pointeur de fonction, ni la deleter dans les classes auxiliaires.

Et notez que cette fonctionnalité est que vous payez seulement si vous l'utilisez. L' sizeof unique_ptr est le même que pour auto_ptr et T* si vous êtes en défaut la deleter, ou spécifier un apatride deleter. Si vous spécifiez un "stateful" deleter (comme un pointeur de fonction), puis de la mémoire supplémentaire est pile-allouée pour stocker l'état de votre deleter.

Naturellement, il convient de souligner que l' default_deletere, utilisé avec l' unique_ptr<Base> est apatrides (ajoute pas de frais généraux), si vous choisissez de suivre le temps-testé conception de rendre ~Base virtuel.

...
virtual ~Base() { cout << "Base::<destroy>" << endl; }
...

int main()
{
    unique_ptr< Base >  p( new Derived );
    cout << p->message() << endl;
    assert(sizeof(unique_ptr< Base >) == sizeof(Base*));
}

Base:<init>
Derived::<init>
Message from Derived!
Derived::<destroy>
Base::<destroy>

Le ci-dessus est vraiment le zéro-frais de solution depuis Base a déjà une fonction virtuelle (message) et donc de faire des ~Base() virtuelle ne pas ajouter d'importantes charges supplémentaires pour Base.

Une dynamique deleter (comme shared_ptr') a été proposé pour l' unique_ptr plusieurs fois au cours de normalisation. Et le tout en un unique propriétaire de pointeur intelligent avec une dynamique deleter ne avoir des cas d'utilisation, c'est trop pour les cas d'utilisation - unique_ptr a été conçu pour. L'un des principaux cas d'utilisation pour l' unique_ptr est sécuritaire de le remplacer auto_ptr. Si unique_ptr avaient une dynamique deleter, il aurait surcharge, au-dessus de auto_ptr, et n'a donc pas été capable de la remplacer entièrement auto_ptr. Et nous, par la suite, aurait été incapable de rendre caduque auto_ptr et de son cortège de risque d'erreur de l'utiliser dans le code générique (déplacement avec copie de la syntaxe).

Mise à jour

Il ya une certaine inquiétude de l'utilisation de void* dans la fonction qui fait la suppression. Un moyen de corriger cela est de rendre le paramètre Type*:

template< class Type >
void deleteFuncImpl( Type* p )
{
    delete p;
}

Cela ne fonctionne pas dans le cas des OP code parce que l' unique_ptr<Derived> est converti en unique_ptr<Base> et par la suite seront de passage à un Base* d'un Derived* à la destruction du temps, qui n'est pas le convertir implicitement.

Une autre solution serait de donner des deleteFuncImpl deux paramètres:

template<class Base, class Type >
void deleteFuncImpl( Base* p )
{
    delete static_cast< Type* >( p );
}

Mais c'est ridiculement maladroit, et pas vraiment mieux que l'original void* de la solution.

Enfin, on peut noter que l'on ne peut pas avoir un comportement indéfini si dans cette déclaration:

unique_ptr< Base, void(*)(void*) >  p( new Derived, deleteFuncImpl<Derived> );
                                           ^^^^^^^                 ^^^^^^^

les deux souligné les types sont les mêmes. La meilleure façon de faire respecter c'est de créer une usine de la fonction:

template <class T, class ...Args>
std::unique_ptr< T, void(*)(void*) >
make_ptr(Args&& ...args)
{
    return std::unique_ptr<T, void(*)(void*)>
                           (new T(std::forward<Args>(args)...),
                            deleteFuncImpl<T>);
}

qui peut être utilisée comme ceci:

std::unique_ptr< Base, void(*)(void*) >  p = make_ptr<Derived>();

Maintenant, même si deleteFuncImpl prend son paramètre par void*, nous savons a priori que le temps d'exécution type d' deleteFuncImpl<T> argument est un T*, ce qui élimine la possibilité d'un décalage entre le moment de l'exécution le type de l'objet, et le static_cast au sein de la deleter fonction.

Dans cette mise à jour de la solution, l' sizeof unique_ptr est deux mots. Je tiens à souligner une fois de plus, cependant, que la solution optimale, à la fois dans l'espace et de la clarté de codage, est de donner de l' Base un destructeur virtuel. Dans cette solution l' sizeof unique_ptr est un mot, le même que pour un pointeur brut.

La fonction de pointeur de-la deleter solution doit être réservée aux cas où une virtual ~Base() n'est pas significatif. Les exemples incluent l'utilisation de std::free et std::close que la deleter.

Enfin, sur la suggestion d'utiliser std::function<void(something-I'm-not-sure-what)>:

Un std::function<void(something-I'm-not-sure-what)> peut être "null". Et donc un pointeur de fonction. Toutefois unique_ptr est spécialement formulée pour faire accidentellement à l'attribution d'une fonction nulle pointeur peu probable, alors qu'il n'a pas de tels protocoles de sécurité en place pour protéger contre une valeur null std::function. Cette compile et se bloque au moment de l'exécution:

std::unique_ptr<int, std::function<void(void*)>> p(new int(3));

terminate called without an active exception
Abort trap: 6

Mais cette attrape l'erreur au moment de la compilation:

std::unique_ptr<int, void(*)(void*)> p(new int(3));

error: static_assert failed "unique_ptr constructed with null function pointer deleter"

En outre, std::function ajoute une charge sur un pointeur de fonction comme indiqué par Johannes Schaub dans les commentaires de cette réponse.

std::function non seulement ajoute de la vitesse et de la taille probable de frais généraux par rapport à un pointeur de fonction, mais dans ce cas est également plus sujettes à erreur.

11voto

Ma question a été assez bien déjà répondu.

Mais juste au cas où les gens se demandaient, j'ai eu la conviction erronée que l' unique_ptr<Derived> pourrait être déplacé à un unique_ptr<Base> et serait alors rappelez-vous la deleter pour l' Derived objet, c'est à dire, qu' Base n'aurait pas besoin d'avoir un destructeur virtuel. Que c'était faux. J'avais sélectionnez Kerrek SB commentaire comme "la réponse", sauf qu'on ne peut pas le faire pour un commentaire.

@Howard: le code ci-dessous illustre une façon de parvenir à ce que je croyais le coût d'une attribuée dynamiquement deleter fallait dire qu' unique_ptr pris en charge hors de la boîte:

#include <iostream>
#include <memory>           // std::unique_ptr
#include <functional>       // function
#include <utility>          // move
#include <string>
using namespace std;

class Base
{
public:
    Base() { cout << "Base:<init>" << endl; }
    ~Base() { cout << "Base::<destroy>" << endl; }
    virtual string message() const { return "Message from Base!"; }
};

class Derived
    : public Base
{
public:
    Derived() { cout << "Derived::<init>" << endl; }
    ~Derived() { cout << "Derived::<destroy>" << endl; }
    virtual string message() const { return "Message from Derived!"; }
};

class BoundDeleter
{
private:
    typedef void (*DeleteFunc)( void* p );

    DeleteFunc  deleteFunc_;
    void*       pObject_;

    template< class Type >
    static void deleteFuncImpl( void* p )
    {
        delete static_cast< Type* >( p );
    }

public:
    template< class Type >
    BoundDeleter( Type* pObject )
        : deleteFunc_( &deleteFuncImpl< Type > )
        , pObject_( pObject )
    {}

    BoundDeleter( BoundDeleter&& other )
        : deleteFunc_( move( other.deleteFunc_ ) )
        , pObject_( move( other.pObject_ ) )
    {}

    void operator() (void*) const
    {
        deleteFunc_( pObject_ );
    }
};

template< class Type >
class SafeCleanupUniquePtr
    : protected unique_ptr< Type, BoundDeleter >
{
public:
    typedef unique_ptr< Type, BoundDeleter >    Base;

    using Base::operator->;
    using Base::operator*;

    template< class ActualType >
    SafeCleanupUniquePtr( ActualType* p )
        : Base( p, BoundDeleter( p ) )
    {}

    template< class Other >
    SafeCleanupUniquePtr( SafeCleanupUniquePtr< Other >&& other )
        : Base( move( other ) )
    {}
};

int main()
{
    SafeCleanupUniquePtr< Base >  p( new Derived );
    cout << p->message() << endl;
}

Cheers,

6voto

Jagannath Points 2326

Cela marche. La destruction se passe correctement.

 class Base
{
    public:
     Base() { std::cout << "Base::Base\n"; }
     virtual ~Base() { std::cout << "Base::~Base\n"; }
};


class Derived : public Base
{
    public:
     Derived() { std::cout << "Derived::Derived\n"; }
     virtual ~Derived() { std::cout << "Derived::~Derived\n"; }
};

void Delete(const Base* bp)
{
    delete bp;
}

int main()
{
    std::unique_ptr<Base, void(*)(const Base*)> ptr = std::unique_ptr<Derived, void(*)(const Base*)>(new Derived(), Delete);
}
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X