2012-07-28 4 views
5

Ich schreibe einen Container und möchte dem Benutzer erlauben, benutzerdefinierte Zuordner zu verwenden, aber ich kann nicht sagen, ob ich Zuweiser um Verweis oder nach Wert herumgeben sollte.Kann ich annehmen, dass Zuweiser ihren Speicherpool nicht direkt behalten (und daher kopiert werden können)?

Ist es garantiert (oder zumindest eine vernünftige Annahme zu machen), dass ein allocator Objekt nicht seinen Speicherpool enthält direkt, und daher wäre es OK sein, eine allocator zu kopieren und die Speicherpools des Zuweiser erwarten Cross-kompatibel sein? Oder muss ich Allokatoren immer als Referenz übergeben?

(Ich habe das vorbei Referenz Harms Leistung um einen Faktor von> 2 gefunden, da der Compiler über Aliasing beginnt sich Gedanken, so macht es ein, ob ich auf dieser Annahme verlassen kann.)

+0

Können Sie bitte einige Einzelheiten hinzufügen, was Sie zu tun versuchen? Sie müssen natürlich mit Stateful Allocators vorsichtig sein, aber bestimmte Standardoperationen wie das Verschieben eines Containers aus einem anderen Container haben Standard-Idiome. –

+0

@KerrekSB: Ah, ok. Zum Beispiel versuche ich, eine "resize" - oder "reserve" -Operationsoperation als "copy-this-container-and-swap" -Operation zu implementieren. Dies funktioniert, wenn der Zuordner keinen eigenen Speicherpool hat. (Wenn ja, könnte ich zweimal tauschen, aber das Erstellen einer Kopie könnte immer noch teuer sein und könnte den Stapel überfluten, wenn es einen eigenen Pool enthält.) – Mehrdad

Antwort

9

In C++ 11 Abschnitt 17.6.3.5 Zuweisungsanforderungen [allocator.requirements] gibt die Anforderungen für konforme Zuweiser an. Zu den Anforderungen gehören:

X     an Allocator class for type T 
... 
a, a1, a2   values of type X& 
... 
a1 == a2    bool   returns true only if storage 
            allocated from each can be 
            deallocated via the other. 
            operator== shall be reflexive, 
            symmetric, and transitive, and 
            shall not exit via an exception. 
... 
X a1(a);       Shall not exit via an exception. 
            post: a1 == a 

I.e. Wenn Sie einen Zuordner kopieren, müssen die zwei Kopien in der Lage sein, die Zeiger des anderen zu löschen.

Vorstellbar könnte man interne Puffer in Zuweiser setzen, aber Kopien müssten eine Liste der anderen Puffer behalten. Oder vielleicht könnte ein Allokator eine Invariante haben, dass die Deallokation immer ein No-Op ist, weil der Zeiger immer von einem internen Puffer kommt (entweder von Ihrem eigenen oder von einer anderen Kopie).

Aber was auch immer das Schema, Kopien müssen "Cross-kompatibel" sein.

aktualisieren

ist hier ein allocator C++ 11 entspricht, welche die "kurzen String-Optimierung" der Fall ist. Um es C++ 11 kompatibel machen, musste ich setze die „internen“ den Allocator externe Puffer, so dass Kopien sind gleich:

#include <cstddef> 

template <std::size_t N> 
class arena 
{ 
    static const std::size_t alignment = 16; 
    alignas(alignment) char buf_[N]; 
    char* ptr_; 

    std::size_t 
    align_up(std::size_t n) {return n + (alignment-1) & ~(alignment-1);} 

public: 
    arena() : ptr_(buf_) {} 
    arena(const arena&) = delete; 
    arena& operator=(const arena&) = delete; 

    char* allocate(std::size_t n) 
    { 
     n = align_up(n); 
     if (buf_ + N - ptr_ >= n) 
     { 
      char* r = ptr_; 
      ptr_ += n; 
      return r; 
     } 
     return static_cast<char*>(::operator new(n)); 
    } 
    void deallocate(char* p, std::size_t n) 
    { 
     n = align_up(n); 
     if (buf_ <= p && p < buf_ + N) 
     { 
      if (p + n == ptr_) 
       ptr_ = p; 
     } 
     else 
      ::operator delete(p); 
    } 
}; 

template <class T, std::size_t N> 
class stack_allocator 
{ 
    arena<N>& a_; 
public: 
    typedef T value_type; 

public: 
    template <class U> struct rebind {typedef stack_allocator<U, N> other;}; 

    explicit stack_allocator(arena<N>& a) : a_(a) {} 
    template <class U> 
     stack_allocator(const stack_allocator<U, N>& a) 
      : a_(a.a_) {} 
    stack_allocator(const stack_allocator&) = default; 
    stack_allocator& operator=(const stack_allocator&) = delete; 

    T* allocate(std::size_t n) 
    { 
     return reinterpret_cast<T*>(a_.allocate(n*sizeof(T))); 
    } 
    void deallocate(T* p, std::size_t n) 
    { 
     a_.deallocate(reinterpret_cast<char*>(p), n*sizeof(T)); 
    } 

    template <class T1, std::size_t N1, class U, std::size_t M> 
    friend 
    bool 
    operator==(const stack_allocator<T1, N1>& x, const stack_allocator<U, M>& y); 

    template <class U, std::size_t M> friend class stack_allocator; 
}; 

template <class T, std::size_t N, class U, std::size_t M> 
bool 
operator==(const stack_allocator<T, N>& x, const stack_allocator<U, M>& y) 
{ 
    return N == M && &x.a_ == &y.a_; 
} 

template <class T, std::size_t N, class U, std::size_t M> 
bool 
operator!=(const stack_allocator<T, N>& x, const stack_allocator<U, M>& y) 
{ 
    return !(x == y); 
} 

