Apa keuntungan menggunakan nullptr?


163

Sepotong kode ini secara konseptual melakukan hal yang sama untuk tiga pointer (inisialisasi pointer aman):

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

Jadi, apa keuntungan dari menetapkan pointer nullptrlebih dari memberi mereka nilai NULLatau 0?


39
Untuk satu hal, fungsi kelebihan mengambil intdan void *tidak akan memilih intversi daripada void *versi saat menggunakan nullptr.
chris

2
Yah f(nullptr)berbeda dari f(NULL). Tetapi sejauh menyangkut kode di atas (menugaskan ke variabel lokal), ketiga pointer persis sama. Satu-satunya keuntungan adalah keterbacaan kode.
balki

2
Saya mendukung untuk menjadikan ini sebagai FAQ, @Prasoon. Terima kasih!
sbi

1
NB NULL secara historis tidak dijamin menjadi 0, tetapi sebagai oc C99, dalam cara yang sama byte tidak selalu 8 bit panjang dan benar dan salah adalah nilai-nilai tergantung arsitektur. Pertanyaan ini berfokus pada nullptrbutthat perbedaan antara 0 danNULL
awiebe

Jawaban:


180

Dalam kode itu, sepertinya tidak ada keuntungan. Tetapi pertimbangkan fungsi kelebihan beban berikut:

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

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

Fungsi mana yang akan dipanggil? Tentu saja, maksudnya di sini adalah untuk menelepon f(char const *), tetapi pada kenyataannya f(int)akan dipanggil! Itu adalah masalah besar 1 , bukan?

Jadi, solusi untuk masalah tersebut adalah dengan menggunakan nullptr:

f(nullptr); //first function is called

Tentu saja, itu bukan satu-satunya keuntungan nullptr. Ini yang lain:

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

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

Karena dalam templat, jenis nullptrdisimpulkan sebagai nullptr_t, sehingga Anda dapat menulis ini:

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

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

1. Dalam C ++, NULLdidefinisikan sebagai #define NULL 0, jadi pada dasarnya int, itulah mengapa f(int)disebut.


1
Seperti yang dikatakan Mehrdad, kelebihan seperti ini jarang terjadi. Apakah ada manfaat lain yang relevan nullptr? (Tidak. Saya tidak menuntut)
Mark Garcia

2
@MarkGarcia, Ini mungkin membantu: stackoverflow.com/questions/13665349/…
chris

9
Catatan kaki Anda sepertinya mundur. NULLdiperlukan oleh standar untuk memiliki tipe integral, dan itulah sebabnya itu biasanya didefinisikan sebagai 0atau 0L. Juga saya tidak yakin saya suka itu nullptr_tberlebihan, karena hanya menangkap panggilan dengan nullptr, bukan dengan pointer nol dari jenis yang berbeda, seperti (void*)0. Tapi saya percaya itu memiliki beberapa kegunaan, bahkan jika itu hanya menyelamatkan Anda mendefinisikan jenis tempat-nilai tunggal Anda sendiri berarti "tidak ada".
Steve Jessop

1
Keuntungan lain (meskipun diakui kecil) mungkin adalah yang nullptrmemiliki nilai numerik yang jelas sedangkan konstanta penunjuk nol tidak. Konstanta penunjuk nol dikonversi menjadi penunjuk nol jenis itu (apa pun itu). Diperlukan bahwa dua pointer nol dari jenis yang sama membandingkan secara identik, dan konversi boolean mengubah pointer null menjadi false. Tidak ada lagi yang dibutuhkan. Oleh karena itu, adalah mungkin bagi kompiler (konyol, tetapi mungkin) untuk menggunakan mis 0xabcdef1234atau nomor lain untuk penunjuk nol. Di sisi lain, nullptrdiperlukan untuk mengkonversi ke angka nol.
Damon

1
@DeadMG: Apa yang salah dalam jawaban saya? yang f(nullptr)tidak akan memanggil fungsi yang dimaksud? Ada lebih dari satu motivasi. Banyak hal berguna lainnya dapat ditemukan oleh programmer sendiri di tahun-tahun mendatang. Jadi Anda tidak bisa mengatakan bahwa ada hanya satu penggunaan yang benar dari nullptr.
Nawaz

