ข้อดีของการใช้ nullptr คืออะไร


163

ชิ้นส่วนของรหัสนี้แนวคิดจะเป็นสิ่งเดียวกันสำหรับสามตัวชี้ (เริ่มต้นชี้ปลอดภัย):

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

ดังนั้นสิ่งที่เป็นข้อดีของการกำหนดตัวชี้nullptrมากกว่ากำหนดให้ค่าNULLหรือ0?


39
สำหรับสิ่งหนึ่งที่ฟังก์ชั่นมากเกินไปการintและvoid *จะไม่เลือกintรุ่นมากกว่ารุ่นเมื่อใช้void * nullptr
chris

2
ดีแตกต่างจากf(nullptr) f(NULL)แต่เท่าที่เกี่ยวข้องกับรหัสข้างต้น (กำหนดให้กับตัวแปรท้องถิ่น) พอยน์เตอร์ทั้งสามตัวจะเหมือนกันทุกประการ ข้อได้เปรียบเพียงอย่างเดียวคือการอ่านรหัสได้
balki

2
ฉันชอบที่จะทำให้เป็นคำถามที่พบบ่อย @Prasoon ขอบคุณ!
sbi

1
NB NULL ไม่สามารถรับประกันได้ว่าจะเป็น 0 แต่เป็น oc C99 ในลักษณะเดียวกับที่ไบต์ไม่จำเป็นต้องมีความยาว 8 บิตและจริงและเท็จเป็นค่าที่ขึ้นอยู่กับสถาปัตยกรรม คำถามนี้เน้นที่nullptrความแตกต่างระหว่าง 0 และNULL
awiebe

คำตอบ:


180

ในรหัสนั้นดูเหมือนจะไม่ได้เปรียบ แต่ให้พิจารณาฟังก์ชั่นโอเวอร์โหลดต่อไปนี้:

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

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

ฟังก์ชั่นใดจะถูกเรียก? แน่นอนความตั้งใจที่นี่คือการโทรf(char const *)แต่ในความเป็นจริงf(int)จะถูกเรียก! นั่นเป็นปัญหาใหญ่1ใช่มั้ย

ดังนั้นวิธีแก้ไขปัญหาดังกล่าวคือการใช้nullptr:

f(nullptr); //first function is called

nullptrแน่นอนว่าไม่ได้เป็นประโยชน์เฉพาะของ นี่คืออีก:

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

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

เนื่องจากในเทมเพลตประเภทของnullptrจะถูกอนุมานเป็นnullptr_tดังนั้นคุณจึงสามารถเขียนสิ่งนี้ได้:

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

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

1. ใน C ++ NULLหมายถึง#define NULL 0ดังนั้นโดยพื้นฐานintแล้วนั่นคือเหตุผลที่f(int)เรียกว่า


1
อย่างที่ Mehrdad ได้กล่าวไว้การโอเวอร์โหลดประเภทนี้ค่อนข้างหายาก มีข้อได้เปรียบอื่น ๆ ที่เกี่ยวข้องnullptrหรือไม่ (ไม่ใช่ฉันไม่ต้องการ)
Mark Garcia

2
@ MarkGarcia สิ่งนี้อาจมีประโยชน์: stackoverflow.com/questions/13665349/…
chris

9
เชิงอรรถของคุณดูย้อนหลัง NULLถูกต้องตามมาตรฐานที่จะมีชนิดหนึ่งและนั่นเป็นเหตุผลที่มันมักจะเป็นที่กำหนดไว้หรือ0 0Lนอกจากนี้ผมไม่แน่ใจว่าผมต้องการที่nullptr_tเกินเพราะมันจับเฉพาะสายที่มีไม่ได้กับตัวชี้โมฆะของประเภทที่แตกต่างกันเช่นnullptr (void*)0แต่ฉันเชื่อได้ว่ามันมีประโยชน์บางอย่างแม้ว่ามันจะช่วยให้คุณกำหนดประเภทเจ้าของสถานที่ที่มีค่าเดียวของคุณเองเพื่อหมายถึง "ไม่มี"
Steve Jessop

