21

I am curious to know how nullptr works. Standards N4659 and N4849 say:

  1. it has to have type std::nullptr_t;
  2. you cannot take its address;
  3. it can be directly converted to a pointer and pointer to member;
  4. sizeof(std::nullptr_t) == sizeof(void*);
  5. its conversion to bool is false;
  6. its value can be converted to integral type identically to (void*)0, but not backwards;

So it is basically a constant with the same meaning as (void*)0, but it has a different type. I have found the implementation of std::nullptr_t on my device and it is as follows.

#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

I am more interested in the first part though. It seems to satisfy the points 1-5, but I have no idea why it has a subclass __nat and everything related to it. I would also like to know why it fails on integral conversions.

struct nullptr_t2{
    void* __lx;
    struct __nat {int __for_bool_;};
     constexpr nullptr_t2() : __lx(0) {}
     constexpr nullptr_t2(int __nat::*) : __lx(0) {}
     constexpr operator int __nat::*() const {return 0;}
    template <class _Tp>
         constexpr
        operator _Tp* () const {return 0;}
    template <class _Tp, class _Up>
        operator _Tp _Up::* () const {return 0;}
    friend  constexpr bool operator==(nullptr_t2, nullptr_t2) {return true;}
    friend  constexpr bool operator!=(nullptr_t2, nullptr_t2) {return false;}
};
inline constexpr nullptr_t2 __get_nullptr_t2() {return nullptr_t2(0);}
#define nullptr2 __get_nullptr_t2()

int main(){
    long l  = reinterpret_cast<long>(nullptr);
    long l2 = reinterpret_cast<long>(nullptr2); // error: invalid type conversion
    bool b  = nullptr; // warning: implicit conversion
                       // edditor error: a value of type "std::nullptr_t" cannot be used to initialize an entity of type "bool"
    bool b2 = nullptr2;
    if (nullptr){}; // warning: implicit conversion
    if (nullptr2){};
};
L. F.
  • 19,445
  • 8
  • 48
  • 82
