Podstawy nullptr
std::nullptr_t
jest 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ą nullptr
over 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 lockAndCall
zamiast wielu definicji lockAndCallF1, lockAndCallF2 & lockAndCallF3
dla 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 lockAndCall
miał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 lockAndCall
z 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 NULL
jest zasadniczo taka sama. Gdy NULL
jest przekazywany do lockAndCall
, dla parametru ptr wyprowadzany jest typ całkowity, a błąd typu występuje, gdy ptr
przekazywany 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 nullptr
nie ma problemu. Kiedy nullptr
jest przekazywane lockAndCall
, typ for ptr
jest wydedukowany jako std::nullptr_t
. Gdy ptr
jest przekazywany do f3
, następuje niejawna konwersja z std::nullptr_t
na int*
, ponieważ std::nullptr_t
niejawnie 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
.
int
ivoid *
nie wybieraint
wersji zamiastvoid *
wersji podczas używanianullptr
.