1
ข้อดีอีกข้อ (แม้ว่าจะเป็นที่ยอมรับน้อย) อาจเป็นไปได้ว่าnullptrมีค่าตัวเลขที่กำหนดไว้อย่างดีในขณะที่ค่าคงที่ตัวชี้โมฆะไม่ทำงาน ค่าคงที่ตัวชี้โมฆะจะถูกแปลงเป็นตัวชี้โมฆะของประเภทนั้น (อะไรก็ตามที่เป็น) มันเป็นสิ่งจำเป็นที่ทั้งสองตัวชี้ null falseประเภทเดียวกันเปรียบเทียบเหมือนกันและการแปลงบูลจะเปิดตัวชี้โมฆะลง ไม่จำเป็นต้องมีอะไรอีก ดังนั้นจึงเป็นไปได้สำหรับคอมไพเลอร์ (โง่ แต่เป็นไปได้) ที่จะใช้เช่น0xabcdef1234หรือจำนวนอื่น ๆ สำหรับตัวชี้โมฆะ ในทางกลับกันnullptrจำเป็นต้องแปลงให้เป็นศูนย์ตัวเลข
Damon

1
@DeadMG: คำตอบของฉันไม่ถูกต้องคืออะไร? ที่f(nullptr)จะไม่เรียกฟังก์ชั่นที่ต้องการ? มีแรงจูงใจมากกว่าหนึ่งข้อ สิ่งที่มีประโยชน์อื่น ๆ อีกมากมายสามารถค้นพบโดยโปรแกรมเมอร์เองในปีที่ผ่านมา ดังนั้นคุณจึงไม่สามารถพูดได้ว่ามีเพียงหนึ่งในการใช้งานจริงnullptrของ
นาวาซ

87

C ++ 11 เปิดตัวnullptrเป็นที่รู้จักกันเป็นNullอย่างต่อเนื่องชี้และมันช่วยเพิ่มความปลอดภัยประเภทและแก้ไขสถานการณ์ที่ไม่ชัดเจนแตกต่างจากการดำเนินงานที่มีอยู่แล้วขึ้นอยู่กับตัวชี้ null NULLคงที่ nullptrเพื่อให้สามารถเข้าใจได้เปรียบของ ก่อนอื่นเราต้องเข้าใจว่าอะไรคือNULLอะไรและปัญหาที่เกี่ยวข้องกับมันคืออะไร


อะไรNULLกันแน่

C ++ 11 พื้นฐานNULLถูกใช้เพื่อแสดงตัวชี้ที่ไม่มีค่าหรือตัวชี้ที่ไม่ได้ชี้ไปที่สิ่งใดที่ถูกต้อง ขัดกับความคิดที่ได้รับความนิยมNULLไม่ได้เป็นคำหลักใน C ++ มันเป็นตัวระบุที่กำหนดไว้ในส่วนหัวของห้องสมุดมาตรฐาน ในระยะสั้นคุณไม่สามารถใช้NULLโดยไม่รวมส่วนหัวของห้องสมุดมาตรฐานบางส่วน พิจารณาโปรแกรมตัวอย่าง :

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

เอาท์พุท:

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

มาตรฐาน C ++ กำหนด NULL เป็นแมโครที่กำหนดใช้งานที่กำหนดไว้ในไฟล์ส่วนหัวของไลบรารีมาตรฐาน ต้นกำเนิดของโมฆะจาก C และ C ++ ที่สืบทอดมาจากมาตรฐานซีซีที่กำหนดไว้เป็นโมฆะหรือ0 (void *)0แต่ใน C ++ มีความแตกต่างเล็กน้อย

C ++ ไม่สามารถยอมรับข้อกำหนดนี้ได้ ซึ่งแตกต่างจาก C, C ++ เป็นภาษาที่พิมพ์อย่างมาก (C ไม่จำเป็นต้องใช้การส่งคำสั่งที่ชัดเจนจากvoid*ประเภทใด ๆ ในขณะที่ C ++ สั่งการส่งที่ชัดเจน) สิ่งนี้ทำให้นิยามของ NULL ที่ระบุโดย C มาตรฐานไร้ประโยชน์ในนิพจน์ C ++ จำนวนมาก ตัวอย่างเช่น:

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

