Jakie są zalety korzystania z nullptr?


163

Ten fragment kodu koncepcyjnie robi to samo dla trzech wskaźników (bezpieczna inicjalizacja wskaźnika):

int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;

A więc jakie są zalety przypisywania wskaźników nullptrnad przypisywaniem im wartości NULLlub 0?


39
Po pierwsze, przeciążona funkcja pobiera inti void *nie wybiera intwersji zamiast void *wersji podczas używania nullptr.
chris,

2
Cóż, f(nullptr)różni się od f(NULL). Ale jeśli chodzi o powyższy kod (przypisanie do zmiennej lokalnej), wszystkie trzy wskaźniki są dokładnie takie same. Jedyną zaletą jest czytelność kodu.
balki

2
Jestem za umieszczeniem tego w FAQ, @Prasoon. Dzięki!
sbi

1
NB historycznie nie gwarantuje się, że NULL będzie równe 0, ale tak jak oc C99, w taki sam sposób, w jaki bajt niekoniecznie miał długość 8 bitów, a prawda i fałsz były wartościami zależnymi od architektury. To pytanie skupia się na nullptrale to jest różnica między 0 aNULL
awiebe

Odpowiedzi:


180

Wydaje się, że w tym kodzie nie ma żadnej przewagi. Ale rozważ następujące przeciążone funkcje:

void f(char const *ptr);
void f(int v);

f(NULL);  //which function will be called?

Która funkcja zostanie wywołana? Oczywiście intencją tutaj jest zadzwonić f(char const *), ale w rzeczywistości f(int)zostanie wezwany! To jest duży problem 1 , prawda?

Tak więc rozwiązaniem takich problemów jest użycie nullptr:

f(nullptr); //first function is called

Oczywiście to nie jedyna zaleta nullptr. Oto kolejny:

template<typename T, T *ptr>
struct something{};                     //primary template

template<>
struct something<nullptr_t, nullptr>{};  //partial specialization for nullptr

Ponieważ w szablonie typ nullptrjest wydedukowany jako nullptr_t, więc możesz napisać to:

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument

void f(nullptr_t); //an overload to handle nullptr argument!!!

1. W C ++ NULLjest definiowane jako #define NULL 0, więc jest w zasadzie int, dlatego f(int)jest wywoływane.


1
Jak stwierdził Mehrdad, tego rodzaju przeciążenia są dość rzadkie. Czy są inne istotne zalety nullptr? (Nie, nie jestem wymagający)
Mark Garcia,

2
@MarkGarcia, To może być pomocne: stackoverflow.com/questions/13665349/…
chris

9
Twój przypis wydaje się odwrócony. NULLjest wymagany przez standard, aby mieć typ całkowity, dlatego jest zwykle definiowany jako 0lub 0L. Nie jestem też pewien, czy podoba mi się to nullptr_tprzeciążenie, ponieważ wyłapuje tylko wywołania z nullptr, a nie ze wskaźnikiem zerowym innego typu, jak (void*)0. Ale mogę uwierzyć, że ma on pewne zastosowania, nawet jeśli jedyne, co robi, to poza zdefiniowaniem własnego typu elementu zastępczego o pojedynczej wartości, który oznacza „brak”.
Steve Jessop,

1
Inną zaletą (choć wprawdzie niewielką) może być to, że nullptrma dobrze zdefiniowaną wartość liczbową, podczas gdy stałe wskaźnika zerowego nie. Stała wskaźnika zerowego jest konwertowana na wskaźnik zerowy tego typu (cokolwiek to jest). Wymagane jest, aby dwa wskaźniki zerowe tego samego typu były porównywane identycznie, a konwersja wartości logicznych zamienia wskaźnik o wartości null na false. Nic więcej nie jest wymagane. Dlatego możliwe jest, że kompilator (głupi, ale możliwy) użyje np. 0xabcdef1234Lub innej liczby jako wskaźnika pustego. Z drugiej strony nullptrwymagana jest konwersja na numeryczne zero.
Damon,

1
@DeadMG: Co jest niepoprawne w mojej odpowiedzi? która f(nullptr)nie wywoła zamierzonej funkcji? Motywacji było więcej niż jedna. W nadchodzących latach sami programiści mogą odkryć wiele innych przydatnych rzeczy. Nie można więc powiedzieć, że istnieje tylko jeden prawdziwy Wykorzystanie od nullptr.
Nawaz,

87

C ++ 11 wprowadza nullptr, jest znany jako Nullstała wskaźnika i poprawia bezpieczeństwo typów i rozwiązuje niejednoznaczne sytuacje w przeciwieństwie do istniejącej stałej wskaźnika zerowego zależnej od implementacji NULL. Aby móc zrozumieć zalety nullptr. najpierw musimy zrozumieć, co jest NULLi jakie są problemy z tym związane.


