Що саме таке nullptr?


570

Зараз у нас є C ++ 11 з багатьма новими можливостями. Цікавим і заплутаним (принаймні для мене) є нове nullptr.

Ну, більше не потрібно в бридкому макросі NULL.

int* x = nullptr;
myclass* obj = nullptr;

Все-таки я не розумію, як це nullptrпрацює. Наприклад, стаття у Вікіпедії говорить:

C ++ 11 виправляє це, вводячи нове ключове слово, яке служить відмінною константою нульового вказівника: nullptr. Він має тип nullptr_t , який неявно конвертований і порівняний з будь-яким типом вказівника або типом вказівника на член. Це не неявно конвертоване або порівнянне з цілісними типами, за винятком bool.

Як це ключове слово та екземпляр типу?

Крім того, чи є у вас інший приклад (поруч із Вікіпедією), де nullptrвищий за добрий старий 0?


23
пов'язаний факт: nullptrтакож використовується для представлення нульової посилання на керовані ручки в C ++ / CLI.
Мехрдад Афшарі

3
Використовуючи Visual C ++, пам’ятайте, що якщо ви використовуєте nullptr з нативним кодом C / C ++, а потім компілюєте за допомогою параметра компілятора / clr, компілятор не може визначити, чи вказано nullptr власне або кероване значення нульового вказівника. Щоб зробити свій намір зрозумілим компілятору, використовуйте nullptr, щоб вказати кероване значення, або __nullptr, щоб вказати нативне значення. Microsoft реалізувала це як розширення компонента.
cseder

6
Чи nullptr_tгарантовано мати лише одного члена nullptr,? Отже, якщо функція повернулася nullptr_t, тоді компілятор вже знає, яке значення буде повернуто, незалежно від тіла функції?
Аарон Мак-Дейд

8
@AaronMcDaid std::nullptr_tможна створити миттєво , але всі екземпляри будуть ідентичними nullptrтому, що тип визначений як typedef decltype(nullptr) nullptr_t. Я вважаю, що основною причиною такого типу є те, що функції можна перевантажувати спеціально для вилову nullptr, якщо потрібно. Дивіться тут приклад.
Час Джастіна - Відновіть Моніку

5
0 ніколи не був нульовим покажчиком, нульовий покажчик - це покажчик, який можна отримати, перекинувши нульовий літерал на тип вказівника, і він не вказує на будь-який існуючий об'єкт за визначенням.
Свіфт - П’ятничний пиріг

Відповіді:


403

Як це ключове слово та екземпляр типу?

Це не дивно. І те, trueі falseінше, є ключовими словами, і в буквальному сенсі вони мають тип ( bool). nullptrє буквальний тип вказівникаstd::nullptr_t , і це перше значення (ви не можете прийняти його адресу за допомогою &).

  • 4.10про перетворення вказівника говорить, що первісне значення типу std::nullptr_t- це нульова константа вказівника, і що інтегральна константа нуля вказівника може бути перетворена вstd::nullptr_t . Протилежний напрямок заборонено. Це дозволяє перевантажувати функцію як для покажчиків, так і для цілих чисел, і переходити nullptrдо вибору версії вказівника. Версія, яка проходить, NULLабо 0блукаюче вибрала б int.

  • Гравець nullptr_tінтегрального типу потребує а reinterpret_castта має таку саму семантику, як і амплуа(void*)0 і складова для інтегрального типу (визначена реалізація відображення). reinterpret_castНе може перетворити nullptr_tбудь-який тип покажчика. Покладайтеся на неявну конверсію, якщо можливо, або використовуйте static_cast.

  • Стандарт вимагає, щоб це sizeof(nullptr_t)було sizeof(void*).


О, після перегляду мені здається, що умовний оператор не може перетворити 0 в nullptr у таких випадках cond ? nullptr : 0;. Видалено з моєї відповіді.
Йоханнес Шауб - ліб