ถ้า NULL ถูกกำหนดเป็น(void *)0นิพจน์ด้านบนจะไม่ทำงาน

  • กรณีที่ 1:จะไม่รวบรวมเพราะหล่ออัตโนมัติเป็นสิ่งจำเป็นจากการ void *std::string
  • กรณีที่ 2:จะไม่คอมไพล์เนื่องจากvoid *จำเป็นต้องใช้ฟังก์ชันcast to to pointer to member

จึงไม่เหมือน C, C ++ มาตรฐานได้รับคำสั่งในการกำหนดเป็นโมฆะเป็นตัวอักษรตัวเลขหรือ00L


ดังนั้นความต้องการค่าคงตัวโมฆะตัวอื่นเมื่อเรามีNULLอยู่แล้วคืออะไร?

แม้ว่าคณะกรรมการมาตรฐาน C ++ จะมีคำจำกัดความ NULL ซึ่งจะใช้กับ C ++ ได้ แต่คำนิยามนี้มีปัญหาที่เป็นธรรม NULL ทำงานได้ดีพอสำหรับเกือบทุกสถานการณ์ แต่ไม่ใช่ทั้งหมด มันให้ผลลัพธ์ที่น่าแปลกใจและผิดพลาดสำหรับบางสถานการณ์ที่หายาก ตัวอย่างเช่น :

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

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

เอาท์พุท:

In Int version

เห็นได้ชัดว่าความตั้งใจที่ดูเหมือนว่าจะเรียกรุ่นที่ใช้char*เป็นอาร์กิวเมนต์ แต่เป็นผลลัพธ์ที่แสดงฟังก์ชั่นที่ใช้intรุ่นที่ได้รับการเรียก นี่เป็นเพราะ NULL เป็นตัวอักษรตัวเลข

นอกจากนี้เนื่องจากมีการกำหนดการใช้งานว่า NULL เป็น 0 หรือ 0L จึงมีความสับสนในการแก้ไขการโอเวอร์โหลดของฟังก์ชันจำนวนมาก

โปรแกรมตัวอย่าง:

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

การวิเคราะห์ตัวอย่างด้านบน:

  • กรณีที่ 1:โทรdoSomething(char *)ตามที่คาดไว้
  • กรณีที่ 2: การโทรdoSomething(int)แต่อาจchar*เป็นรุ่นที่ต้องการเนื่องจาก0IS ก็เป็นตัวชี้ null เช่นกัน
  • กรณีที่ 3:ถ้าNULLถูกกำหนดให้0โทรdoSomething(int)เมื่อบางทีอาจdoSomething(char *)มีเจตนาอาจส่งผลให้เกิดข้อผิดพลาดเชิงตรรกะที่รันไทม์ หากNULLถูกกำหนดเป็นการ0Lโทรจะคลุมเครือและส่งผลให้เกิดข้อผิดพลาดในการรวบรวม

ดังนั้นขึ้นอยู่กับการใช้งานรหัสเดียวกันสามารถให้ผลลัพธ์ที่หลากหลายซึ่งไม่พึงประสงค์อย่างชัดเจน โดยธรรมชาติแล้วคณะกรรมการมาตรฐาน C ++ ต้องการแก้ไขสิ่งนี้และนั่นคือแรงจูงใจสำคัญสำหรับ nullptr


ดังนั้นมันคืออะไรnullptrและหลีกเลี่ยงปัญหาได้NULLอย่างไร

C ++ 11 แนะนำคำหลักใหม่nullptrเพื่อใช้เป็นค่าคงที่ตัวชี้โมฆะ ซึ่งแตกต่างจาก NULL พฤติกรรมที่ไม่ได้กำหนดใช้งาน ไม่ใช่มาโคร แต่มีชนิดของตัวเอง nullptr std::nullptr_tมีชนิด C ++ 11 กำหนดคุณสมบัติสำหรับ nullptr อย่างเหมาะสมเพื่อหลีกเลี่ยงข้อเสียของ NULL ในการสรุปคุณสมบัติ:

ทรัพย์สินที่ 1:มันมีประเภทของตัวเองstd::nullptr_tและ
ทรัพย์สินที่ 2:มันเป็นโดยปริยายแปลงสภาพและเทียบเท่ากับประเภทตัวชี้ใด ๆ หรือประเภทตัวชี้ไปยังสมาชิก แต่
ทรัพย์สินที่ 3:boolมันไม่ได้โดยปริยายแปลงสภาพหรือเทียบได้กับประเภทหนึ่งยกเว้น

ลองพิจารณาตัวอย่างต่อไปนี้:

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

ในโปรแกรมด้านบน

  • กรณีที่ 1:ตกลง - คุณสมบัติ 2
  • กรณีที่ 2:ไม่ตกลง - คุณสมบัติ 3
  • กรณีที่ 3:ตกลง - คุณสมบัติ 3
  • กรณีที่ 4:ไม่มีความสับสน - char *เวอร์ชันการโทร, คุณสมบัติ 2 & 3

ดังนั้นการแนะนำของ nullptr หลีกเลี่ยงปัญหาทั้งหมดของ NULL เก่าที่ดี

คุณควรใช้nullptrอย่างไรและที่ไหน

กฎของหัวแม่มือสำหรับ C ++ 11 นั้นเป็นเพียงการเริ่มใช้nullptrเมื่อใดก็ตามที่คุณเคยใช้ NULL มาก่อน


การอ้างอิงมาตรฐาน:

C ++ 11 มาตรฐาน: C.3.2.4 Macro NULL
C ++ 11 มาตรฐาน: 18.2 ประเภท
C ++ 11 มาตรฐาน: 4.10 การแปลงตัวชี้
C99 มาตรฐาน: 6.3.2.3 พอยน์เตอร์


ฉันได้ฝึกฝนคำแนะนำสุดท้ายของคุณแล้วตั้งแต่ฉันรู้จักnullptrแม้ว่าฉันจะไม่รู้ว่ารหัสของฉันแตกต่างกันอย่างไร ขอบคุณสำหรับคำตอบที่ดีและโดยเฉพาะอย่างยิ่งสำหรับความพยายาม ทำให้ฉันมีแสงสว่างมากในหัวข้อ
มาร์คการ์เซีย

"ในไฟล์ส่วนหัวของไลบรารีมาตรฐานบางตัว" -> ทำไมไม่เขียน "cstddef" ตั้งแต่ต้น?
mxmlnkn

ทำไมเราควรอนุญาตให้ nullptr แปลงเป็นประเภทบูลได้? คุณช่วยอธิบายเพิ่มเติมได้ไหม?
Robert Wang

... ถูกใช้เพื่อแสดงพอยน์เตอร์ที่ไม่มีค่า ...ตัวแปรจะมีค่าเสมอ อาจเป็นเสียงรบกวนหรือ0xccccc....แต่ตัวแปรที่มีค่าน้อยกว่านั้นเป็นความขัดแย้งโดยธรรมชาติ
3Dave

"กรณีที่ 3: ตกลง - คุณสมบัติ 3" (บรรทัดbool flag = nullptr;) ไม่ไม่ฉันได้รับข้อผิดพลาดต่อไปนี้ในเวลารวบรวมด้วย g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

แรงจูงใจจริงที่นี่คือการส่งต่อที่สมบูรณ์แบบ

พิจารณา:

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

กล่าวง่ายๆคือ 0 เป็นค่าพิเศษแต่ค่าไม่สามารถเผยแพร่ผ่านประเภทระบบเท่านั้น ฟังก์ชั่นการส่งต่อมีความสำคัญและ 0 ไม่สามารถจัดการกับพวกเขา ดังนั้นจึงจำเป็นอย่างยิ่งที่จะต้องแนะนำnullptrว่าประเภทใดเป็นพิเศษและประเภทนั้นสามารถเผยแพร่ได้จริง ในความเป็นจริงทีม MSVC ต้องแนะนำnullptrล่วงหน้าของตารางหลังจากที่พวกเขาดำเนินการอ้างอิง rvalue แล้วค้นพบข้อผิดพลาดนี้สำหรับตัวเอง