Co to jest NULLdokładnie?

Pre C ++ 11 NULLbył używany do reprezentowania wskaźnika, który nie ma wartości lub wskaźnika, który nie wskazuje na nic prawidłowego. W przeciwieństwie do popularnego pojęcia NULLnie jest słowem kluczowym w C ++ . Jest to identyfikator zdefiniowany w standardowych nagłówkach bibliotek. Krótko mówiąc, nie można używać NULLbez dołączenia niektórych standardowych nagłówków bibliotek. Rozważ przykładowy program :

int main()
{ 
    int *ptr = NULL;
    return 0;
}

Wynik:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

Standard C ++ definiuje NULL jako makro zdefiniowane w implementacji, zdefiniowane w niektórych plikach nagłówkowych bibliotek standardowych. Pochodzenie NULL pochodzi z C, a C ++ odziedziczyło je po C. Standard C zdefiniował NULL jako 0lub (void *)0. Ale w C ++ istnieje subtelna różnica.

C ++ nie może zaakceptować tej specyfikacji w obecnej postaci. W przeciwieństwie do C, C ++ jest językiem silnie typizowanym (C nie wymaga jawnego rzutowania z void*do żadnego typu, podczas gdy C ++ wymusza jawne rzutowanie). To sprawia, że ​​definicja NULL określona przez standard C jest bezużyteczna w wielu wyrażeniach C ++. Na przykład:

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

Gdyby NULL zostało zdefiniowane jako (void *)0, żadne z powyższych wyrażeń nie zadziałałoby.

  • Przypadek 1: nie zostanie skompilowany, ponieważ wymagane jest automatyczne rzutowanie z void *do std::string.
  • Przypadek 2: nie zostanie skompilowany, ponieważ void *wymagane jest rzutowanie wskaźnika z do na funkcję składową.

Tak więc w przeciwieństwie do C, C ++ Standard nakazał zdefiniować NULL jako literał numeryczny 0lub 0L.


Więc jaka jest potrzeba innej stałej wskaźnika zerowego, skoro NULLjuż mamy ?

Chociaż komitet ds. Standardów C ++ wymyślił definicję NULL, która będzie działać dla C ++, ta definicja miała swój własny udział w problemach. NULL działał wystarczająco dobrze w prawie wszystkich scenariuszach, ale nie we wszystkich. Dało zaskakujące i błędne wyniki dla niektórych rzadkich scenariuszy. Na przykład :

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    doSomething(NULL);
    return 0;
}

Wynik:

In Int version

Oczywiście intencją wydaje się być wywołanie wersji, która przyjmuje char*jako argument, ale gdy dane wyjściowe pokazują, funkcja, która przyjmuje intwersję, zostaje wywołana. Dzieje się tak, ponieważ NULL jest literałem numerycznym.

Ponadto, ponieważ jest zdefiniowane w implementacji, czy NULL jest 0 czy 0L, może być wiele nieporozumień w rozwiązywaniu przeciążeń funkcji.

Przykładowy program:

#include <cstddef>

void doSomething(int);
void doSomething(char *);

int main()
{
  doSomething(static_cast <char *>(0));    // Case 1
  doSomething(0);                          // Case 2
  doSomething(NULL)                        // Case 3
}

Analiza powyższego fragmentu:

  • Przypadek 1: połączenia doSomething(char *)zgodnie z oczekiwaniami.
  • Przypadek 2: wywołania, doSomething(int)ale być może char*pożądana była wersja, ponieważ 0JEST również pustym wskaźnikiem.
  • Przypadek 3: Jeśli NULLjest zdefiniowane jako 0, wywołuje, doSomething(int)gdy być może doSomething(char *)było zamierzone, być może powodując błąd logiczny w czasie wykonywania. Jeśli NULLjest zdefiniowane jako 0L, wywołanie jest niejednoznaczne i powoduje błąd kompilacji.

Tak więc, w zależności od implementacji, ten sam kod może dawać różne wyniki, co jest oczywiście niepożądane. Oczywiście komitet normalizacyjny C ++ chciał to naprawić i to jest główna motywacja dla nullptr.


Więc co to jest nullptri jak pozwala uniknąć problemów NULL?

C ++ 11 wprowadza nowe słowo kluczowe, nullptrktóre służy jako stała wskaźnika zerowego. W przeciwieństwie do NULL, jego zachowanie nie jest zdefiniowane w ramach implementacji. Nie jest to makro, ale ma swój własny typ. nullptr ma typ std::nullptr_t. C ++ 11 odpowiednio definiuje właściwości nullptr, aby uniknąć wad NULL. Podsumowując jego właściwości:

Właściwość 1: ma swój własny typ std::nullptr_ti
Właściwość 2: jest niejawnie konwertowalna i porównywalna z dowolnym typem wskaźnika lub typem wskaźnika do elementu członkowskiego, ale
Właściwość 3: nie jest niejawnie konwertowalna ani porównywalna z typami całkowitymi, z wyjątkiem bool.

Rozważmy następujący przykład:

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    char *pc = nullptr;      // Case 1
    int i = nullptr;         // Case 2
    bool flag = nullptr;     // Case 3

    doSomething(nullptr);    // Case 4
    return 0;
}

W powyższym programie

  • Przypadek 1: OK - Właściwość 2
  • Przypadek 2: Nie OK - Właściwość 3
  • Przypadek 3: OK - Właściwość 3
  • Przypadek 4: brak nieporozumień - char *wersja połączeń , właściwość 2 i 3

W ten sposób wprowadzenie nullptr pozwala uniknąć wszystkich problemów starego dobrego NULL.

Jak i gdzie powinieneś używać nullptr?

Praktyczna zasada dotycząca C ++ 11 jest taka, że ​​po prostu zacznij używać nullptrzawsze, gdy w przeszłości używałbyś NULL.


Standardowe odniesienia:

C ++ 11 Standard: C.3.2.4 Macro NULL
C ++ 11 Standard: 18.2 Typy
C ++ 11 Standard: 4.10 Konwersje wskaźników
C99 Standard: 6.3.2.3 Wskaźniki


Ćwiczę już twoją ostatnią radę, odkąd się nullptrzorientowałem, chociaż nie wiedziałem, jaką różnicę robi z moim kodem. Dzięki za świetną odpowiedź, a zwłaszcza za wysiłek. Dało mi dużo światła na ten temat.
Mark Garcia,

„w niektórych standardowych plikach nagłówkowych bibliotek”. -> czemu po prostu nie napisać „cstddef” od początku?
mxmlnkn

Dlaczego powinniśmy zezwolić na zamianę nullptr na typ bool? Czy mógłbyś wyjaśnić więcej?
Robert Wang

... był używany do reprezentowania wskaźnika, który nie ma wartości ... Zmienne zawsze mają wartość. Może to być szum, 0xccccc....ale zmienna bez wartości jest wewnętrzną sprzecznością.
Zapisz

„Przypadek 3: OK - Właściwość 3” (wiersz bool flag = nullptr;). Nie, nie OK, podczas kompilacji z g ++ 6 error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

Prawdziwą motywacją jest tutaj doskonała spedycja .

Rozważać:

void f(int* p);
template<typename T> void forward(T&& t) {
    f(std::forward<T>(t));
}
int main() {
    forward(0); // FAIL
}

Mówiąc najprościej, 0 jest wartością specjalną , ale wartości nie mogą być propagowane przez system - tylko typy mogą. Funkcje przekazujące są niezbędne i 0 nie może sobie z nimi poradzić. Stąd absolutnie konieczne było wprowadzenie nullptr, gdzie typ jest tym, co jest szczególne, a typ rzeczywiście może się rozmnażać. W rzeczywistości zespół MSVC musiał wprowadzić nullptrprzed terminem po zaimplementowaniu odniesień rvalue, a następnie odkrył tę pułapkę dla siebie.

Jest kilka innych narożnych przypadków, w których nullptrżycie może być łatwiejsze - ale nie jest to sprawa podstawowa, ponieważ odlew może rozwiązać te problemy. Rozważać

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

Wywołuje dwa oddzielne przeciążenia. Ponadto rozważ

void f(int*);
void f(long*);
int main() { f(0); }

To jest niejednoznaczne. Ale dzięki nullptr możesz zapewnić

void f(std::nullptr_t)
int main() { f(nullptr); }

7
Zabawny. Połowa odpowiedzi jest taka sama, jak dwie pozostałe odpowiedzi, które według Ciebie są odpowiedziami „całkiem niepoprawnymi” !!!
Nawaz

Problem przekazywania można również rozwiązać za pomocą obsady. forward((int*)0)Pracuje. Czy coś mi brakuje?
jcsahnwaldt Reinstate Monica

5

Podstawy nullptr

std::nullptr_tjest typem literału pustego wskaźnika, nullptr. Jest to prvalue / rvalue typu std::nullptr_t. Istnieją niejawne konwersje z nullptr na wartość wskaźnika o wartości null dowolnego typu wskaźnika.