87

C ++ 11 memperkenalkan nullptr, ini dikenal sebagai Nullpointer konstan dan ini meningkatkan keamanan jenis dan menyelesaikan situasi ambigu tidak seperti implementasi yang ada tergantung konstan pointer nol NULL. Untuk bisa memahami kelebihan dari nullptr. pertama-tama kita perlu memahami apa NULLdan apa masalah yang terkait dengannya.


Apa NULLsebenarnya?

Pre C ++ 11 NULLdigunakan untuk mewakili pointer yang tidak memiliki nilai atau pointer yang tidak menunjuk ke sesuatu yang valid. Berlawanan dengan gagasan populer NULLbukanlah kata kunci dalam C ++ . Ini adalah pengidentifikasi yang didefinisikan dalam header perpustakaan standar. Singkatnya, Anda tidak dapat menggunakan NULLtanpa menyertakan beberapa header perpustakaan standar. Pertimbangkan program Sampel :

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

Keluaran:

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

Standar C ++ mendefinisikan NULL sebagai implementasi makro didefinisikan didefinisikan dalam file header perpustakaan standar tertentu. Asal usul NULL adalah dari C dan C ++ mewarisinya dari C. Standar C mendefinisikan NULL sebagai 0atau (void *)0. Tetapi dalam C ++ ada perbedaan yang halus.

C ++ tidak dapat menerima spesifikasi ini apa adanya. Tidak seperti C, C ++ adalah bahasa yang sangat diketik (C tidak memerlukan pemeran eksplisit dari void*jenis apa pun, sementara C ++ mewajibkan pemeran eksplisit). Ini membuat definisi NULL yang ditentukan oleh standar C tidak berguna dalam banyak ekspresi C ++. Sebagai contoh:

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

Jika NULL didefinisikan sebagai (void *)0, tak satu pun dari ekspresi di atas akan berfungsi.

  • Kasus 1: Tidak akan dikompilasi karena pemain otomatis diperlukan dari void *ke std::string.
  • Kasus 2: Tidak akan dikompilasi karena void *fungsi dari penunjuk ke fungsi anggota diperlukan.

Jadi tidak seperti C, C ++ Standard diamanatkan untuk mendefinisikan NULL sebagai numerik literal 0atau 0L.


Jadi, apa perlunya konstanta null pointer lain ketika kita NULLsudah memilikinya ?

Meskipun komite Standar C ++ menghasilkan definisi NULL yang akan bekerja untuk C ++, definisi ini memiliki masalah yang sama. NULL bekerja cukup baik untuk hampir semua skenario tetapi tidak semua. Ini memberikan hasil yang mengejutkan dan keliru untuk skenario langka tertentu. Sebagai contoh :

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

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

Keluaran:

In Int version

Jelas, maksudnya adalah untuk memanggil versi yang mengambil char*sebagai argumen, tetapi sebagai output menunjukkan fungsi yang mengambil intversi dipanggil. Ini karena NULL adalah literal numerik.

Selain itu, karena ini adalah implementasi yang ditentukan apakah NULL adalah 0 atau 0L, ada banyak kebingungan dalam resolusi fungsi yang berlebihan.

Program Sampel:

#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
}

Menganalisa cuplikan di atas:

  • Kasus 1: panggilan doSomething(char *)seperti yang diharapkan.
  • Kasus 2: panggilan doSomething(int)tetapi mungkin char*versi diinginkan karena 0IS juga merupakan pointer nol.
  • Kasus 3: Jika NULLdidefinisikan sebagai 0, panggilan doSomething(int)saat mungkin doSomething(char *)dimaksudkan, mungkin mengakibatkan kesalahan logika saat runtime. Jika NULLdidefinisikan sebagai 0L, panggilan tidak jelas dan menghasilkan kesalahan kompilasi.

