Czy separator shared_ptr jest przechowywany w pamięci przydzielonej przez niestandardowy program przydzielający?

22

Powiedzmy, że mam shared_ptrniestandardowy alokator i niestandardowy usuwacz.

Nie mogę znaleźć w standardzie niczego, co mówi o tym, gdzie powinien być przechowywany usuwacz: nie mówi, że niestandardowy alokator zostanie użyty do pamięci usuwacza, i nie mówi, że nie będzie.

Czy jest to nieokreślone, czy coś mi brakuje?

Lekkość Wyścigi na orbicie
źródło

Odpowiedzi:

11

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

Efekty: Konstruuje obiekt shared_ptr, który jest właścicielem obiektu p i usuwacza d. Drugi i czwarty konstruktor używają kopii pamięci w celu alokacji pamięci do użytku wewnętrznego.

Drugi i czwarty konstruktor mają następujące prototypy:

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

W najnowszej wersji pliku util.smartptr.shared.const / 10 jest równoważny z naszym celem:

Efekty: Konstruuje obiekt shared_ptr, który jest właścicielem obiektu p i usuwacza d. Gdy T nie jest typem tablicy, pierwszy i drugi konstruktor umożliwiają share_from_this z p. Drugi i czwarty konstruktor używają kopii pamięci w celu alokacji pamięci do użytku wewnętrznego. Jeśli zgłoszony zostanie wyjątek, wywoływane jest d (p).

Dlatego alokator jest używany, jeśli zachodzi potrzeba alokacji go w przydzielonej pamięci. Na podstawie obecnego standardu i na podstawie odpowiednich raportów o wadach przydział nie jest obowiązkowy, ale zakłada komitet.

  • Chociaż interfejs shared_ptrpozwala implementacja tam, gdzie nigdy nie jest blok sterowania i wszystko shared_ptri weak_ptrsą umieszczane w połączonej listy, nie ma takiego wdrożenia w praktyce. Ponadto zmodyfikowano sformułowanie, zakładając na przykład, że use_countjest ono wspólne.

  • Deleter jest wymagany tylko do ruchu konstrukcyjnego. Dlatego nie jest możliwe posiadanie kilku kopii w shared_ptr.

Można sobie wyobrazić implementację, która umieszcza separator w specjalnie zaprojektowanym shared_ptri przenosi go, gdy jest shared_ptron usuwany. Chociaż implementacja wydaje się zgodna, jest to również dziwne, zwłaszcza, że ​​do liczenia użycia może być potrzebny blok kontrolny (być może jest to możliwe, ale jeszcze dziwniejsze, aby zrobić to samo z liczbą użytych).

