Czym jest idiom kopiowanie i wymiana?

Czym jest ten idiom i kiedy należy go używać? Jakie problemy rozwiązuje? Czy idiom zmienia się po użyciu C++11?

Chociaż było to wspominane w wielu miejscach, nie mieliśmy żadnego pojedynczego pytania" co to jest " i odpowiedzi, więc oto jest. Oto częściowa lista miejsc, w których był wcześniej wymieniony:

Author: Community, 2010-07-19

5 answers

Przegląd

Dlaczego potrzebujemy idiomu copy-and-swap?

Każda klasa, która zarządza zasobem (opakowanie , jak inteligentny wskaźnik), musi zaimplementować wielką trójkę . Podczas gdy cele i implementacja konstruktora kopiującego i destruktora są proste, operator kopiującego przypisania jest prawdopodobnie najbardziej dopracowany i trudny. Jak należy to zrobić? Jakich pułapek należy unikać?

Idiom copy-and-swap jest rozwiązanie i elegancko pomaga operatorowi przypisania osiągnąć dwie rzeczy: unikanie powielania kodu i zapewnienie silnej gwarancji wyjątku .

Jak to działa?

Koncepcyjnie , działa poprzez użycie funkcji copy-constructor do utworzenia lokalnej kopii danych, a następnie pobiera skopiowane dane za pomocą funkcji swap, zamieniając stare dane na nowe. Następnie Kopia tymczasowa ulega zniszczeniu, zabierając ze sobą stare dane. Jesteśmy w lewo z kopią nowych danych.

Aby użyć idiomu copy-and-swap, potrzebujemy trzech rzeczy: działającego konstruktora kopiującego, działającego destruktora (oba są podstawą każdego wrappera, więc i tak powinny być kompletne) i swap funkcji.

Funkcja swap jest nie rzucającą funkcją, która zamienia dwa obiekty klasy, member na member. Możemy pokusić się o użycie std::swap zamiast podania własnego, ale byłoby to niemożliwe; std::swap używa konstruktora kopiującego i operatora przyporządkowania kopii w ramach jego implementacji, a ostatecznie będziemy próbować zdefiniować operatora przyporządkowania pod względem samym w sobie!

(nie tylko to, ale niewykwalifikowane wywołania do swap użyją naszego niestandardowego operatora swap, pomijając niepotrzebną budowę i zniszczenie naszej klasy, które pociągnęłyby za sobą std::swap.)


Szczegółowe wyjaśnienie

Cel

Rozważmy konkretny przypadek. Chcemy zarządzać, w zupełnie bezużytecznej klasie, a tablica dynamiczna. Zaczynamy od działającego konstruktora, konstruktora kopiującego i destruktora: {]}
#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Ta klasa prawie zarządza tablicą z powodzeniem, ale musi operator= działać poprawnie.

Nieudane rozwiązanie

Tak może wyglądać naiwna implementacja:]}
// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

I mówimy, że skończyliśmy; to teraz zarządza tablicą, bez przecieków. Jednak cierpi na trzy problemy, oznaczone kolejno w kodzie jako (n).

  1. Pierwszy to test na samodzielne zadanie. To sprawdzanie służy dwóm celom: jest to łatwy sposób, aby uniemożliwić nam uruchamianie niepotrzebnego kodu podczas samodzielnego przypisywania i chroni nas przed subtelnymi błędami (takimi jak usunięcie tablicy tylko po to, aby spróbować ją skopiować). Ale we wszystkich innych przypadkach służy jedynie spowolnieniu programu i działa jak szum w kodzie; samo-przypisanie rzadko się zdarza, więc przez większość czasu ta kontrola jest marnotrawstwem. Byłoby lepiej, gdyby operator mógł pracować prawidłowo bez niego.

  2. Drugi jest to, że zapewnia tylko podstawową gwarancję wyjątku. Jeśli new int[mSize] nie powiedzie się, *this zostanie zmodyfikowany. (Mianowicie rozmiar jest zły, a dane zniknęły! W związku z tym, że nie jest to możliwe, nie jest to możliwe.]}

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. Kod się rozszerzył! Co prowadzi nas do trzeciego problemu: powielanie kodu. Nasz operator przydziałów skutecznie powiela cały kod, który już napisaliśmy gdzie indziej, a to straszna rzecz.

