O deletador de shared_ptr é armazenado na memória alocada pelo alocador personalizado?

22

Digamos que eu tenha um shared_ptralocador personalizado e um deleter personalizado.

Não consigo encontrar nada no padrão que fale sobre onde o deletador deve ser armazenado: não diz que o alocador personalizado será usado para a memória do deletador e não diz que não será.

Isso não é especificado ou estou perdendo alguma coisa?

Raças de leveza em órbita
fonte

Respostas:

11

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

Efeitos: constrói um objeto shared_ptr que possui o objeto pe deleter d. O segundo e o quarto construtores devem usar uma cópia de a para alocar memória para uso interno.

O segundo e o quarto construtores têm estes protótipos:

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

No rascunho mais recente, util.smartptr.shared.const / 10 é equivalente para nosso propósito:

Efeitos: constrói um objeto shared_ptr que possui o objeto pe deleter d. Quando T não é um tipo de matriz, o primeiro e o segundo construtores ativam shared_from_this com p. O segundo e o quarto construtores devem usar uma cópia de a para alocar memória para uso interno. Se uma exceção for lançada, d (p) será chamado.

Portanto, o alocador é usado se houver necessidade de alocá-lo na memória alocada. Com base no padrão atual e nos relatórios de defeitos relevantes, a alocação não é obrigatória, mas assumida pelo comitê.

  • Embora a interface de shared_ptrpermita uma implementação em que nunca haja um bloco de controle e tudo shared_ptre weak_ptrseja colocada em uma lista vinculada, não existe tal implementação na prática. Além disso, o texto foi modificado assumindo, por exemplo, que ele use_counté compartilhado.

  • O deleter é necessário para mover apenas construtível. Portanto, não é possível ter várias cópias no shared_ptr.

Pode-se imaginar uma implementação que coloque o deleter em um projeto especialmente projetado shared_ptre o mova quando o especial shared_ptrfor excluído. Embora a implementação pareça estar em conformidade, também é estranha, especialmente porque um bloco de controle pode ser necessário para a contagem de uso (talvez seja possível, mas ainda mais estranho, fazer a mesma coisa com a contagem de uso).

DRs relevantes que encontrei: 545 , 575 , 2434 (que reconhecem que todas as implementações estão usando um bloco de controle e parecem implicar que restrições de multi-threading o exigem de alguma forma), 2802 (que exige que o deleter se mova apenas construtível e, portanto, impeça a implementação onde o deleter é copiado entre vários shared_ptr).

AProgrammer
fonte
2
"alocar memória para uso interno" E se a implementação não alocar memória para uso interno, para começar? Pode usar um membro.
LF
11
@LF Não pode, a interface não permite isso.
AProgrammer 19/11/19
Teoricamente, ele ainda pode usar algum tipo de "otimização de pequeno deleter", certo?
LF
O estranho é que não consigo encontrar nada sobre o uso do mesmo alocador (cópia de a) para desalocar essa memória. O que implicaria algum armazenamento dessa cópia de a. Não há informações sobre isso em [util.smartptr.shared.dest].
Daniel Langr
11
@DanielsaysreinstateMonica, gostaria de saber se em util.smartptr.shared / 1: "O modelo de classe shared_ptr armazena um ponteiro, geralmente obtido por meio de novo. Shared_ptr implementa semântica de propriedade compartilhada; o último proprietário restante do ponteiro é responsável por destruir o objeto, ou liberando os recursos associados ao ponteiro armazenado ". a liberação dos recursos associados ao ponteiro armazenado não se destina a isso. Mas o bloco de controle também deve sobreviver até que o último ponteiro fraco seja excluído.
AProgrammer 19/11/19
4

De std :: shared_ptr , temos:

O bloco de controle é um objeto alocado dinamicamente que contém:

  • um ponteiro para o objeto gerenciado ou o próprio objeto gerenciado;
  • o deleter (tipo apagado);
  • o alocador (apagado);
  • o número de shared_ptrs que possuem o objeto gerenciado;
  • o número de fraco_ptrs que se referem ao objeto gerenciado.

E de std :: assignate_shared , obtemos:

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

Constrói um objeto do tipo T e o agrupa em um std :: shared_ptr [...] para usar uma alocação para o bloco de controle do ponteiro compartilhado e o objeto T.

Portanto, parece que std :: assignate_shared deve alocar o deletercom o seu Alloc.

EDIT: E a partir de n4810§20.11.3.6 Criação [util.smartptr.shared.create]

1 Os requisitos comuns que se aplicam a todos make_shared, allocate_shared, make_shared_default_init, e allocate_shared_default_initsobrecargas, a menos que especificado de outra forma, estão descritos abaixo.

[...]

7 Observações: (7.1) - As implementações devem executar não mais que uma alocação de memória. [Nota: Isso fornece eficiência equivalente a um ponteiro inteligente intrusivo. - end note]

[Ênfase toda minha]

Portanto, o padrão está dizendo que std::allocate_shared deve ser usado Allocpara o bloco de controle.

Paul Evans
fonte
11
Sinto muito por cppreference não é um texto normativo. É um ótimo recurso, mas não necessariamente para perguntas de juristas .
StoryTeller - Unslander Monica 19/11/19
@ StoryTeller-UnslanderMonica Concordo totalmente - examinou o padrão mais recente e não conseguiu encontrar nada, então foi com a cppreference.
Paul Evans
n4810Resposta encontrada e atualizada.
Paul Evans
11
No entanto, isso está falando make_shared, não dos próprios construtores. Ainda assim, posso usar um membro para pequenos deleters.
LF
3

Eu acredito que isso não é especificado.

Aqui está a especificação dos construtores relevantes: [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);

Efeitos: constrói um shared_­ptrobjeto que possui o objeto pe o deleter d. Quando Tnão é um tipo de matriz, o primeiro e o segundo construtores são ativados shared_­from_­thiscom p. O segundo e o quarto construtores devem usar uma cópia apara alocar memória para uso interno . Se uma exceção é lançada, d(p)é chamada.

Agora, minha interpretação é que, quando a implementação precisa de memória para uso interno, ela o faz usando a. Isso não significa que a implementação precise usar essa memória para colocar tudo. Por exemplo, suponha que exista essa implementação estranha:

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

Esta implementação "usa uma cópia apara alocar memória para uso interno"? Sim. Ele nunca aloca memória, exceto usando a. Há muitos problemas com essa implementação ingênua, mas digamos que ela mude para o uso de alocadores, exceto no caso mais simples, no qual o shared_ptrarquivo é construído diretamente a partir de um ponteiro e nunca é copiado ou movido ou referenciado de outra forma e não há outras complicações. A questão é que, simplesmente porque não conseguimos imaginar uma implementação válida, por si só não prova que ela não possa existir teoricamente. Não estou dizendo que essa implementação possa realmente ser encontrada no mundo real, apenas que o padrão não parece proibi-lo ativamente.

LF
fonte
OMI shared_ptrpara tipos pequenos aloca memória na pilha. E, portanto, não atende aos requisitos padrão
bartop
11
@ Bartop Ele não "aloca" nenhuma memória na pilha. _Deleter menor é uma parte incondicional da representação de um shared_ptr. Chamar um construtor neste espaço não significa alocar nada. Caso contrário, mesmo segurando um ponteiro para o bloco de controle conta como "alocar memória", certo? :-)
LF
Mas o deleter não precisa ser copiado, então como isso funcionaria?
Nicol Bolas
@NicolBolas Umm ... Use std::move(__d)e volte para allocatequando a cópia for necessária.
LF