Es könnte wie folgt verwendet werden:

#include <vector> 

template <class T, std::size_t N> using A = stack_allocator<T, N>; 
template <class T, std::size_t N> using Vector = std::vector<T, stack_allocator<T, N>>; 

int main() 
{ 
    const std::size_t N = 1024; 
    arena<N> a; 
    Vector<int, N> v{A<int, N>(a)}; 
    v.reserve(100); 
    for (int i = 0; i < 100; ++i) 
     v.push_back(i); 
    Vector<int, N> v2 = std::move(v); 
    v = v2; 
} 

All Allokationen für das obige Problem werden von der lokalen arena gezogen, die 1 KB groß ist. Sie sollten in der Lage sein, diesen Allokator nach Wert oder Verweis zu übergeben.

+0

+1 Thanks for diese feine antwort! –

+0

+1 in der tat, das beantwortet meine frage direkt. (Und ihre anderen antworten auf SO sind auch sehr hilfreich, btw.) Danke ein bund! :) – Mehrdad

+0

@Howard: Hmm, auf eine andere Anmerkung, ich bin nicht sicher "kreuzkompatibel" ist für mich ausreichend klar. Wenn ich einen Zuordner kopiere, dessen Deallokationsroutine ein No-Op ist (er enthält seinen eigenen Puffer), dann wären alle seine Operationen "kreuzkompatibel". Aber wenn der kopierte Allokator zerstört wird, indem er den Gültigkeitsbereich verlässt, was passiert mit den Items, die ihm zugewiesen wurden? – Mehrdad

11

Der alte C++ Standard schreibt Anforderungen für einen standardkonformen Zuordner vor: Diese Anforderungen beinhalten, dass Sie Alloc<T> a, b, dann a == b, und Sie können b verwenden, um die Zuordnung von Objekten aufzuheben, die mit a zugewiesen wurden. Zuweiser sind grundsätzlich zustandslos.


In C++ 11, hat sich die Situation mehr beteiligt viel, da es jetzt Unterstützung für Stateful Verteilern ist. Beim Kopieren und Verschieben von Objekten gibt es bestimmte Regeln, ob ein Container kopiert oder aus einem anderen Container verschoben werden kann, wenn sich die Zuordner unterscheiden und die Zuordner kopiert oder verschoben werden.

Nur um Ihre Frage zuerst zu beantworten: Nein, können Sie definitiv nicht annehmen, dass es sinnvoll ist, Ihren Allokator herum kopieren, und Ihr Zuordner möglicherweise nicht einmal kopierbar sein.

Hier 23.2.1/7 zu diesem Thema:

Soweit nicht anders angegeben, sind alle in dieser Klausel definierten Container Speicher erhalten einen Zuteiler (siehe 17.6.3.5). Kopierkonstruktoren für diese Containertypen erhalten einen Zuordner, indem sie für ihre ersten Parameter allocator_traits<allocator_-type>::select_on_container_copy_construction aufrufen. Move-Konstruktoren erhalten einen Allokator nach der Move-Konstruktion aus dem Zuordner des zu verschiebenden Containers. Ein solcher Umzug des Allokators darf nicht über eine Ausnahme erfolgen. Alle anderen Konstruktoren für diese Containertypen nehmen ein Allocator&-Argument (17.6.3.5), einen Zuordner, dessen Werttyp dem Werttyp des Containers entspricht. [Hinweis: Wenn ein Aufruf eines Konstruktors den Standardwert eines optionalen Zuweisungsarguments verwendet, muss der Zuordnertyp die Wertinitialisierung unterstützen. Anmerkung: Eine Kopie dieses Zuordners wird für jede Speicherzuweisung verwendet, die von diesen Konstruktoren und allen Elementfunktionen während der Lebensdauer jedes Containerobjekts ausgeführt wird oder bis der Zuordner ersetzt wird. Der Allokator kann nur durch Zuweisung oder swap() ersetzt werden. Der Zuweisungszuordner wird nur dann durch Zuordnungszuweisung, Zuordnungszuordnung oder Umlagerung des Zuweisers ausgeführt, wenn allocator_traits<allocator_type>::propagate_on_container_copy_assignment::value, allocator_traits<allocator_type>::propagate_on_container_move_assignment::value oder allocator_traits<allocator_type>::propagate_on_container_swap::value innerhalb der Implementierung der entsprechenden Containeroperation zutrifft. Das Verhalten eines Aufrufs der Swap-Funktion eines Containers ist undefiniert, es sei denn, die Objekte, die ausgetauscht werden, weisen Zuordnungen auf, die Gleichheit oder allocator_traits<allocator_type>::propagate_on_container_swap::value sind wahr. Bei allen in dieser Klausel definierten Containertypen gibt das Element get_allocator() eine Kopie des zum Erstellen des Containers verwendeten Zuweisers oder, falls dieser Zuordner ersetzt wurde, eine Kopie des letzten Ersatzes zurück.

Siehe auch die Dokumentation von std::allocator_traits für eine Zusammenfassung.

+0

Oohhhhhhh ... also gibt es einen ['scoped_allocator_adaptor'] (http: // en .cppreference.com/w/cpp/memory/scoped_allocator_adaptor) speziell für diesen Zweck? Ich denke, das bedeutet, dass ich sie nicht einfach blind kopieren kann. :(+1 tolle Antwort, danke! – Mehrdad

+0

@Mehrdad: Die Scoped-Sache ist für Situationen, wo Sie Containern von Containern haben und Sie möchten, dass Ihr benutzerdefinierter Allokator überall hin verwendet wird. –

+0

Oh hmmm interessant. – Mehrdad

Verwandte Themen