W naszym przypadku, rdzeń to tylko dwie linie (alokacja i kopia), ale przy bardziej złożonych zasobach ten kod może być dość kłopotliwy. Powinniśmy starać się nigdy się nie powtarzać.

(można by się zastanawiać: jeśli ta ilość kodu jest potrzebna do prawidłowego zarządzania jednym zasobem, co jeśli moja klasa zarządza więcej niż jednym? Chociaż może się to wydawać słusznym problemem, a w rzeczywistości wymaga nietrywialnego try/catch klauzule, to nie jest problem. To dlatego, że klasa powinna zarządzać jeden zasób tylko!)

Skuteczne rozwiązanie

Jak wspomniano, idiom Kopiuj i wymieniaj rozwiąże wszystkie te problemy. Ale w tej chwili mamy wszystkie wymagania oprócz jednego: funkcję swap. Podczas gdy reguła trzech z powodzeniem pociąga za sobą istnienie naszego konstruktora kopiującego, operatora przypisania i destruktora, tak naprawdę powinna być nazywana "wielką trójką i pół": za każdym razem, gdy twoja klasa zarządza zasobem, ma również sens, aby zapewnić swap funkcję.

Musimy dodać Zamiana funkcjonalności na naszą klasę, A robimy to w następujący sposób†:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

(tutaj {[44] } jest wyjaśnienie dlaczego public friend swap.) Teraz nie tylko możemy zamienić nasze dumb_array ' s, ale swapy w ogóle mogą być bardziej wydajne; to tylko swapy wskaźników i rozmiarów, zamiast przydzielania i kopiowania całych tablic. Oprócz tego bonusu w zakresie funkcjonalności i wydajności, jesteśmy teraz gotowi wdrożyć idiom Kopiuj i wymieniaj.

Bez dalszych ceregieli, naszym operatorem przypisania jest:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

I to jest to! Jednym zamachem wszystkie trzy problemy są elegancko rozwiązywane naraz.

Dlaczego to działa?

Najpierw zauważamy ważny wybór: argument parametru jest brany przez-wartość. Chociaż można równie łatwo zrobić następujące (i rzeczywiście, wiele naiwnych implementacji idiomu zrobić): {]}

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Tracimy ważną możliwość optymalizacji . Nie tylko to, ale ten wybór jest krytyczny w C++11, który jest omówiony później. (Na ogólne Uwaga, niezwykle przydatna wskazówka jest następująca: jeśli masz zamiar zrobić kopię czegoś w funkcji, pozwól kompilatorowi zrobić to na liście parametrów.‡)

Tak czy inaczej, ta metoda pozyskiwania naszego zasobu jest kluczem do wyeliminowania duplikacji kodu: możemy użyć kodu z konstruktora kopiującego, aby wykonać kopię i nigdy nie musimy powtarzać żadnego bitu. Teraz, gdy kopia została wykonana, jesteśmy gotowi do wymiany.

Zauważ, że po wprowadzeniu funkcji wszystkie nowe dane są już przydzielone, skopiowane i gotowe do użycia. To daje nam silną gwarancję WYJĄTKÓW za darmo: nie wejdziemy nawet do funkcji, jeśli konstrukcja kopii zawiedzie, i dlatego nie jest możliwe, aby zmienić stan *this. (Co zrobiliśmy ręcznie wcześniej dla silnej gwarancji WYJĄTKÓW, kompilator robi dla nas teraz; jak miło.)

W tym momencie jesteśmy wolni od domu, ponieważ swap nie rzucamy. Zamieniamy nasze bieżące dane z kopiowanymi, bezpiecznie zmieniając nasz stan, a stare dane zostają wprowadzone do tymczasowego. Stare dane są następnie zwalniane, gdy funkcja powróci. (Gdzie na końcu zakresu parametru i wywołany jest jego Destruktor.)

Ponieważ idiom nie powtarza kodu, nie możemy wprowadzać błędów wewnątrz operatora. Zauważ, że oznacza to, że jesteśmy pozbawieni potrzeby samodzielnego sprawdzenia, pozwalając na jednolitą implementację operator=. (Dodatkowo, nie mamy już kary za wykonywanie zadań bez siebie.)

I to jest idiom copy-and-swap.

A co z C++11?