88
Зауважте, що NULLнавіть не гарантується 0. Це може бути 0L, в такому випадку заклик до void f(int); void f(char *);буде неоднозначним. nullptrзавжди надаватиме перевагу версії вказівника, і ніколи не називати її int. Також зауважте, що він nullptr може бути конвертованим bool(у проекті сказано, що в 4.12).
Йоханнес Шауб - ліб

@litb: тож щодо f (int) і f (void *) - чи буде f (0) все-таки неоднозначним?
Стів Фоллі

27
@Steve, ні, що не буде викликати intверсію. Але f(0L)неоднозначно, тому що як long -> intдобре , як long -> void*це однаково дорого. Отже, якщо NULL 0Lу вашому компіляторі, то виклик f(NULL)буде неоднозначним, враховуючи ці дві функції. Не так, nullptrзвичайно.
Йоханнес Шауб - ліб

2
@SvenS Це не повинно бути визначено, як (void*)0у C ++. Але він може бути визначений як будь-яка довільна константа нульового покажчика, яку будь-яка інтегральна константа зі значенням 0 і nullptrвиконує. Отже, більшість точно не буде, але може . (Ви забули пінг мені btw ..)
Deduplicator

60

Від nullptr: безпечний і чіткий ясний вказівник :

Нове ключове слово C ++ 09 nullptr позначає константу rvalue, яка служить універсальним нульовим літеральним покажчиком, замінюючи помилковий і слабо типізований літерал 0 та сумнозвісний макрос NULL. Таким чином, nullptr припиняє понад 30 років збентеження, неоднозначності та помилок. У наступних розділах представлено засіб nullptr та показано, як воно може виправити недуги NULL та 0.

Інші посилання:


17
C ++ 09? Чи не називалося це C ++ 0x до серпня 2011 року?
Майкл Дорст

2
@anthropomorphic Ну це його призначення. C ++ 0x використовувався, поки він ще не працює, тому що не було відомо, буде завершено 2008 або 2009 рр. Зауважте, що він фактично став C ++ 0B, ​​що означає C ++ 11. Дивіться stroustrup.com/C++11FAQ.html
mxmlnkn

44

Чому nullptr в C ++ 11? Що це? Чому NULL недостатній?

Експерт із C ++ Алекс Аллайн каже це чудово тут (мій акцент додано жирним шрифтом):

... уявіть, що у вас є дві наступні декларації функції:

void func(int n); 
void func(char *s);

func( NULL ); // guess which function gets called?

Хоча, схоже, буде називатися друга функція - ви, зрештою, передаєте те, що, здається, вказівник, - це справді перша функція, яка буде викликана! Проблема полягає в тому, що оскільки NULL дорівнює 0, а 0 - ціле число, перша версія func буде викликана замість цього. Це така річ, яка, так, не відбувається постійно, але коли це відбувається, надзвичайно засмучує і заплутує. Якщо ви не знали деталей того, що відбувається, це може виглядати як помилка компілятора. Мовна функція, схожа на помилку компілятора, - це не те, що потрібно.

Введіть nullptr. У C ++ 11 nullptr - це нове ключове слово, яке можна (і повинно!) Використовувати для представлення покажчиків NULL; Іншими словами, де б ви раніше не писали NULL, замість цього слід використовувати nullptr. Вам більше не зрозуміло, програміст , (всі знають, що означає NULL), але це більш явно для компілятора , який більше не буде бачити 0, які всюди використовуються, щоб мати особливе значення при використанні в якості покажчика.

Аллайн закінчує свою статтю:

Незалежно від усього цього - правило C ++ 11 - просто починати користуватися, nullptrколи б ви інакше використовувались NULLу минулому.

(Мої слова):

Нарешті, не забувайте, що nullptrце об’єкт - клас. Він може бути використаний у будь-якому місці, NULLякий раніше використовувався, але якщо вам потрібен його тип з якихось причин, його можна витягтиdecltype(nullptr) або безпосередньо , як описано std::nullptr_t, що це просто typedefзdecltype(nullptr) .