Literał 0 to int, a nie wskaźnik. Jeśli C ++ znajdzie 0 w kontekście, w którym można użyć tylko wskaźnika, niechętnie zinterpretuje 0 jako wskaźnik zerowy, ale jest to pozycja rezerwowa. Podstawową zasadą C ++ jest to, że 0 jest liczbą int, a nie wskaźnikiem.

Zaleta 1 - Usuń niejednoznaczność podczas przeciążania typów wskaźników i typów całkowitych

W C ++ 98 główną tego konsekwencją było to, że przeciążanie typów wskaźnikowych i całkowitych mogło prowadzić do niespodzianek. Przekazywanie 0 lub NULL do takich przeciążeń nigdy nie jest nazywane przeciążeniem wskaźnika:

   void fun(int); // two overloads of fun
    void fun(void*);
    fun(0); // calls f(int), not fun(void*)
    fun(NULL); // might not compile, but typically calls fun(int). Never calls fun(void*)

Interesującą rzeczą w tym wywołaniu jest sprzeczność między pozornym znaczeniem kodu źródłowego („Wywołuję zabawę z NULL - wskaźnikiem zerowym”) a jego rzeczywistym znaczeniem („Wzywam zabawę jakąś liczbą całkowitą - nie zerową wskaźnik").

Zaletą nullptr jest to, że nie ma typu całkowitego. Wywołanie przeciążonej funkcji fun z nullptr wywołuje void * overload (tj. Przeciążenie wskaźnika), ponieważ nullptr nie może być postrzegane jako coś integralnego:

fun(nullptr); // calls fun(void*) overload 

Użycie nullptr zamiast 0 lub NULL pozwala uniknąć niespodzianek związanych z rozwiązywaniem przeciążenia.

Kolejną zaletą nullptrover NULL(0)podczas korzystania auto na typ zwracany

Na przykład załóżmy, że napotkasz to w bazie kodu:

auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}

Jeśli nie wiesz (lub nie możesz łatwo dowiedzieć się), co zwraca findRecord, może nie być jasne, czy wynik jest typem wskaźnikowym, czy typem całkowitym. W końcu 0 (na podstawie jakiego wyniku jest testowany) może pójść w obie strony. Z drugiej strony, jeśli zobaczysz następujące informacje,

auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}

nie ma dwuznaczności: wynik musi być typem wskaźnikowym.

Zaleta 3

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

void lockAndCallF1()
{
        MuxtexGuard g(f1m); // lock mutex for f1
        auto result = f1(static_cast<int>(0)); // pass 0 as null ptr to f1
        cout<< result<<endl;
}

void lockAndCallF2()
{
        MuxtexGuard g(f2m); // lock mutex for f2
        auto result = f2(static_cast<int>(NULL)); // pass NULL as null ptr to f2
        cout<< result<<endl;
}
void lockAndCallF3()
{
        MuxtexGuard g(f3m); // lock mutex for f2
        auto result = f3(nullptr);// pass nullptr as null ptr to f3 
        cout<< result<<endl;
} // unlock mutex
int main()
{
        lockAndCallF1();
        lockAndCallF2();
        lockAndCallF3();
        return 0;
}

Powyższy program jest kompilowany i wykonywany pomyślnie, ale lockAndCallF1, lockAndCallF2 i lockAndCallF3 mają nadmiarowy kod. Szkoda pisać taki kod, jeśli możemy napisać dla nich szablon lockAndCallF1, lockAndCallF2 & lockAndCallF3. Więc można to uogólnić za pomocą szablonu. Napisałem funkcję szablonu lockAndCallzamiast wielu definicji lockAndCallF1, lockAndCallF2 & lockAndCallF3dla nadmiarowego kodu.

Kod jest ponownie rozkładany, jak poniżej:

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr)
{
        MuxtexGuard g(mutex);
        return func(ptr);
}
int main()
{
        auto result1 = lockAndCall(f1, f1m, 0); //compilation failed 
        //do something
        auto result2 = lockAndCall(f2, f2m, NULL); //compilation failed
        //do something
        auto result3 = lockAndCall(f3, f3m, nullptr);
        //do something
        return 0;
}

Szczegółowa analiza, dlaczego kompilacja nie powiodła się dla, a lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)nie dlalockAndCall(f3, f3m, nullptr)

Dlaczego kompilacja lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)nie powiodła się?