Kolejna wersja C++, C++11, wprowadza jedną bardzo ważną zmianę w sposobie zarządzania zasobami: reguła trzech jest terazregułą czterech (i pół). Dlaczego? Ponieważ nie tylko musimy być w stanie kopiować-konstruować nasz zasób, musimy również go przenosić-konstruować.

Na szczęście dla nas, to proste:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};
Co tu się dzieje? Przypomnijmy sobie cel ruchu-Budowa: aby wziąć zasoby z innej instancji klasy, pozostawiając ją w stanie gwarantowanym do przypisania i zniszczenia.

Więc to, co zrobiliśmy jest proste: zainicjalizować za pomocą domyślnego konstruktora (funkcja C++11), a następnie swap z other; wiemy, że domyślna instancja naszej klasy może być bezpiecznie przypisana i zniszczona, więc wiemy, że {27]} będzie w stanie zrobić to samo, po zamianie.

(zauważ, że niektóre Kompilatory nie obsługują delegowania konstruktorów; w tym przypadku musimy ręcznie domyślnie konstruuj klasę. Jest to niefortunne, ale na szczęście trywialne zadanie.)

Dlaczego to działa?

To jedyna zmiana, jaką musimy wprowadzić w naszej klasie, więc dlaczego to działa? Zapamiętaj zawsze ważną decyzję, jaką podjęliśmy, aby parametr był wartością, a nie referencją:
dumb_array& operator=(dumb_array other); // (1)

Teraz, jeśli other jest inicjalizowana z wartością R, będzie skonstruowana z ruchu . Idealnie. W ten sam sposób C++03 pozwala nam ponownie wykorzystać funkcjonalność copy-constructor C++11 automatycznie wybierze konstruktor move-constructor, jeśli jest to właściwe. (I, oczywiście, jak wspomniano wcześniej w linkowanym artykule, kopiowanie / przenoszenie wartości może zostać całkowicie pominięte.)

I tak kończy się idiom copy-and-swap.


Przypisy

*dlaczego ustawiamy mArray NA null? Ponieważ jeśli jakiś kolejny kod w operatorze rzuca, może zostać wywołany Destruktor dumb_array; a jeśli tak się stanie bez ustawiając ją na null, próbujemy usunąć pamięć, która została już usunięta! Unikamy tego, ustawiając go na null, ponieważ usunięcie null nie jest operacją.

†istnieją inne twierdzenia, że powinniśmy specjalizować się std::swap dla naszego typu, dostarczać w klasie swap wzdłuż boku funkcji swobodnej swap itd. Ale to wszystko jest niepotrzebne: jakiekolwiek właściwe użycie swap będzie przez wywołanie bez zastrzeżeń, a nasza funkcja zostanie znaleziona przez ADL . Wystarczy jedna funkcja.

‡powodem jest proste: gdy masz zasób dla siebie, możesz go zamienić i / lub przenieść (C++11) w dowolne miejsce. Wykonując kopię na liście parametrów, maksymalizujesz optymalizację.

 1874
Author: GManNickG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:54

Zadanie, w jego sercu, to dwa kroki: zburzenie starego stanu obiektu oraz budowanie nowego państwa jako kopia stanu jakiegoś innego obiektu.

Zasadniczo, to jest to, co destructor oraz Konstruktor kopiujący zrobić, więc pierwszym pomysłem byłoby delegowanie pracy do nich. Jednakże, ponieważ zniszczenie nie może zawieść, podczas gdy budowa może, w rzeczywistości chcemy zrobić to na odwrót : najpierw wykonaj część konstruktywną a jeśli to się uda, więc zrób niszczycielską część. Idiom copy-and-swap jest na to sposobem: najpierw wywołuje Konstruktor kopiujący klasy, aby utworzyć tymczasowy, a następnie zamienia jego dane na tymczasowe, a następnie pozwala destruktorowi tymczasowemu zniszczyć stary stan.
Ponieważ swap() ma nigdy nie zawieść, jedyną częścią, która może zawieść, jest konstrukcja kopiująca. To jest wykonywane najpierw, a jeśli się nie powiedzie, nic nie zostanie zmienione w docelowym obiekcie.

W swojej udoskonalonej formie copy-and-swap jest zaimplementowany poprzez wykonanie kopii przez zainicjalizowanie (niereferencyjnego) parametru operatora przypisania:
T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
 234