Jadi, tergantung pada implementasi, kode yang sama dapat memberikan berbagai hasil, yang jelas tidak diinginkan. Secara alami, komite standar C ++ ingin memperbaiki ini dan itu adalah motivasi utama untuk nullptr.


Jadi apa nullptrdan bagaimana cara menghindari masalah NULL?

C ++ 11 memperkenalkan kata kunci baru nullptruntuk dijadikan konstanta penunjuk nol. Tidak seperti NULL, perilakunya tidak ditentukan oleh implementasi. Ini bukan makro tetapi memiliki tipe sendiri. nullptr memiliki tipe std::nullptr_t. C ++ 11 mendefinisikan properti untuk nullptr secara tepat untuk menghindari kerugian NULL. Untuk meringkas propertinya:

Properti 1: ia memiliki tipe sendiri std::nullptr_t, dan
Properti 2: itu secara implisit dapat dikonversi dan sebanding dengan semua tipe pointer atau tipe pointer-ke-anggota, tetapi
Properti 3: itu tidak secara implisit dapat dikonversi atau sebanding dengan tipe integral, kecuali untuk bool.

Perhatikan contoh berikut:

#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;
}

Dalam program di atas,

  • Kasus 1: Oke - Properti 2
  • Kasus 2: Tidak Oke - Properti 3
  • Kasus 3: Oke - Properti 3
  • Kasus 4: Tanpa kebingungan - char *Versi panggilan , Properti 2 & 3

Dengan demikian pengenalan nullptr menghindari semua masalah NULL tua yang baik.

Bagaimana dan di mana Anda harus menggunakan nullptr?

Aturan praktis untuk C ++ 11 adalah mulai menggunakan nullptrkapan saja Anda akan menggunakan NULL di masa lalu.


Referensi Standar:

C ++ 11 Standar: C.3.2.4 Makro NULL
C ++ 11 Standar: 18.2 Jenis
C ++ 11 Standar: 4.10 Konversi pointer
Standar C99: 6.3.2.3 Pointer


Saya sudah menjalankan saran terakhir Anda sejak saya tahu nullptr, meskipun saya tidak tahu apa bedanya kode saya. Terima kasih atas jawaban yang luar biasa dan terutama untuk usahanya. Memberi saya banyak informasi tentang topik ini.
Mark Garcia

"dalam file header perpustakaan standar tertentu." -> mengapa tidak hanya menulis "cstddef" dari awal?
mxmlnkn

Mengapa kita harus membiarkan nullptr dapat dikonversi menjadi tipe bool? Bisakah Anda menjelaskan lebih lanjut?
Robert Wang

... digunakan untuk mewakili pointer yang tidak memiliki nilai ... Variabel selalu memiliki nilai. Mungkin berisik, atau 0xccccc...., tetapi, variabel kurang-nilai merupakan kontradiksi yang melekat.
3Dave

"Kasus 3: OK - Properti 3" (baris bool flag = nullptr;). Tidak, tidak OK, saya mendapatkan kesalahan berikut pada waktu kompilasi dengan g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

Motivasi sesungguhnya di sini adalah penerusan yang sempurna .

Mempertimbangkan:

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

Sederhananya, 0 adalah nilai khusus , tetapi nilai tidak dapat disebarkan melalui tipe sistem saja. Fungsi penerusan sangat penting, dan 0 tidak dapat mengatasinya. Oleh karena itu, mutlak diperlukan untuk memperkenalkan nullptr, di mana tipenya adalah apa yang spesial, dan tipenya memang bisa diperbanyak. Bahkan, tim MSVC harus memperkenalkan nullptrlebih awal dari jadwal setelah mereka menerapkan referensi nilai dan kemudian menemukan perangkap ini untuk diri mereka sendiri.

Ada beberapa kasus sudut lain di mana nullptrdapat membuat hidup lebih mudah - tetapi itu bukan kasus inti, karena para pemain dapat memecahkan masalah ini. Mempertimbangkan

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

Panggilan dua kelebihan beban yang terpisah. Selain itu, pertimbangkan

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

Ini ambigu. Tapi, dengan nullptr, Anda bisa menyediakan

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