Odpowiednie DR, które znalazłem: 545 , 575 , 2434 (które potwierdzają, że wszystkie implementacje używają bloku sterującego i wydają się sugerować, że ograniczenia wielowątkowe w pewien sposób tego wymagają), 2802 (co wymaga, aby deleter poruszał się konstruowalnie, a tym samym uniemożliwiał implementację w przypadku, gdy usuwacz jest kopiowany pomiędzy kilkoma shared_ptr.

AProgrammer
źródło
2
„przydzielić pamięć do użytku wewnętrznego” Co się stanie, jeśli implementacja nie zamierza przydzielić pamięci do użytku wewnętrznego na początek? Może używać członka.
LF
1
@LF Nie może, interfejs na to nie pozwala.
AProgrammer
Teoretycznie nadal można zastosować pewnego rodzaju „optymalizację małych detektorów”, prawda?
LF,
Dziwne jest to, że nie mogę znaleźć niczego na temat używania tego samego alokatora (kopii a) w celu zwolnienia tej pamięci. Co oznaczałoby pewne przechowywanie tej kopii a. Brak informacji na ten temat w [util.smartptr.shared.dest].
Daniel Langr
1
@DanielsaysreinstateMonica, zastanawiam się, czy w util.smartptr.shared / 1: „Szablon klasy shared_ptr przechowuje wskaźnik, zwykle uzyskiwany przez nowy. Shared_ptr implementuje semantykę współwłasności; ostatni pozostały właściciel wskaźnika jest odpowiedzialny za zniszczenie obiektu, lub w inny sposób zwalniając zasoby związane z przechowywanym wskaźnikiem. ” że zwalniając zasoby związane z zapisanego wskaźnika nie jest przeznaczony do tego. Ale blok kontrolny powinien również przetrwać do momentu usunięcia ostatniego słabego wskaźnika.
AProgrammer
4

Od std :: shared_ptr mamy:

Blok sterujący jest dynamicznie przydzielanym obiektem, który przechowuje:

  • wskaźnik do obiektu zarządzanego lub sam obiekt zarządzany;
  • deleter (skasowany typ);
  • alokator (skasowany typ);
  • liczba shared_ptrs, które są właścicielem zarządzanego obiektu;
  • liczba słabych punktów odnoszących się do zarządzanego obiektu.

A od std :: przydzielate_shared otrzymujemy:

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

Konstruuje obiekt typu T i zawija go w std :: shared_ptr [...] w celu użycia jednej alokacji zarówno dla bloku kontrolnego wskaźnika wspólnego, jak i obiektu T.

Wygląda więc na to, że std :: assignate_shared powinien przydzielić to deleterz twoim Alloc.

EDYCJA: I od n4810§20.11.3.6 Creation [util.smartptr.shared.create]

1 Wspólne wymogi, które mają zastosowanie do wszystkich make_shared, allocate_shared, make_shared_default_init, i allocate_shared_default_initprzeciążeń, o ile nie zaznaczono inaczej, są opisane poniżej.

[...]

7 Uwagi: (7.1) - Implementacje powinny wykonywać nie więcej niż jeden przydział pamięci. [Uwaga: Zapewnia to wydajność równoważną inwazyjnemu inteligentnemu wskaźnikowi. —Wskazówka]

[Podkreśl wszystkie moje]

Tak więc standard mówi, że std::allocate_shared należy użyć Allocbloku kontrolnego.

Paul Evans
źródło
1
Przykro mi z powodu preferencji nie jest tekstem normatywnym. To świetny zasób, ale niekoniecznie na pytania prawników językowych .
StoryTeller - Unslander Monica,
@ StoryTeller-UnslanderMonica Całkowicie się zgadzam - przejrzałem najnowszy standard i nie mogłem nic znaleźć, więc poszedłem z cppreference.
Paul Evans,
Znaleziono n4810i zaktualizowano odpowiedź.
Paul Evans,
1
To jednak mówi o make_sharedsamych konstruktorach. Nadal mogę używać członka do małych programów usuwających.
LF
3

Uważam, że nie jest to określone.

Oto specyfikacja odpowiednich konstruktorów: [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);

Efekty: Konstruuje shared_­ptrobiekt będący właścicielem obiektu pi separatora d. Gdy Tnie jest typem tablicy, pierwszy i drugi konstruktor włączają za shared_­from_­thispomocą p. Drugi i czwarty konstruktor wykorzystają kopię ado przydzielenia pamięci do użytku wewnętrznego . W przypadku zgłoszenia wyjątku d(p)wywoływany jest.

Moja interpretacja jest taka, że ​​gdy implementacja potrzebuje pamięci do użytku wewnętrznego, robi to za pomocą a. Nie oznacza to, że implementacja musi wykorzystać tę pamięć do umieszczenia wszystkiego. Załóżmy na przykład, że jest taka dziwna implementacja:

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

Czy ta implementacja „używa kopii w acelu przydzielenia pamięci do użytku wewnętrznego”? Tak. Nigdy nie przydziela pamięci, chyba że za pomocą a. Jest wiele problemów z tą naiwną implementacją, ale powiedzmy, że przełącza się ona na używanie alokatorów we wszystkich, z wyjątkiem najprostszego przypadku, w którym shared_ptrjest on konstruowany bezpośrednio ze wskaźnika i nigdy nie jest kopiowany, przenoszony ani w inny sposób przywoływany i nie ma innych komplikacji. Chodzi o to, że tylko dlatego, że nie wyobrażamy sobie prawidłowej implementacji, sama w sobie nie dowodzi, że nie może teoretycznie istnieć. Nie twierdzę, że taka implementacja może być faktycznie znaleziona w prawdziwym świecie, tylko że standard nie wydaje się aktywnie jej zabraniać.

LF
źródło
IMO shared_ptrdla małych typów przydziela pamięć na stosie. I tak nie spełnia standardowych wymagań
Bartop
1
@bartop Nie „przydziela” żadnej pamięci na stosie. _Smaller_deleter jest bezwarunkowo częścią reprezentacji share_ptr. Wywołanie konstruktora w tym miejscu nie oznacza przydzielenia niczego. W przeciwnym razie nawet przytrzymanie wskaźnika do bloku kontrolnego liczy się jako „alokacja pamięci”, prawda? :-)
LF
Ale narzędzie do usuwania nie musi być kopiowalne, więc jak to zadziała?
Nicol Bolas,
@NicolBolas Umm ... Użyj std::move(__d)i cofnij się, allocategdy wymagana jest kopia.
LF,