Vom Benutzer bereitgestellte std :: allocator-Spezialisierung

8

Klassenvorlagen im ::stdNamespace können im Allgemeinen durch Programme für benutzerdefinierte Typen spezialisiert werden. Ich habe keine Ausnahme von dieser Regel für gefunden std::allocator.

Darf ich mich also auf std::allocatormeine eigenen Typen spezialisieren? Und wenn ich darf, muss ich dann alle Mitglieder der std::allocatorprimären Vorlage bereitstellen , da viele von ihnen von bereitgestellt werden können std::allocator_traits(und daher in C ++ 17 veraltet sind)?

Betrachten Sie dieses Programm

#include<vector>
#include<utility>
#include<type_traits>
#include<iostream>
#include<limits>
#include<stdexcept>

struct A { };

namespace std {
    template<>
    struct allocator<A> {
        using value_type = A;
        using size_type = std::size_t;
        using difference_type = std::ptrdiff_t;
        using propagate_on_container_move_assignment = std::true_type;

        allocator() = default;

        template<class U>
        allocator(const allocator<U>&) noexcept {}

        value_type* allocate(std::size_t n) {
            if(std::numeric_limits<std::size_t>::max()/sizeof(value_type) < n)
                throw std::bad_array_new_length{};
            std::cout << "Allocating for " << n << "\n";
            return static_cast<value_type*>(::operator new(n*sizeof(value_type)));
        }

        void deallocate(value_type* p, std::size_t) {
            ::operator delete(p);
        }

        template<class U, class... Args>
        void construct(U* p, Args&&... args) {
            std::cout << "Constructing one\n";
            ::new((void *)p) U(std::forward<Args>(args)...);
        };

        template<class U>
        void destroy( U* p ) {
            p->~U();
        }

        size_type max_size() const noexcept {
            return std::numeric_limits<size_type>::max()/sizeof(value_type);
        }
    };
}

int main() {
    std::vector<A> v(2);
    for(int i=0; i<6; i++) {
        v.emplace_back();
    }
    std::cout << v.size();
}

Die Ausgabe dieses Programms mit libc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libc++) ist:

Allocating for 2
Constructing one
Constructing one
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

und die Ausgabe mit libstdc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libstdc++) ist:

Allocating for 2
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

Wie Sie sehen können libstdc ++ ehren nicht immer die Überlastung , constructdass ich zur Verfügung gestellt haben , und wenn ich die entfernen construct, destroyoder max_sizeMitglieder, dann wird das Programm nicht einmal mit libstdc ++ beschwerte sich über diese fehlenden Mitglieder kompilieren, obwohl sie geliefert werden std::allocator_traits.

Hat das Programm ein undefiniertes Verhalten und sind daher beide Standardbibliotheken korrekt, oder ist das Verhalten des Programms genau definiert und die Standardbibliothek für die Verwendung meiner Spezialisierung erforderlich?


Beachten Sie, dass es einige Mitglieder aus std::allocatorder primären Vorlage gibt, die ich in meiner Spezialisierung noch ausgelassen habe. Muss ich sie auch hinzufügen?

Um genau zu sein, habe ich ausgelassen

using is_always_equal = std::true_type

std::allocator_traitsDies wird von bereitgestellt, da mein Allokator leer ist, aber Teil der std::allocatorSchnittstelle wäre.

Ich verließ auch heraus pointer, const_pointer, reference, const_reference, rebindund address, die alle durch zur Verfügung gestellt werdenstd::allocator_traits und in C ++ als veraltet 17 für std::allocator‚s - Schnittstelle.

Wenn Sie der Meinung sind, dass alle diese Elemente so definiert werden müssen, dass sie mit der std::allocatorBenutzeroberfläche übereinstimmen , sollten Sie sie dem Code hinzufügen.

Nussbaum
quelle
Ich nehme das alles zurück. Ich habe es in Visual Studio 2019 versucht und es enthält alle Konstruktoraufrufe (sogar die Kopierkonstruktoraufrufe). . Trotzdem denke ich, dass libstdc ++ es so implementiert, dass der Optimierer glaubt, dass es sie entfernen kann.
Spencer

Antworten:

3

Gemäß 23.2.1 [container.requirements.general] / 3:

