2016-02-29 11 views
13

Niektórzy programiści jawnie nazywają konstruktory i destruktory w przypadku niektórych obejść. Wiem, to nie jest dobra praktyka, ale wydaje się, że robi się to, aby zrealizować pewne scenariusze. Na przykład w tym artykule Beautiful Native Libraries autor stosuje tę technikę.Czy jawnie wywołujesz konstruktory i destruktory bezpieczne, zgodnie ze standardem C++?

W poniższym kodzie, na końcu, można zauważyć, że konstruktor nazywa wprost:

#include <limits> 

template <class T> 
struct proxy_allocator { 
    typedef size_t size_type; 
    typedef ptrdiff_t difference_type; 
    typedef T *pointer; 
    typedef const T *const_pointer; 
    typedef T& reference; 
    typedef const T &const_reference; 
    typedef T value_type; 

    template <class U> 
    struct rebind { 
     typedef proxy_allocator<U> other; 
    }; 

    proxy_allocator() throw() {} 
    proxy_allocator(const proxy_allocator &) throw() {} 
    template <class U> 
    proxy_allocator(const proxy_allocator<U> &) throw() {} 
    ~proxy_allocator() throw() {} 

    pointer address(reference x) const { return &x; } 
    const_pointer address(const_reference x) const { return &x; } 

    pointer allocate(size_type s, void const * = 0) { 
     return s ? reinterpret_cast<pointer>(yl_malloc(s * sizeof(T))) : 0; 
    } 

    void deallocate(pointer p, size_type) { 
     yl_free(p); 
    } 

    size_type max_size() const throw() { 
     return std::numeric_limits<size_t>::max()/sizeof(T); 
    } 

    void construct(pointer p, const T& val) { 
     new (reinterpret_cast<void *>(p)) T(val); 
    } 

    void destroy(pointer p) { 
     p->~T(); 
    } 

    bool operator==(const proxy_allocator<T> &other) const { 
     return true; 
    } 

    bool operator!=(const proxy_allocator<T> &other) const { 
     return false; 
    } 
}; 

Dla niektórych sytuacjach takich jak ta, może być konieczne, aby zadzwonić konstruktorów i destruktorów wyraźnie, ale co czy standard mówi: czy jest to niezdefiniowane zachowanie, czy jest to nieokreślone zachowanie, czy jest to zdefiniowane zachowanie implementacji, czy jest dobrze zdefiniowane?

+1

Musisz tylko upewnić się, że każdy obiekt jest skonstruowany raz i zniszczony po raz –

+0

Pamiętaj, że mówiąc ściśle, nie możesz "wywołać konstruktora". Konstruktory nie mają nazw. Są wywoływane podczas inicjowania obiektu. Jednym ze sposobów zainicjowania obiektu jest użycie 'nowego'. Jedna składnia 'nowego' to nowe miejsce docelowe, które tworzy obiekt w danej lokalizacji. – isanae

Odpowiedz

21

Tak, jest obsługiwany i dobrze zdefiniowany, jest bezpieczny.

new (reinterpret_cast<void *>(p)) T(val); 

Czy nazywany placement new syntax i jest używany do konstruowania obiektu w specific memory location, domyślne zachowanie; takie, jakie są wymagane w opublikowanym alokatorze. Jeśli nowe miejsce docelowe zostanie przeciążone dla określonego typu T, zostanie ono wywołane zamiast nowego globalnego miejsca docelowego.

Jedynym sposobem na zniszczenie takiego skonstruowanego obiektu jest explicitly call the destructorp->~T();.

Użycie miejsca docelowego nowe i jawne zniszczenie wymaga/zezwala, że ​​zaimplementowany kod kontroluje czas życia obiektu - kompilator oferuje w tym przypadku niewielką pomoc; dlatego ważne jest, aby obiekty zostały zbudowane w dobrze wyrównanych i odpowiednio przydzielonych lokalizacjach. Ich użycie często znajduje się w alokatorach, takich jak OP i std::allocator.

+3

Uwaga: jest ona ważna i dozwolona, ​​z pewnością; jednak prawdopodobnie powinien pojawić się tylko w alokatorach/kontenerach. –