7
Lucu. Setengah dari jawaban itu sama dengan dua jawaban lain yang menurut Anda adalah jawaban yang "sangat salah" !!!
Nawaz

Masalah penerusan juga bisa diselesaikan dengan pemain. forward((int*)0)bekerja. Apakah saya melewatkan sesuatu?
jcsahnwaldt Reinstate Monica

5

Dasar-dasar nullptr

std::nullptr_tadalah tipe dari null pointer literal, nullptr. Ini adalah nilai / nilai jenis std::nullptr_t. Ada konversi implisit dari nilai nullptr ke null dari semua jenis pointer.

0 literal adalah int, bukan pointer. Jika C ++ menemukan dirinya melihat 0 dalam konteks di mana hanya sebuah pointer dapat digunakan, itu akan dengan enggan menafsirkan 0 sebagai pointer nol, tetapi itu adalah posisi mundur. Kebijakan utama C ++ adalah 0 adalah int, bukan pointer.

Keuntungan 1 - Hapus ambiguitas saat overloading pada pointer dan tipe integral

Dalam C + + 98, implikasi utama dari ini adalah bahwa kelebihan pada pointer dan tipe integral dapat menyebabkan kejutan. Melewati 0 atau NULL ke kelebihan seperti itu tidak pernah disebut kelebihan pointer:

   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*)

Hal yang menarik tentang panggilan itu adalah kontradiksi antara makna yang tampak dari kode sumber (“Saya menyebut kesenangan dengan NULL-the null pointer”) dan artinya yang sebenarnya (“Saya menyebut kesenangan dengan semacam integer — bukan nol pointer ").

Keuntungan nullptr adalah tidak memiliki tipe integral. Memanggil fungsi overload menyenangkan dengan nullptr memanggil void * overload (mis., Pointer kelebihan beban), karena nullptr tidak dapat dilihat sebagai sesuatu yang tidak terpisahkan:

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

Menggunakan nullptr bukan 0 atau NULL sehingga menghindari kejutan resolusi kelebihan beban.

Keuntungan lain dari nullptrover NULL(0)ketika menggunakan otomatis untuk tipe pengembalian

Misalnya, anggap Anda menjumpai ini dalam basis kode:

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

Jika Anda tidak tahu (atau tidak bisa dengan mudah menemukan) apa yang ditemukan olehRecord, mungkin tidak jelas apakah hasilnya adalah tipe pointer atau tipe integral. Lagi pula, 0 (hasil apa yang diuji terhadap) bisa jalan baik. Jika Anda melihat yang berikut, di sisi lain,

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

tidak ada ambiguitas: hasil harus berupa tipe pointer.

Keuntungan 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;
}

Kompilasi program di atas dan dijalankan dengan sukses tetapi lockAndCallF1, lockAndCallF2 & lockAndCallF3 memiliki kode yang berlebihan. Sangat disayangkan untuk menulis kode seperti ini jika kita dapat menulis template untuk semua ini lockAndCallF1, lockAndCallF2 & lockAndCallF3. Sehingga bisa digeneralisasi dengan template. Saya telah menulis fungsi template lockAndCallalih-alih beberapa definisi lockAndCallF1, lockAndCallF2 & lockAndCallF3untuk kode redundan.

Kode difaktorkan ulang sebagai berikut:

#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;
}

Analisis detail mengapa kompilasi gagal karena lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)tidak untuklockAndCall(f3, f3m, nullptr)

Mengapa kompilasi lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)gagal?

Masalahnya adalah bahwa ketika 0 diteruskan ke lockAndCall, pengurangan tipe templat menendang untuk mengetahui jenisnya. Tipe 0 adalah int, jadi itulah tipe ptr parameter di dalam instantiasi panggilan ini ke lockAndCall. Sayangnya, ini berarti bahwa dalam panggilan ke func di dalam lockAndCall, sebuah int dikirimkan, dan itu tidak kompatibel dengan std::shared_ptr<int>parameter yang f1diharapkan. 0 yang diteruskan dalam panggilan ke lockAndCalldimaksudkan untuk mewakili pointer nol, tetapi yang benar-benar diteruskan adalah int. Mencoba meneruskan int ini ke f1 karena std::shared_ptr<int>merupakan jenis kesalahan. Panggilan lockAndCalldengan 0 gagal karena di dalam templat, int dikirimkan ke fungsi yang memerlukan a std::shared_ptr<int>.