Список літератури:

  1. Cprogramming.com: Кращі типи в C ++ 11 - nullptr, класи перерахунків (сильно набрані перерахування) та cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t

2
Треба сказати, що ваша відповідь занижена, це було легко зрозуміти через ваш приклад.
mss

37

Коли у вас є функція, яка може отримувати покажчики на декілька типів, виклик її за допомогою NULLнеоднозначний. Спосіб, яким це зараз подобається, дуже хитромудрий, приймаючи Int і припускаючи, що це NULL.

template <class T>
class ptr {
    T* p_;
    public:
        ptr(T* p) : p_(p) {}

        template <class U>
        ptr(U* u) : p_(dynamic_cast<T*>(u)) { }

        // Without this ptr<T> p(NULL) would be ambiguous
        ptr(int null) : p_(NULL)  { assert(null == NULL); }
};

У C++11вас буде можливість перевантажуватись nullptr_tтак, що ptr<T> p(42);буде помилкою часу компіляції, а не час виконання assert.

ptr(std::nullptr_t) : p_(nullptr)  {  }

Що робити, якщо NULLце визначено як0L ?
LF

9

nullptrне можна присвоювати інтегральному типу, такому як тип intлише вказівника; або вбудований тип вказівника, наприклад, int *ptrабо розумний вказівник, такий якstd::shared_ptr<T>

Я вважаю, що це важлива відмінність NULL все ще можна присвоїти як інтегральному типу, так і покажчику, оскільки NULLце макрос, розширений до 0якого може служити як початковим значенням, intтак і вказівником.


Зауважте, що ця відповідь неправильна. NULLне гарантовано буде розширено до 0.
LF

6

Крім того, чи є у вас інший приклад (поруч із Вікіпедією), де nullptrвище, ніж старий добрий 0?

Так. Це також (спрощений) приклад у реальному світі, який виник у нашому виробничому коді. Він виділявся лише тим, що gcc змогла видавати попередження при перехресному компілюванні на платформу з різною шириною реєстру (все ще не точно точно зрозуміло, чому лише при перехресному компілюванні з x86_64 на x86, попереджаєwarning: converting to non-pointer type 'int' from NULL ):

Розглянемо цей код (C ++ 03):

#include <iostream>

struct B {};

struct A
{
    operator B*() {return 0;}
    operator bool() {return true;}
};

int main()
{
    A a;
    B* pb = 0;
    typedef void* null_ptr_t;
    null_ptr_t null = 0;

    std::cout << "(a == pb): " << (a == pb) << std::endl;
    std::cout << "(a == 0): " << (a == 0) << std::endl; // no warning
    std::cout << "(a == NULL): " << (a == NULL) << std::endl; // warns sometimes
    std::cout << "(a == null): " << (a == null) << std::endl;
}

Це дає такий вихід:

(a == pb): 1
(a == 0): 0
(a == NULL): 0
(a == null): 1