ยังมีอีกหลายมุมที่nullptrจะทำให้ชีวิตง่ายขึ้น - แต่ไม่ใช่กรณีหลักเนื่องจากนักแสดงสามารถแก้ปัญหาเหล่านี้ได้ พิจารณา

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

เรียกโอเวอร์โหลดสองตัวแยกกัน นอกจากนี้ให้พิจารณา

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

นี่มันคลุมเครือ แต่ด้วย nullptr คุณสามารถให้

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

7
ตลก. ครึ่งหนึ่งของคำตอบนั้นเหมือนกับคำตอบอีกสองคำซึ่งตามคำตอบของคุณคือ"ค่อนข้างไม่ถูกต้อง" !!!
นาวาซ

ปัญหาการส่งต่อสามารถแก้ไขได้ด้วยการร่าย forward((int*)0)โรงงาน ฉันพลาดอะไรไปรึเปล่า?
jcsahnwaldt Reinstate Monica

5

พื้นฐานของ nullptr

std::nullptr_tเป็นชนิดของตัวชี้ null ตัวอักษร nullptr มันเป็น prvalue / rvalue std::nullptr_tประเภท มีการแปลงโดยนัยจาก nullptr เป็นค่าตัวชี้ null ของประเภทตัวชี้ใด ๆ

ตัวอักษร 0 เป็น int ไม่ใช่ตัวชี้ ถ้า C ++ พบว่าตัวเองกำลังมองหา 0 ในบริบทที่สามารถใช้ตัวชี้ได้เท่านั้นมันจะตีความว่า 0 เป็นตัวชี้โมฆะอย่างไม่น่าเชื่อ แต่นั่นคือตำแหน่งทางเลือก นโยบายหลักของ C ++ คือ 0 คือ int ไม่ใช่ตัวชี้

ข้อได้เปรียบ 1 - ลบความคลุมเครือเมื่อมีการใช้งานมากเกินตัวชี้และส่วนประกอบ

ใน C ++ 98 ความหมายหลักของเรื่องนี้คือการโอเวอร์โหลดบนตัวชี้และประเภทอินทิกรัลอาจนำไปสู่ความประหลาดใจ การผ่าน 0 หรือ NULL ไปยังโอเวอร์โหลดดังกล่าวไม่เคยเรียกว่าตัวชี้โอเวอร์โหลด:

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

สิ่งที่น่าสนใจเกี่ยวกับการโทรนั้นคือความขัดแย้งระหว่างความหมายที่ชัดเจนของซอร์สโค้ด (“ ฉันกำลังเรียกความสนุกกับตัวชี้ NULL-the null”) และความหมายที่แท้จริงของมัน ชี้”)

ข้อได้เปรียบของ nullptr คือมันไม่มีประเภทที่สมบูรณ์ การเรียกฟังก์ชั่นโอเวอร์โหลดด้วยความสนุกด้วย nullptr จะเรียกโมฆะ * overload (เช่นตัวชี้โอเวอร์โหลด) เนื่องจาก nullptr ไม่สามารถดูเป็นส่วนสำคัญได้:

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

ใช้ nullptr แทน 0 หรือ NULL จึงหลีกเลี่ยงความประหลาดใจในการแก้ปัญหาการโอเวอร์โหลด

ประโยชน์ของผู้อื่นnullptrมากกว่าNULL(0)เมื่อใช้รถยนต์ประเภทกลับ

ตัวอย่างเช่นสมมติว่าคุณพบสิ่งนี้ในฐานรหัส:

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