Author: sbi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-22 14:26:50

Jest już kilka dobrych odpowiedzi. Skupię się głównie na tym, co myślę, że im brakuje-wyjaśnieniu "minusów" z idiomem copy-and-swap....

Czym jest idiom copy-and-swap?

Sposób implementacji operatora przyporządkowania pod względem funkcji swap:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}
Podstawową ideą jest to, że:]}
  • Najbardziej podatną na błędy częścią przypisywania do obiektu jest zapewnienie zasobów, których nowy stan potrzebuje nabyte (np. pamięć, deskryptory)

  • Ta akwizycja może być podjęta przed modyfikacją bieżącego stanu obiektu (tj. *this), jeśli zostanie wykonana kopia nowej wartości, dlatego rhs jest akceptowana przez wartość (tj. skopiowana), a nie przez odniesienie

  • Zmiana stanu lokalnej kopii rhs i *this jest zazwyczaj stosunkowo łatwa do zrobienia bez potencjalnych błędów / WYJĄTKÓW, biorąc pod uwagę, że lokalna kopia nie po uruchomieniu destruktora potrzebny jest jakiś konkretny stan, podobnie jak obiekt przeniesiony Z in > = C++11)

Kiedy należy go stosować? (Jakie problemy rozwiązuje [/create] ?)

  • Jeśli chcesz, aby przypisane-to nie miało wpływu na przypisanie, które rzuca wyjątek, zakładając, że masz lub możesz napisać swap z silną gwarancją wyjątku, a najlepiej taki, który nie może fail/throw..†

  • Gdy chcesz mieć przejrzysty, łatwy do zrozumienia, solidny sposób definiowania operatora przypisania w kategoriach (prostszego) konstruktora kopiującego, swap i funkcji destruktora.

    • SELF-assignment done as a copy-and-swap pozwala uniknąć często pomijanych przypadków edge.‡

  • gdy jakakolwiek kara wydajnościowa lub chwilowo wyższe wykorzystanie zasobów utworzone przez posiadanie dodatkowego tymczasowego obiektu podczas przydziału nie jest ważne dla Ciebie podanie. ⁂

swap rzucanie: na ogół możliwe jest niezawodne Wymienianie elementów danych, które obiekty śledzą za pomocą wskaźnika, ale elementy danych nie-wskaźnikowych, które nie mają wymiany bez rzucania, lub dla których Zamiana musi być zaimplementowana jako X tmp = lhs; lhs = rhs; rhs = tmp; i konstrukcja kopiowania lub przypisanie mogą rzucać, nadal mogą nie działać, pozostawiając niektóre elementy danych zamienione, a inne nie. Ten potencjał odnosi się nawet do C++03 std::string's jak James komentuje inną odpowiedź:

@wilhelmtell: w C++03 nie ma wzmianki o wyjątkach potencjalnie rzucanych przez std:: string:: swap (które jest wywoływane przez std:: swap). W C++0x, std:: string:: swap jest noexcept i nie może wyrzucać WYJĄTKÓW. - James McNellis Dec 22 ' 10 at 15: 24