Я не бачу, як це покращується при використанні nullptr (і C ++ 11). Якщо встановити pb на nullptr, перше порівняння оцінюється як істинне (порівнюючи яблука з грушами ..). Другий випадок ще гірший: якщо порівнювати a з nullptr, він перетворить a в B *, а потім оцінить до true знову (до того, як він був відлитий на bool і expr оцінили як false). Це все нагадує мені JavaScript, і мені цікаво, чи отримаємо === в C ++ у майбутньому :(
Nils

5

Ну а в інших мовах є зарезервовані слова, які є екземплярами типів. Наприклад, Python:

>>> None = 5
  File "<stdin>", line 1
SyntaxError: assignment to None
>>> type(None)
<type 'NoneType'>

Це насправді досить близьке порівняння, оскільки, Noneяк правило, використовується для чогось, що не було інтіалізовано, але в той же час порівняння, такі якNone == 0 помилкові.

З іншого боку, у звичайному C NULL == 0повертається справжній IIRC, оскільки NULLце лише макрос, який повертає 0, що завжди є недійсною адресою (AFAIK).


4
NULLявляє собою макрос, який розширюється до нуля, постійний нульовий кидок вказівника виробляє нульовий покажчик. Нульовий вказівник не повинен бути нульовим (але часто є), нуль не завжди є недійсною адресою, а нестаціонарний нульовий припис покажчику не повинен бути нульовим, а нульовий покажчик - кинутим на ціле число не повинно бути нульовим. Я сподіваюся, що я все зрозумів, нічого не забувши. Довідка: c-faq.com/null/null2.html
Семюел Едвін Уорд

3

Це ключове слово, оскільки стандарт визначатиме його як таке. ;-) Згідно з останньою версією проекту (n2914)

2.14.7 Літерали покажчика [lex.nullptr]

pointer-literal:
nullptr

Літеральний покажчик - це ключове слово nullptr. Це ревальвація типу std::nullptr_t.

Це корисно, оскільки не неявно перетворюється на цілісне значення.


2

Скажімо, у вас є функція (f), яка перевантажена, щоб приймати і int, і char *. Перед C ++ 11, Якщо ви хочете викликати його з нульовим вказівником, і ви використовували NULL (тобто значення 0), ви б назвали той, який перевантажений для int:

void f(int);
void f(char*);

void g() 
{
  f(0); // Calls f(int).
  f(NULL); // Equals to f(0). Calls f(int).
}

Це, мабуть, не те, чого ти хотів. C ++ 11 вирішує це за допомогою nullptr; Тепер ви можете написати наступне:

void g()
{
  f(nullptr); //calls f(char*)
}

1

Дозвольте спершу дати вам реалізацію непродуманого nullptr_t

struct nullptr_t 
{
    void operator&() const = delete;  // Can't take address of nullptr

    template<class T>
    inline operator T*() const { return 0; }

    template<class C, class T>
    inline operator T C::*() const { return 0; }
};

nullptr_t nullptr;

nullptrє тонким прикладом ідіоми Resolver типу повернення для автоматичного виведення нульового вказівника правильного типу залежно від типу примірника, якому він призначається.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Як ви можете вище, при nullptrпризначенні цілого вказівника створюється intтип типової функції шаблонованої конверсії. І те саме стосується і покажчиків методів.
  • Таким чином, використовуючи функціональність шаблону, ми фактично створюємо відповідний тип нульового вказівника щоразу, коли ми робимо, призначення нового типу.
  • Оскільки nullptrце ціле буквене значення зі значенням нуль, ви не можете використовувати його адресу, яку ми досягли, видаливши & operator.

Навіщо нам це потрібно nullptrв першу чергу?

  • Як ви бачите, у традиційних NULLє деякі проблеми з цим, як показано нижче:

1️⃣ Неявне перетворення

char *str = NULL; // Implicit conversion from void * to char *
int i = NULL;     // OK, but `i` is not pointer type

2️⃣ Функція викликає неоднозначність

void func(int) {}
void func(int*){}
void func(bool){}

func(NULL);     // Which one to call?
  • Компіляція створює таку помилку:
error: call to 'func' is ambiguous
    func(NULL);
    ^~~~
note: candidate function void func(bool){}
                              ^
note: candidate function void func(int*){}
                              ^
note: candidate function void func(int){}
                              ^
1 error generated.
compiler exit status 1

3️⃣ Перевантаження конструктора

struct String
{
    String(uint32_t)    {   /* size of string */    }
    String(const char*) {       /* string */        }
};

String s1( NULL );
String s2( 5 );
  • У таких випадках потрібно явне приведення (тобто  String s((char*)0)).

0

