Які переваги використання 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). Але що стосується вищевказаного коду (присвоєння локальній змінній), то всі три покажчики абсолютно однакові. Єдина перевага - читабельність коду.
балки

2
Я виступаю за те, щоб зробити це FAQ, @Prasoon. Дякую!
sbi

1
NB NULL історично не гарантовано дорівнює 0, але, як oc C99, майже так само, як байт не обов'язково був 8 біт завдовжки, а true і false були значеннями, що залежать від архітектури. Це питання зосереджено на тому 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
Як заявив Мехрдад, подібні перевантаження досить рідкісні. Чи є інші відповідні переваги nullptr? (Ні. Я не вимагаю)
Марк Гарсія

2
@MarkGarcia, Це може бути корисним: stackoverflow.com/questions/13665349/…
chris

9
Ваша виноска здається назад. NULLстандарт вимагає мати інтегральний тип, і тому його зазвичай визначають як 0або 0L. Також я не впевнений, що мені подобається це nullptr_tперевантаження, оскільки воно ловить лише дзвінки nullptr, а не нульовий покажчик іншого типу (void*)0. Але я можу повірити, що він має певну користь, навіть якщо все, що він робить, це заощадити, визначивши однозначний власний тип власника місць, щоб він означав «немає».
Стів Джессоп

1
Ще однією перевагою (хоча і визнається незначною) може бути те, що nullptrмає чітко визначене числове значення, тоді як нульові константи покажчика цього не мають. Константа нульового покажчика перетворюється на нульовий покажчик цього типу (що б там не було). Потрібно, щоб два нульових вказівника одного типу порівнювали однаково, і булева конверсія перетворює нульовий покажчик на false. Більше нічого не потрібно. Тому компілятор (нерозумно, але можливо) може використовувати, наприклад, 0xabcdef1234або якесь інше число для нульового вказівника. З іншого боку, nullptrпотрібно перетворити на числовий нуль.
Деймон

1
@DeadMG: Що в моїй відповіді невірно? що f(nullptr)не буде викликати передбачувану функцію? Було більше мотивацій. Багато інших корисних речей можуть відкрити самі програмісти в найближчі роки. Таким чином , ви не можете сказати , що є тільки один істинний використання в nullptr.
Наваз

87

C ++ 11 вводить nullptr, він відомий як Nullконстанта вказівника, і він покращує безпеку типу та вирішує неоднозначні ситуації на відміну від існуючої залежної від реалізації нульової постійної вказівника NULL. Вміти зрозуміти переваги nullptr. спершу нам потрібно зрозуміти, що таке NULLі з чим пов’язані проблеми.


Що NULLсаме?

Pre 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 як макрос, визначений реалізацією, визначений у певних стандартних файлах заголовків бібліотеки. Походження NULL походить від C, а C ++ успадковується від C. Стандарт C визначає NULL як 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 *потрібен перехід від покажчика до функції члена.

Таким чином, на відміну від C, C ++ Standard мандат визначати NULL як числовий буквальний 0або 0L.


Тож у чому необхідність іншої постійної нульової вказівника, коли вона 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 також є нульовим покажчиком.
  • Випадок 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 Макро NULL
C ++ 11 Стандарт: 18.2 Типи
C ++ 11 Стандарт: 4.10 Перетворення покажчиків
C99 Стандарт: 6.3.2.3 Покажчики


Я вже практикую вашу останню пораду, оскільки знаю nullptr, хоча я не знав, яка різниця це насправді для мого коду. Дякую за чудову відповідь і особливо за старання. Висвітлив цю тему.
Марк Гарсія

"у певних стандартних файлах заголовків бібліотеки." -> чому б просто не написати "cstddef" від початку?
mxmlnkn

Чому ми повинні дозволяти nullptr бути конвертованим у тип bool? Не могли б ви докладніше розробити детальніше?
Роберт Ван

... використовувався для представлення покажчика, який не має значення ... Змінні завжди мають значення. Це може бути шум, або 0xccccc...., але змінна величина без значення - властива суперечність.
3Dave

"Справа 3: Гаразд - Властивість 3" (рядок bool flag = nullptr;). Ні, не гаразд, я отримую таку помилку під час компіляції з g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Георг

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достроково після того, як вони впровадили посилання на рецензування, а потім відкрили для себе цю проблему.

Є кілька інших кутових випадків, коли 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

Основи нульптр

std::nullptr_t- тип нульового вказівника, буквальний, nullptr. Це первісне / rvalue типу std::nullptr_t. Існують неявні перетворення від nullptr до null pointer значення будь-якого типу вказівника.

Буквальний 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 - нульовим вказівником") та його фактичним значенням ("Я називаю забаву з якимось цілим числом, а не нулем") покажчик ”).

Перевага nullptr полягає в тому, що він не має інтегрального типу. Виклик перевантаженої функції весело за допомогою nullptr викликає void * 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. На жаль, це означає, що у виклику функціонувати всередині 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—ant або 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 - це окремий тип, який не є ні типом вказівника, ні вказівником на тип члена; швидше, первісне значення цього типу - це константа нульового покажчика і може бути перетворена в значення нульового вказівника або значення нульового вказівника члена.
chris

2
@DeadMG: Було більше мотивів. Багато інших корисних речей можуть відкрити самі програмісти в найближчі роки. Таким чином , ви не можете сказати , що є тільки один істинний використання в nullptr.
Наваз

2
@DeadMG: Але ви сказали, що ця відповідь "зовсім неправильна", просто тому, що вона не говорить про "справжню мотивацію", про яку ви говорили у своїй відповіді. Мало того, що ця відповідь (і моя також) отримала від вас відгук.
Наваз

4

Як вже говорили інші, його головна перевага полягає в перевантаженнях. І хоча явні intта перевантаження покажчиків можуть бути рідкісними, врахуйте стандартні функції бібліотеки на кшталт std::fill(що вкусило мене не раз у C ++ 03):

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

Чи не компілювати: Cannot convert int to MyClass*.


2

ІМО важливіше, ніж ті проблеми із перевантаженням: у глибоко вкладених конструкціях шаблонів важко не прослідкувати типи, а надання явних підписів - це досить зусилля. Тож для всього, що ви використовуєте, чим точніше зосереджено на передбачуваній меті, тим краще, це зменшить потребу в явних підписах і дозволить компілятору видавати більш проникливі повідомлення про помилки, коли щось піде не так.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.