Analisis untuk panggilan yang terlibat NULLpada dasarnya sama. Ketika NULLditeruskan ke lockAndCall, tipe integral disimpulkan untuk parameter ptr, dan kesalahan tipe terjadi ketika ptr— tipe int atau int-like — diteruskan ke f2, yang mengharapkan mendapatkan a std::unique_ptr<int>.

Sebaliknya, panggilan yang terlibat nullptrtidak memiliki masalah. Ketika nullptrditeruskan ke lockAndCall, tipe untuk ptrdideduksi menjadi std::nullptr_t. Ketika ptrditeruskan ke f3, ada konversi implisit dari std::nullptr_tke int*, karena std::nullptr_tsecara implisit mengkonversi ke semua jenis pointer.

Dianjurkan, Setiap kali Anda ingin merujuk ke pointer nol, gunakan nullptr, bukan 0 atau NULL.


4

Tidak ada keuntungan langsung dari nullptrcara Anda menunjukkan contoh.
Tetapi pertimbangkan situasi di mana Anda memiliki 2 fungsi dengan nama yang sama; 1 mengambil intdan satu lagiint*

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

Jika Anda ingin menelepon foo(int*)dengan mengirimkan NULL, maka caranya adalah:

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

nullptrmembuatnya lebih mudah dan intuitif :

foo(nullptr);

Tautan tambahan dari halaman web Bjarne.
Tidak relevan tetapi pada catatan sisi C ++ 11:

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

3
Untuk referensi, decltype(nullptr)adalah std::nullptr_t.
chris

2
@MarkGarcia, Ini adalah tipe full-blown sejauh yang saya tahu.
chris

5
@MarkGarcia, Ini pertanyaan yang menarik. cppreference memiliki: typedef decltype(nullptr) nullptr_t;. Saya kira saya bisa melihat dalam standar. Ah, menemukannya: Catatan: std :: nullptr_t adalah tipe berbeda yang bukan tipe pointer atau pointer ke tipe anggota; sebaliknya, nilai awal dari jenis ini adalah konstanta penunjuk nol dan dapat dikonversi ke nilai penunjuk nol atau nilai penunjuk anggota nol.
chris

2
@DeadMG: Ada lebih dari satu motivasi. Banyak hal berguna lainnya dapat ditemukan oleh programmer sendiri di tahun-tahun mendatang. Jadi Anda tidak bisa mengatakan bahwa ada hanya satu penggunaan yang benar dari nullptr.
Nawaz

2
@DeadMG: Tapi Anda mengatakan jawaban ini "sangat tidak benar" hanya karena itu tidak berbicara tentang "motivasi sejati" yang Anda bicarakan dalam jawaban Anda. Tidak hanya jawaban ini (dan juga jawaban saya) menerima downvote dari Anda.
Nawaz

4

Seperti yang sudah dikatakan orang lain, keunggulan utamanya terletak pada kelebihan beban. Dan sementara intoverload eksplisit vs pointer bisa jarang terjadi, pertimbangkan fungsi-fungsi pustaka standar seperti std::fill(yang telah menggigit saya lebih dari sekali di C ++ 03):

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

Tidak kompilasi: Cannot convert int to MyClass*.


2

IMO lebih penting daripada masalah kelebihan: dalam konstruksi templat bersarang mendalam, sulit untuk tidak kehilangan jejak jenisnya, dan memberikan tanda tangan eksplisit adalah upaya yang cukup. Jadi untuk semua yang Anda gunakan, semakin tepat fokus ke tujuan yang dimaksud, semakin baik, itu akan mengurangi kebutuhan untuk tanda tangan eksplisit dan memungkinkan kompiler untuk menghasilkan pesan kesalahan yang lebih berwawasan ketika terjadi kesalahan.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.