Für die von diesem Unterabschnitt betroffenen Komponenten, die ein deklarieren allocator_type, müssen in diesen Komponenten gespeicherte Objekte mit der allocator_traits<allocator_type>::constructFunktion erstellt werden

Auch gemäß 17.6.4.2.1:

Das Programm kann dem Namespace stdnur dann eine Vorlagenspezialisierung für eine Standardbibliotheksvorlage hinzufügen, wenn die Deklaration von einem benutzerdefinierten Typ abhängt und die Spezialisierung die Standardbibliotheksanforderungen für die ursprüngliche Vorlage erfüllt und nicht ausdrücklich verboten ist.

Ich glaube nicht, dass der Standard die Spezialisierung verbietet std::allocator, da ich alle Abschnitte gelesen habe std::allocatorund nichts erwähnt habe. Ich habe mir auch angesehen, wie es aussieht, wenn der Standard die Spezialisierung verbietet, und ich habe nichts Vergleichbares gefunden std::allocator.

Die Anforderungen für Allocatorsind hier und Ihre Spezialisierung erfüllt sie.

Daher kann ich nur schlussfolgern, dass libstdc ++ tatsächlich gegen den Standard verstößt (vielleicht habe ich irgendwo einen Fehler gemacht). Ich habe festgestellt, dass std::allocatorlibstdc ++ , wenn man sich nur spezialisiert , mit der Platzierung new für den Konstruktor reagiert, da sie eine speziell für diesen Fall spezialisierte Vorlage haben, während sie den angegebenen Allokator für andere Operationen verwenden. Der relevante Code ist hier (dies ist in namespace std; allocatorhier ist ::std::allocator):

  // __uninitialized_default_n_a
  // Fills [first, first + n) with n default constructed value_types(s),
  // constructed with the allocator alloc.
  template<typename _ForwardIterator, typename _Size, typename _Allocator>
    _ForwardIterator
    __uninitialized_default_n_a(_ForwardIterator __first, _Size __n, 
                _Allocator& __alloc)
    {
      _ForwardIterator __cur = __first;
      __try
    {
      typedef __gnu_cxx::__alloc_traits<_Allocator> __traits;
      for (; __n > 0; --__n, (void) ++__cur)
        __traits::construct(__alloc, std::__addressof(*__cur));
      return __cur;
    }
      __catch(...)
    {
      std::_Destroy(__first, __cur, __alloc);
      __throw_exception_again;
    }
    }

  template<typename _ForwardIterator, typename _Size, typename _Tp>
    inline _ForwardIterator
    __uninitialized_default_n_a(_ForwardIterator __first, _Size __n, 
                allocator<_Tp>&)
    { return std::__uninitialized_default_n(__first, __n); }

std::__uninitialized_default_nAnrufe, std::_Constructdie Platzierung neu verwenden. Dies erklärt, warum in Ihrer Ausgabe "Konstruieren einer" vor "Zuweisen für 4" nicht angezeigt wird.

EDIT: Wie OP in einem Kommentar betonte, std::__uninitialized_default_nruft

__uninitialized_default_n_1<__is_trivial(_ValueType)
                             && __assignable>::
__uninit_default_n(__first, __n)

das hat eigentlich eine spezialisierung wenn __is_trivial(_ValueType) && __assignableist true, was ist hier . Es verwendet std::fill_n(wo valuetrivial konstruiert ist), anstatt std::_Constructjedes Element aufzurufen . Da Aes trivial und kopierbar ist, würde es diese Spezialisierung tatsächlich aufrufen. Dies wird natürlich auch nicht verwendet std::allocator_traits<allocator_type>::construct.

Leonid
quelle
1
In meinem speziellen Fall wird der std::_ConstructAnruf von dem, was ich sagen kann, nicht tatsächlich verwendet , da er Atrivial kopierbar und trivial kopierzuweisbar ist. Daher wird die andere Spezialisierung von __uninitialized_default_n_1gewählt, die std::fill_nstattdessen aufruft und dann erneut an memcpy/ sendet memset. Ich war mir dessen bewusst, wie es libstdc ++ optimiert, aber ich wusste nicht, dass der std::allocator::constructAufruf auch für nicht triviale Typen übersprungen wird. Es kann also nur ein Versehen sein, wie libstdc ++ identifiziert, dass die von der Bibliothek bereitgestellte Bibliothek std::allocatorverwendet wird.
Walnuss
Du hast recht. Bearbeitet.
Leonid