‡ implementacja operatora przyporządkowania, która wydaje się rozsądna przy przypisywaniu z odrębnego obiektu, może łatwo zawieść dla przypisania własnego. Chociaż może się wydawać niewyobrażalne, że kod klienta spróbuje nawet sam się przydzielić, może można to zrobić stosunkowo łatwo podczas operacji algo na kontenerach, z kodem x = f(x); gdzie f jest (być może tylko dla niektórych gałęzi #ifdef) makro ala #define f(x) x lub funkcją zwracającą odwołanie do x, a nawet (prawdopodobnie nieefektywnym, ale zwięzłym) kodem jak x = c1 ? x * 2 : c2 ? x / 2 : x;). Na przykład:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

Przy samodzielnym przypisaniu, powyższy kod delete x.p_; wskazuje p_na nowo przydzielony region sterty, a następnie próbuje odczytać niezainicjalizowane dane w nim zawarte (nieokreślone zachowanie), jeśli nie robi nic dziwnego, copy próbuje przypisać się do każdego właśnie zniszczonego "T"!


Idiom copy-and-swap może wprowadzać nieefektywności lub ograniczenia ze względu na użycie dodatkowej tymczasowej (gdy parametr operatora jest konstruowany jako kopia):
struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Tutaj, ręcznie napisany Client::operator= Może Sprawdzić, czy *this jest już podłączony do tego samego serwera co rhs (Być może wysyłanie kodu "reset", jeśli jest to użyteczne), podczas gdy podejście copy-and-swap wywoła Konstruktor kopiujący, który prawdopodobnie zostałby napisany w celu otwarcia odrębnego połączenia gniazda, a następnie zamknięcia oryginalnego. Nie tylko może to oznaczać zdalną interakcję z siecią zamiast prostej kopii zmiennych w procesie, ale może również powodować ograniczenia klientów lub serwerów w zasobach gniazd lub połączeniach. (Oczywiście ta klasa ma dość okropny interfejs, ale to już inna sprawa ; - P).

 33
Author: Tony Delroy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-07 06:46:09

Ta odpowiedź jest bardziej jak dodatek i niewielka modyfikacja do powyższych odpowiedzi.

W niektórych wersjach Visual Studio (i ewentualnie innych kompilatorów) jest błąd, który jest naprawdę irytujący i nie ma sensu. Jeśli więc zadeklarujesz / zdefiniujesz swoją swap funkcję w ten sposób:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... kompilator będzie krzyczał na ciebie, gdy wywołasz funkcję swap:

Tutaj wpisz opis obrazka

Ma to coś wspólnego z wywołaniem friend funkcji i this obiektu przekazany jako parametr.


Sposobem obejścia tego problemu jest nie używanie słowa kluczowego friend i ponowne zdefiniowanie funkcji swap:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Tym razem możesz po prostu wywołać swap i przekazać other, czyniąc kompilator szczęśliwym:

Tutaj wpisz opis obrazka


W końcu nie musisz używać funkcji friend do zamiany 2 obiektów. Równie sensowne jest stworzenie swap funkcji member, która ma jeden obiekt other jako parametr.

Masz już dostęp do this obiekt, więc podanie go jako parametru jest technicznie zbędne.

 20
Author: Oleksiy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-04 05:34:49

Chciałbym dodać słowo ostrzeżenia, gdy masz do czynienia z kontenerami świadomymi alokatorów w stylu C++11. Zamiana i przypisanie mają subtelnie różną semantykę.

Dla konkretnej wartości rozważmy kontener std::vector<T, A>, Gdzie A jest pewnym stateful allocator type, A porównamy następujące funkcje:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Celem obu funkcji fs i fm jest nadanie a stanu, który b miał początkowo. Istnieje jednak ukryte pytanie: co się stanie, jeśli a.get_allocator() != b.get_allocator()? Odpowiedź brzmi: to zależy. Napiszmy AT = std::allocator_traits<A>.

  • Jeśli AT::propagate_on_container_move_assignment jest std::true_type, to fm przypisuje alokator a z wartością b.get_allocator(), w przeciwnym razie nie i a nadal używa swojego pierwotnego alokatora. W takim przypadku elementy danych muszą być wymieniane indywidualnie, ponieważ przechowywanie a i b nie jest zgodne.

  • Jeśli AT::propagate_on_container_swap jest std::true_type, to fs zamienia zarówno dane, jak i alokatory w oczekiwanym Moda.

  • Jeśli AT::propagate_on_container_swap jest std::false_type, to potrzebujemy dynamicznego sprawdzenia.

    • jeśli a.get_allocator() == b.get_allocator(), to oba pojemniki używają kompatybilnego magazynu, a Zamiana odbywa się w zwykły sposób.
    • Jednakże, jeśli a.get_allocator() != b.get_allocator(), program ma nieokreślone zachowanie (por. [Pojemnik.wymagania.ogólne / 8].

Wynik jest taki, że zamiana stała się nietrywialną operacją w C++11, gdy tylko kontener zacznie obsługiwać alokatory stateful. Jest to nieco "zaawansowany przypadek użycia", ale nie jest to całkowicie mało prawdopodobne, ponieważ optymalizacje ruchu zwykle stają się interesujące dopiero wtedy, gdy Klasa zarządza zasobem, a pamięć jest jednym z najpopularniejszych zasobów.

 10
Author: Kerrek SB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-24 08:16:06