Le deleter d'un shared_ptr est-il stocké en mémoire alloué par l'allocateur personnalisé?

22

Disons que j'ai un shared_ptravec un allocateur personnalisé et un suppresseur personnalisé.

Je ne trouve rien dans la norme qui parle de l'endroit où le suppresseur doit être stocké: il ne dit pas que l'allocateur personnalisé sera utilisé pour la mémoire du suppresseur, et il ne dit pas qu'il ne le sera pas .

Est-ce non spécifié ou ai-je juste oublié quelque chose?

Courses de légèreté en orbite
la source

Réponses:

11

util.smartptr.shared.const / 9 en C ++ 11:

Effets: construit un objet shared_ptr qui possède l'objet p et le deleter d. Les deuxième et quatrième constructeurs doivent utiliser une copie de a pour allouer de la mémoire à un usage interne.

Les deuxième et quatrième constructeurs ont ces prototypes:

template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template<class D, class A> shared_ptr(nullptr_t p, D d, A a);

Dans la dernière version, util.smartptr.shared.const / 10 est équivalent à notre objectif:

Effets: construit un objet shared_ptr qui possède l'objet p et le deleter d. Lorsque T n'est pas un type de tableau, les premier et deuxième constructeurs activent shared_from_this avec p. Les deuxième et quatrième constructeurs doivent utiliser une copie de a pour allouer de la mémoire à un usage interne. Si une exception est levée, d (p) est appelé.

L'allocateur est donc utilisé s'il est nécessaire de l'allouer dans la mémoire allouée. Sur la base de la norme actuelle et des rapports de défauts pertinents, l'allocation n'est pas obligatoire mais assumée par le comité.

  • Bien que l'interface de shared_ptrautorise une implémentation où il n'y a jamais de bloc de contrôle et tout shared_ptret weak_ptrsoit placé dans une liste chaînée, il n'y a pas une telle implémentation dans la pratique. De plus, le libellé a été modifié en supposant, par exemple, que le use_countest partagé.

  • Le deleter est nécessaire pour déplacer uniquement constructible. Ainsi, il n'est pas possible d'avoir plusieurs exemplaires dans le shared_ptr.

On peut imaginer une implémentation qui place le délétère dans un cadre spécialement conçu shared_ptret le déplace lorsqu'il shared_ptrest supprimé. Bien que l'implémentation semble conforme, elle est également étrange, d'autant plus qu'un bloc de contrôle peut être nécessaire pour le compte d'utilisation (il est peut-être possible mais encore plus étrange de faire la même chose avec le compte d'utilisation).

DR pertinents que j'ai trouvés: 545 , 575 , 2434 (qui reconnaissent que toutes les implémentations utilisent un bloc de contrôle et semblent impliquer que les contraintes multithread le mandatent quelque peu), 2802 (qui exige que le suppresseur ne se déplace que constructible et empêche ainsi l'implémentation lorsque le deleter est copié entre plusieurs shared_ptr).

AProgrammer
la source
2
"pour allouer de la mémoire pour un usage interne" Que faire si l'implémentation ne va pas allouer de la mémoire pour un usage interne pour commencer? Il peut utiliser un membre.
LF
1
@LF Ce n'est pas possible, l'interface ne le permet pas.
Programmeur
Théoriquement, il peut toujours utiliser une sorte d '"optimisation de petit deleter", non?
LF
Ce qui est bizarre, c'est que je ne trouve rien sur l'utilisation du même allocateur (copie de a) pour désallouer cette mémoire. Ce qui impliquerait un certain stockage de cette copie de a. Il n'y a aucune information à ce sujet dans [util.smartptr.shared.dest].
Daniel Langr
1
@DanielsaysreinstateMonica, je me demande si dans util.smartptr.shared / 1: "Le modèle de classe shared_ptr stocke un pointeur, généralement obtenu via new. Shared_ptr implémente la sémantique de la propriété partagée; le dernier propriétaire restant du pointeur est responsable de la destruction de l'objet, ou libérer autrement les ressources associées au pointeur stocké. " la libération des ressources associées au pointeur stocké n'est pas prévue pour cela. Mais le bloc de contrôle devrait également survivre jusqu'à ce que le dernier pointeur faible soit supprimé.
Programmeur
4

De std :: shared_ptr, nous avons:

Le bloc de contrôle est un objet alloué dynamiquement qui contient:

  • soit un pointeur vers l'objet géré ou l'objet géré lui-même;
  • le délétère (effacé);
  • l'allocateur (effacé);
  • le nombre de shared_ptrs qui possèdent l'objet géré;
  • le nombre de faibles_ptr qui se réfèrent à l'objet géré.