+3

@ MatthieuM. Czy dyskryminujące są kontenery związków? Opcje? Auto-storage o maksymalnych rozmiarach stałych 'wektor'-like? (nie kwalifikuje się jako kontener pod normą) FF "funkcja"? Prawdopodobnie powinien występować tylko na niskim poziomie, ostrożny kod, ale tylko "alokatory/kontenery" przesuwają go. – Yakk

+2

@Yakk: Uważam, że 'optional',' variant' i max-fixed-sized to yes, ponieważ ich jedyną rolą jest umieszczanie innych obiektów. Nie mam pojęcia, co to jest FZŚ. –

8

Tak, jest całkowicie bezpieczny. W gruncie rzeczy wszystkie standardowe pojemniki , takie jak std::vector, domyślnie używają techniki, ponieważ jest to jedyny sposób oddzielenia alokacji pamięci od konstrukcji elementu.

Dokładniej, szablony standardowych kontenerów mają argument szablonu Allocator, który domyślnie przyjmuje wartość std::allocator, a std::allocator używa nowego miejsca w jego funkcji składowej allocate.

to, na przykład, co pozwala std::vector wdrożyć push_back takie, że alokacja pamięci nie musi się zdarzyć cały czas, lecz niektóre dodatkowo pamięć jest alokowana gdy prądowa nie jest już wystarczające, przygotowanie miejsca dla elementów dodano: przyszłośćpush_back s.

Oznacza to, że po wywołaniu setki razy w pętli, std::vector jest na tyle sprytny, aby nie przydzielać pamięci za każdym razem, co pomaga w wydajności, ponieważ realokacja i przenoszenie istniejącej zawartości kontenera do nowego miejsca w pamięci jest kosztowne.

Przykład:

#include <vector> 
#include <iostream> 

int main() 
{ 
    std::vector<int> v; 

    std::cout << "initial capacity: " << v.capacity() << "\n"; 

    for (int i = 0; i < 100; ++i) 
    { 
     v.push_back(0); 

     std::cout << "capacity after " << (i + 1) << " push_back()s: " 
      << v.capacity() << "\n"; 
    } 
} 

wyjściowa:

initial capacity: 0 
capacity after 1 push_back()s: 1 
capacity after 2 push_back()s: 2 
capacity after 3 push_back()s: 3 
capacity after 4 push_back()s: 4 
capacity after 5 push_back()s: 6 
capacity after 6 push_back()s: 6 
capacity after 7 push_back()s: 9 
capacity after 8 push_back()s: 9 
capacity after 9 push_back()s: 9 
capacity after 10 push_back()s: 13 
capacity after 11 push_back()s: 13 
capacity after 12 push_back()s: 13 
capacity after 13 push_back()s: 13 
capacity after 14 push_back()s: 19 

(...)

capacity after 94 push_back()s: 94 
capacity after 95 push_back()s: 141 
capacity after 96 push_back()s: 141 
capacity after 97 push_back()s: 141 
capacity after 98 push_back()s: 141 
capacity after 99 push_back()s: 141 
capacity after 100 push_back()s: 141 

Ale oczywiście, nie chcą wywołać konstruktor dla potencjalne przyszłe elementy:. Dla int nie ma to znaczenia, ale potrzebujemy rozwiązania dla każdego T, w tym typów bez domyślnych konstruktorów. Jest to moc nowego miejsca docelowego: najpierw przydziel pamięć, następnie umieść elementy w przydzielonej pamięci później, korzystając z ręcznego wywołania konstruktora.


Na marginesie, wszystko to byłoby niemożliwe przy new[]. W rzeczywistości new[] jest dość bezużyteczną funkcją językową.


P.S .: Tylko dlatego, że standardowe pojemniki wewnętrznie używają nowego miejsca docelowego, nie oznacza to, że powinieneś zaszaleć w swoim własnym kodzie. Jest to technika niskopoziomowa, a jeśli nie zaimplementujesz swojej własnej ogólnej struktury danych, ponieważ żaden standardowy kontener nie zapewnia potrzebnej funkcjonalności, możesz nigdy nie znaleźć dla niej żadnego zastosowania.

Powiązane problemy