หากคุณไม่ทราบ (หรือไม่สามารถหาได้ง่าย) สิ่งที่ findRecord ส่งคืนอาจไม่ชัดเจนว่าผลลัพธ์เป็นชนิดตัวชี้หรือชนิดอินทิกรัล ท้ายที่สุด 0 (สิ่งที่ทดสอบกับผลลัพธ์) สามารถไปทางใด หากคุณเห็นสิ่งต่อไปนี้ในทางกลับกัน

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

ไม่มีความกำกวม: ผลลัพธ์จะต้องเป็นประเภทพอยน์เตอร์

ข้อได้เปรียบ 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;
}

ข้างต้นโปรแกรมรวบรวมและดำเนินการสำเร็จ แต่ lockAndCallF1, lockAndCallF2 & lockAndCallF3 มีรหัสซ้ำซ้อน มันน่าเสียดายที่จะเขียนโค้ดแบบนี้ถ้าเราสามารถเขียนเทมเพลตสำหรับสิ่งเหล่านี้lockAndCallF1, lockAndCallF2 & lockAndCallF3ได้ ดังนั้นจึงสามารถวางนัยกับแม่แบบ ฉันได้เขียนฟังก์ชั่นเทมเพลตlockAndCallแทนคำจำกัดความหลายคำlockAndCallF1, lockAndCallF2 & lockAndCallF3สำหรับรหัสซ้ำซ้อน

รหัสจะถูกแยกออกเป็นส่วน ๆ ดังต่อไปนี้:

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

การวิเคราะห์รายละเอียดว่าทำไมการรวบรวมล้มเหลวlockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)ไม่ใช่เพราะlockAndCall(f3, f3m, nullptr)

ทำไมการรวบรวมlockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)ล้มเหลว?

ปัญหาคือเมื่อ 0 ถูกส่งผ่านไปยัง lockAndCall การลดประเภทเทมเพลตจะเริ่มขึ้นเพื่อหาประเภท ชนิดของ 0 คือ int ดังนั้นจึงเป็นชนิดของพารามิเตอร์ ptr ภายในการสร้างอินสแตนซ์ของการเรียกนี้เพื่อ lockAndCall น่าเสียดายที่นี่หมายความว่าในการเรียกไปยัง func ภายใน lockAndCall int จะถูกส่งผ่านและไม่เข้ากันได้กับstd::shared_ptr<int>พารามิเตอร์ที่f1คาดหวัง 0 ที่ส่งผ่านไปยังการโทรไปlockAndCallนั้นมีจุดประสงค์เพื่อแสดงตัวชี้โมฆะ แต่สิ่งที่ได้รับการส่งผ่านจริงคือ int พยายามส่งค่า int นี้ไปยัง f1 เนื่องจาก a std::shared_ptr<int>เป็นข้อผิดพลาดประเภท การโทรไปlockAndCallมี 0 ล้มเหลวเพราะภายในแม่แบบเป็น int std::shared_ptr<int>จะถูกส่งผ่านไปยังฟังก์ชั่นที่ต้องใช้เป็น

การวิเคราะห์การโทรเกี่ยวข้องNULLเป็นหลักเดียวกัน เมื่อNULLถูกส่งผ่านไปยังlockAndCallประเภทหนึ่งจะถูกอนุมานสำหรับพารามิเตอร์ ptr และข้อผิดพลาดประเภทเกิดขึ้นเมื่อptr- ประเภท int หรือเหมือน int - ถูกส่งผ่านไปf2ซึ่งคาดว่าจะได้รับstd::unique_ptr<int>ซึ่งคาดว่าจะได้รับ

ในทางตรงกันข้ามการโทรที่เกี่ยวข้องnullptrไม่มีปัญหา เมื่อnullptrถูกส่งไปยังlockAndCallประเภทสำหรับจะสรุปได้ว่าจะเป็นptr std::nullptr_tเมื่อptrถูกส่งผ่านไปf3ยังมีการแปลงโดยนัยจากstd::nullptr_tเป็นint*เพราะstd::nullptr_tแปลงโดยอ้อมไปเป็นตัวชี้ทุกประเภท