Et de std :: allocate_shared nous obtenons:

template< class T, class Alloc, class... Args >
shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );

Construit un objet de type T et l'enveloppe dans un std :: shared_ptr [...] afin d'utiliser une allocation à la fois pour le bloc de contrôle du pointeur partagé et l'objet T.

Il semble donc que std :: allocate_shared devrait allouer le deleteravec votre Alloc.

EDIT: Et à partir du n4810§ 20.11.3.6 Création [util.smartptr.shared.create]

1 Les exigences communes applicables à tous make_shared, allocate_shared, make_shared_default_init, et allocate_shared_default_initsurcharges, sauf indication contraire, sont décrits ci - dessous.

[...]

7 Remarques: (7.1) - Les implémentations ne doivent pas effectuer plus d'une allocation de mémoire. [Remarque: Cela fournit une efficacité équivalente à un pointeur intelligent intrusif. —Fin note]

[Souligne tout le mien]

Donc, la norme dit que cela std::allocate_shared devrait être utilisé Allocpour le bloc de contrôle.

Paul Evans
la source
1
Je suis désolé par cppreference n'est pas un texte normatif. C'est une excellente ressource, mais pas nécessairement pour les questions de langue-avocat .
StoryTeller - Unslander Monica
@ StoryTeller-UnslanderMonica Tout à fait d'accord - j'ai regardé la dernière norme et je n'ai rien trouvé, donc je suis allé avec cppreference.
Paul Evans
n4810Réponse trouvée et mise à jour.
Paul Evans
1
Cependant, cela parle make_shared, pas des constructeurs eux-mêmes. Pourtant, je peux utiliser un membre pour les petits délétères.
LF
3

Je crois que cela n'est pas spécifié.

Voici la spécification des constructeurs concernés: [util.smartptr.shared.const] / 10

template<class Y, class D> shared_ptr(Y* p, D d);
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

Effets: construit un shared_­ptrobjet propriétaire de l'objet pet du suppresseur d. Lorsque Tn'est pas un type de tableau, les premier et deuxième constructeurs activent shared_­from_­thisavec p. Les deuxième et quatrième constructeurs doivent utiliser une copie de apour allouer de la mémoire à un usage interne . Si une exception est levée, d(p)est appelée.

Maintenant, mon interprétation est que lorsque l'implémentation a besoin de mémoire pour un usage interne, elle le fait en utilisant a. Cela ne signifie pas que l'implémentation doit utiliser cette mémoire pour tout placer. Par exemple, supposons qu'il y ait cette implémentation étrange:

template <typename T>
class shared_ptr : /* ... */ {
    // ...
    std::aligned_storage<16> _Small_deleter;
    // ...
public:
    // ...
    template <class _D, class _A>
    shared_ptr(nullptr_t, _D __d, _A __a) // for example
        : _Allocator_base{__a}
    {
        if constexpr (sizeof(_D) <= 16)
            _Construct_at(&_Small_deleter, std::move(__d));
        else
            // use 'a' to allocate storage for the deleter
    }
// ...
};

Cette implémentation "utilise-t-elle une copie de apour allouer de la mémoire à un usage interne"? Oui. Il n'alloue jamais de mémoire sauf en utilisant a. Il y a beaucoup de problèmes avec cette implémentation naïve, mais disons qu'elle passe à l'utilisation d'allocateurs dans tous les cas sauf le plus simple dans lequel le shared_ptrest construit directement à partir d'un pointeur et n'est jamais copié ou déplacé ou autrement référencé et il n'y a pas d'autres complications. Le fait est que ce n'est pas parce que nous n'imaginons pas qu'une implémentation valide prouve qu'elle ne peut théoriquement pas exister. Je ne dis pas qu'une telle implémentation se trouve réellement dans le monde réel, mais simplement que la norme ne semble pas l'interdire activement.

LF
la source
IMO votre shared_ptrpour les petits types alloue de la mémoire sur la pile. Et ne répond donc pas aux exigences standard
bartop
1
@bartop Il n'alloue pas de mémoire sur la pile. _Smaller_deleter fait inconditionnellement partie de la représentation d'un shared_ptr. Appeler un constructeur sur cet espace ne signifie rien allouer. Sinon, même maintenir un pointeur sur le bloc de contrôle compte comme «allouer de la mémoire», non? :-)
LF
Mais le suppresseur n'est pas obligatoirement copiable, alors comment cela fonctionnerait-il?
Nicol Bolas
@NicolBolas Umm ... Utilisez std::move(__d), et revenez à allocatequand une copie est requise.
LF