Problem polega na tym, że po przekazaniu wartości 0 do lockAndCall rozpoczyna się odliczenie typu szablonu, aby określić jego typ. Typ 0 to int, więc jest to typ parametru ptr wewnątrz instancji tego wywołania lockAndCall. Niestety oznacza to, że w wywołaniu funkcji func wewnątrz lockAndCall przekazywana jest wartość int, która nie jest zgodna z oczekiwanym std::shared_ptr<int>parametrem f1. Wartość 0 przekazana w wywołaniu do lockAndCallmiała reprezentować wskaźnik zerowy, ale faktycznie przekazano wartość int. Próba przekazania tego int do f1 jako a std::shared_ptr<int>jest błędem typu. Wywołanie lockAndCallz wartością 0 kończy się niepowodzeniem, ponieważ wewnątrz szablonu int jest przekazywany do funkcji, która wymaga rozszerzenia std::shared_ptr<int>.

Analiza rozmowy z udziałem NULLjest zasadniczo taka sama. Gdy NULLjest przekazywany do lockAndCall, dla parametru ptr wyprowadzany jest typ całkowity, a błąd typu występuje, gdy ptrprzekazywany jest typ typu int lub typu int f2, który oczekuje, że zostanie pobrany plik std::unique_ptr<int>.

W przeciwieństwie do połączenia z udziałem nullptrnie ma problemu. Kiedy nullptrjest przekazywane lockAndCall, typ for ptrjest wydedukowany jako std::nullptr_t. Gdy ptrjest przekazywany do f3, następuje niejawna konwersja z std::nullptr_tna int*, ponieważ std::nullptr_tniejawnie konwertuje do wszystkich typów wskaźników.

Zalecane jest, aby zawsze, gdy chcesz odwołać się do pustego wskaźnika, użyj nullptr, a nie 0 lub NULL.


4

Nie ma bezpośredniej korzyści z nullptrpokazania przykładów.
Ale rozważ sytuację, w której masz 2 funkcje o tej samej nazwie; 1 bierze, inta kolejnyint*

void foo(int);
void foo(int*);

Jeśli chcesz zadzwonić foo(int*), przekazując NULL, sposób jest następujący:

foo((int*)0); // note: foo(NULL) means foo(0)

nullptrsprawia, że ​​jest to łatwiejsze i bardziej intuicyjne :

foo(nullptr);

Dodatkowe łącze ze strony internetowej Bjarne'a.
Nieistotne, ale w C ++ 11 uwaga dodatkowa:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)

3
Dla odniesienia decltype(nullptr)jest std::nullptr_t.
chris,

2
@MarkGarcia, o ile wiem, to pełnowymiarowy typ.
chris,

5
@MarkGarcia, to interesujące pytanie. cppreference posiada: typedef decltype(nullptr) nullptr_t;. Chyba mogę zajrzeć do standardu. Ach, znalazłem to: Uwaga: std :: nullptr_t to odrębny typ, który nie jest ani typem wskaźnikowym, ani wskaźnikiem do typu składowego; raczej wartość prvalue tego typu jest stałą wskaźnika o wartości null i można ją przekonwertować na wartość wskaźnika o wartości null lub wartość wskaźnika do elementu członkowskiego o wartości null.
chris

2
@DeadMG: Było więcej niż jedna motywacja. W nadchodzących latach sami programiści mogą odkryć wiele innych przydatnych rzeczy. Nie można więc powiedzieć, że istnieje tylko jeden prawdziwy Wykorzystanie od nullptr.
Nawaz,

2
@DeadMG: Ale powiedziałeś, że ta odpowiedź jest „całkiem niepoprawna” tylko dlatego, że nie mówi o „prawdziwej motywacji” , o której mówiłeś w swojej odpowiedzi. Nie tylko, że ta odpowiedź (i moja również) otrzymała od Ciebie negatywną opinię.
Nawaz,

4

Jak już powiedzieli inni, jego podstawową zaletą są przeciążenia. I chociaż intprzeciążenia jawne i wskaźnikowe mogą być rzadkie, rozważ standardowe funkcje biblioteczne, takie jak std::fill(które ugryzło mnie więcej niż raz w C ++ 03):

MyClass *arr[4];
std::fill_n(arr, 4, NULL);

Nie kompilacji: Cannot convert int to MyClass*.


2

IMO ważniejsze niż te przeciążenia: w głęboko zagnieżdżonych konstrukcjach szablonów trudno nie stracić orientacji w typach, a podawanie wyraźnych podpisów jest nie lada wyzwaniem. Tak więc w przypadku wszystkiego, czego używasz, im bardziej precyzyjnie koncentrujesz się na zamierzonym celu, tym lepiej, zmniejszy to potrzebę stosowania jawnych podpisów i pozwoli kompilatorowi na generowanie bardziej wnikliwych komunikatów o błędach, gdy coś pójdzie nie tak.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.