ก็จะแนะนำเมื่อใดก็ตามที่คุณต้องการที่จะอ้างถึงตัวชี้โมฆะ, nullptr การใช้งานไม่เป็น 0 NULLหรือ


4

ไม่มีข้อได้เปรียบโดยตรงจากการมีnullptrในแบบที่คุณแสดงตัวอย่าง
แต่ให้พิจารณาสถานการณ์ที่คุณมี 2 ฟังก์ชั่นที่มีชื่อเหมือนกัน 1 intครั้งและอีกอันint*

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

ถ้าคุณต้องการโทรหาfoo(int*)โดยผ่าน NULL วิธีคือ:

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

nullptrทำให้ง่ายและใช้งานง่ายขึ้น :

foo(nullptr);

ลิงค์เพิ่มเติมจากหน้าเว็บของ Bjarne
ไม่เกี่ยวข้อง แต่ขึ้นกับข้อความด้านข้าง C ++ 11:

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

3
สำหรับการอ้างอิงคือdecltype(nullptr) std::nullptr_t
chris

2
@ MarkGarcia มันเป็นแบบเต็มเป่าเท่าที่ฉันรู้
chris

5
@ MarkGarcia มันเป็นคำถามที่น่าสนใจ cppreference typedef decltype(nullptr) nullptr_t;มี: ฉันเดาว่าฉันสามารถดูได้ในมาตรฐาน อ่าพบแล้ว: หมายเหตุ: std :: nullptr_t เป็นประเภทที่แตกต่างกันซึ่งไม่ใช่ประเภทตัวชี้หรือตัวชี้ไปยังประเภทสมาชิก ค่อนข้าง prvalue ประเภทนี้เป็นค่าคงที่ตัวชี้โมฆะและสามารถแปลงเป็นค่าตัวชี้โมฆะหรือค่าตัวชี้สมาชิกโมฆะ
chris

2
@DeadMG: มีมากกว่าหนึ่งแรงจูงใจ สิ่งที่มีประโยชน์อื่น ๆ อีกมากมายสามารถค้นพบโดยโปรแกรมเมอร์เองในปีที่ผ่านมา ดังนั้นคุณจึงไม่สามารถพูดได้ว่ามีเพียงหนึ่งในการใช้งานจริงnullptrของ
นาวาซ

2
@DeadMG: แต่คุณตอบว่าคำตอบนี้"ค่อนข้างไม่ถูกต้อง"เพียงเพราะมันไม่ได้พูดถึง"แรงจูงใจที่แท้จริง" ที่คุณพูดถึงในคำตอบของคุณ ไม่เพียง แต่คำตอบนี้ (และของฉันเช่นกัน) ยังได้รับ downvote จากคุณ
นาวาซ

4

เช่นเดียวกับที่คนอื่นพูดไปแล้วข้อได้เปรียบหลักอยู่ที่การโอเวอร์โหลด และในขณะที่ชัดเจนintกับตัวชี้โอเวอร์โหลดอาจหายากลองใช้ฟังก์ชั่นstd::fillไลบรารี่มาตรฐานเช่น(ซึ่งกัดฉันมากกว่าหนึ่งครั้งใน C ++ 03):

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

ไม่ได้รวบรวม: Cannot convert int to MyClass*.


2

IMO มีความสำคัญมากกว่าปัญหาการโอเวอร์โหลดเหล่านั้น: ในการสร้างเทมเพลตที่ซ้อนกันลึก ๆ มันเป็นการยากที่จะไม่ติดตามประเภทและการให้ลายเซ็นที่ชัดเจนนั้นเป็นความพยายาม ดังนั้นสำหรับทุกสิ่งที่คุณใช้ยิ่งมุ่งเน้นไปที่วัตถุประสงค์ที่ตั้งใจไว้มากขึ้นยิ่งดีขึ้นก็จะลดความต้องการลายเซ็นที่ชัดเจนและช่วยให้คอมไพเลอร์ในการผลิตข้อความผิดพลาดที่ลึกซึ้งยิ่งขึ้นเมื่อมีอะไรผิดพลาด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.