0 раніше було єдиним цілим значенням, яке може використовуватися як ініціалізатор без кидок для покажчиків: ви не можете ініціалізувати покажчики з іншими цілими значеннями без накиду. Ви можете розглядати 0 як консекспр синглетон, синтаксично схожий на ціле число. Він може ініціювати будь-який покажчик або ціле число. Але дивно, що ви побачите, що він не має виразного типу: цеint . То як же 0 може ініціалізувати покажчики, а 1 не може? Практична відповідь полягала в тому, що нам потрібен засіб визначення нульового значення вказівника, і пряме неявне перетворення intв покажчик є помилковим. Таким чином, 0 став справжнім звіром дивного дива з доісторичної епохи. nullptrбуло запропоновано бути реальним однотонним конспексом представлення нульового значення для ініціалізації покажчиків. Він не може бути використаний для прямої ініціалізації цілих чисел та усунення неоднозначностей, пов'язаних із визначенням NULLу термінах 0. nullptrМожна визначити як бібліотеку з використанням синтаксису std, але семантично виглядав як відсутній основний компонент. NULLтепер застаріло на користь nullptr, якщо певна бібліотека не вирішить визначити його як nullptr.


-1

Ось заголовок LLVM

// -*- C++ -*-
//===--------------------------- __nullptr --------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef _LIBCPP_NULLPTR
#define _LIBCPP_NULLPTR

#include <__config>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif

#ifdef _LIBCPP_HAS_NO_NULLPTR

_LIBCPP_BEGIN_NAMESPACE_STD

struct _LIBCPP_TEMPLATE_VIS nullptr_t
{
    void* __lx;

    struct __nat {int __for_bool_;};

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t() : __lx(0) {}
    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t(int __nat::*) : __lx(0) {}

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR operator int __nat::*() const {return 0;}

    template <class _Tp>
        _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
        operator _Tp* () const {return 0;}

    template <class _Tp, class _Up>
        _LIBCPP_INLINE_VISIBILITY
        operator _Tp _Up::* () const {return 0;}

    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator==(nullptr_t, nullptr_t) {return true;}
    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator!=(nullptr_t, nullptr_t) {return false;}
};

inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t __get_nullptr_t() {return nullptr_t(0);}

#define nullptr _VSTD::__get_nullptr_t()

_LIBCPP_END_NAMESPACE_STD

#else  // _LIBCPP_HAS_NO_NULLPTR

namespace std
{
    typedef decltype(nullptr) nullptr_t;
}

#endif  // _LIBCPP_HAS_NO_NULLPTR

#endif  // _LIBCPP_NULLPTR

(багато чого можна швидко розкрити grep -r /usr/include/*`)

Одне, що вискакує, це *перевантаження оператора (повернення 0 набагато дружніше, ніж segfaulting ...). Інша справа, це не виглядає сумісним зі зберіганням адреси на всіх . Що, порівняно з тим, як відбувається переміщення пустоти * та передача результатів NULL до нормальних покажчиків у якості дозорних значень, очевидно, зменшить фактор "ніколи не забувай, це може бути бомба".


-2

NULL не повинна бути 0. Якщо ви завжди використовуєте NULL, а ніколи 0, NULL може бути будь-яким значенням. Якщо припустити, ви програмуєте мікроконтролер von Neuman з плоскою пам'яттю, який має вектори переривання на рівні 0. Якщо NULL дорівнює 0, а щось пише на покажчик NULL, мікроконтролер виходить з ладу. Якщо NULL дозволить сказати 1024, а в 1024 є зарезервовану змінну, запис не завершить її збій, і ви можете виявити призначення NULL Pointer зсередини програми. Це безглуздо на ПК, але для космічних зондів, військової чи медичної техніки важливо не зазнати аварій.


2
Ну, фактичне значення нульового вказівника в пам'яті може бути не нульовим, але C (і C ++) стандартні мандати компіляторів перетворюють інтегральний 0 буквальний в нульовий покажчик.
bzim
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.