Fullfungo
  • 345
  • 2
  • 13
  • 3
    `nullptr_t` is a fundamental type. How is `int` implemented? – L. F. Apr 18 '20 at 02:42
  • 10
    Note `#ifdef _LIBCPP_HAS_NO_NULLPTR`. This seems like a best-effort workaround for when the compiler doesn't provide `nullptr`. – chris Apr 18 '20 at 02:43
  • @L.F. Similar to how `float` is implemented – Ardent Coder Apr 18 '20 at 02:44
  • *nullptr* is not guaranteed to be implemented, it can have a special definition like the one in the code. – Fullfungo Apr 18 '20 at 02:47
  • 6
    @Fullfungo The standard says that `nullptr_t` is a fundamental type. Implementing it as a class type does not make a conforming implementation. See chris's comment. – L. F. Apr 18 '20 at 02:52
  • Related: [safe bool idiom](https://stackoverflow.com/a/6242355/9716597) [Is the safe-bool idiom obsolete in C++11?](https://stackoverflow.com/q/6242768/9716597) – L. F. Apr 18 '20 at 02:56
  • @L. F. Ok, makes sense. I still want to know what `__nat` does and why it is there, maybe this will help me in my own code. – Fullfungo Apr 18 '20 at 02:57
  • 1
    @L.F. Does the standard technically require that a fundamental type is not a class type? – eerorika Apr 18 '20 at 02:57
  • @Fullfungo it's basically used for the pre-C++11 [safe bool idiom](https://www.artima.com/cppsource/safebool.html). A simple `operator bool` will allow it to be converted to, say, `int` or class types constructible from `bool`. – L. F. Apr 18 '20 at 03:03
  • @eerorika Wow, I always assumed that to be true before, but seems the closest I can find is a [note](https://timsong-cpp.github.io/cppwp/n4659/basic.types#1) that says "There are two kinds of types: fundamental types and compound types" ... – L. F. Apr 18 '20 at 03:11
  • 2
    @eerorika: `is_class` and `is_null_pointer` cannot both be true for the same type. [Only one of the primary type category functions can return true for a specific type.](https://timsong-cpp.github.io/cppwp/n4659/meta.unary.cat#3) – Nicol Bolas Apr 18 '20 at 03:15
  • @NicolBolas In simple words: Primitive types and User-defined types are mutually exclusive classifications – Ardent Coder Apr 18 '20 at 03:19
  • @NicolBolas, Could the implementation _technically_ special-case it so those still return correct results? – chris Apr 18 '20 at 03:19
  • @chris: It's version of `is_class` and `is_null_pointer` could lie, making an explicit exception for this `std::nullptr_t` type by returning "incorrect" values. – Nicol Bolas Apr 18 '20 at 03:20
  • 1
    If it helps, the `nullptr` keyword is very similar to the `true` and `false` keywords: all are literals which represent values, for a type which has very few values in the first place. There are only two `bool` values, and only one `std::nullptr_t` value. – aschepler Apr 18 '20 at 11:13
  • @chris The implementation can do any kinds of "cheats" it likes to get behavior of conforming programs to act like the imaginary C++ "virtual machine", per the "as if" rule (https://timsong-cpp.github.io/cppwp/intro.abstract#1 and footnote). – aschepler Apr 18 '20 at 11:15

2 Answers2

23

I am curious to know how nullptr works.

It works in the simplest way possible: by fiat. It works because the C++ standard says it works, and it works the way it does because the C++ standard says that implementations must make it work in that fashion.

It's important to recognize that it is impossible to implement std::nullptr_t using the rules of the C++ language. The conversion from a null pointer constant of type std::nullptr_t to a pointer is not a user-defined conversion. That means that you can go from a null pointer constant to a pointer, then through a user-defined conversion to some other type, all in a single implicit conversion sequence.

That's not possible if you implement nullptr_t as a class. Conversion operators represent user-defined conversions, and C++'s implicit conversion sequence rules don't allow for more than one user-defined conversion in such a sequence.

So the code you posted is a nice approximation of std::nullptr_t, but it is nothing more than that. It is not a legitimate implementation of the type. This was probably from an older version of the compiler (left in for backwards-compatibility reasons) before the compiler provided proper support for std::nullptr_t. You can see this by the fact that it #defines nullptr, while C++11 says that nullptr is a keyword, not a macro.

C++ cannot implement std::nullptr_t, just as C++ cannot implement int or void*. Only the implementation can implement those things. This is what makes it a "fundamental type"; it's a part of the language.


its value can be converted to integral type identically to (void*)0, but not backwards;

There is no implicit conversion from a null pointer constant to integral types. There is a conversion from 0 to an integral type, but that's because it's the integer literal zero, which is... an integer.

nullptr_t can be cast to an integer type (via reinterpret_cast), but it can only be implicitly converted to pointers and to bool.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • What is meant by "it is impossible to implement std::nullptr_t using the rules of the C++ language"? Does it mean a C++ compiler cannot be completely written in C++ itself (I'm guessing not)? – northerner Apr 18 '20 at 13:50
  • 3
    @northerner: I mean that you cannot write a type that is exactly equivalent to the behavior required of `std::nullptr_t`. Just as you cannot write a type that is exactly equivalent to the behavior required of `int`. You can get close, but there will still be significant differences. And I'm not talking about trait detectors like `is_class` that expose that your type is user-defined. There are things about the required behavior of fundamental types that you simply cannot copy by using the rules of the language. – Nicol Bolas Apr 18 '20 at 13:58
  • 1
    Just a wording quibble. When you say "C++ cannot implement `nullptr_t`" you speak too broadly. And saying "only the implementation can implement it" only confuses matters. What you mean is that `nullptr_t` cannot be implemented" **in the C++ library** because it's part of the base language. – Spencer Apr 18 '20 at 15:35
  • 1
    @Spencer: No, I meant exactly what I said: C++ the language cannot be used to implement a type that does everything that `std::nullptr_t` is required to do. Just as C++ the language cannot implement a type that does everything that `int` is required to do. – Nicol Bolas Apr 18 '20 at 15:49
  • You did not say "C++ cannot be used to implement", you said "C++ cannot implement". If you edit your answer to say the former rather than the latter, I will be OK with that. – Spencer Apr 18 '20 at 16:00
  • @Spencer: From the first sentence, second paragraph: "it is impossible to implement std::nullptr_t using the rules of the C++ language." Context matters. – Nicol Bolas Apr 18 '20 at 16:01
  • And I'm not contradicting that. I just want you to word it better in your answer, because words matter. – Spencer Apr 18 '20 at 16:02
0

nullptr is a fundamental type, it's part of the C++ language implementation. Unlike to NULL keyword which is a macro evaluated in integer constant 0, the nullptr is actually a keyword implying pointer literal. As int is actually a builtin type in the language of size 4 byte (x64), nullptr is also a builtin type of size 8 byte (x64)

mada
  • 1,646
  • 1
  • 15