Jak działa implementacja cull ntrptr?

13

Jestem ciekawy, jak nullptrdziała. Normy N4659 i N4849 mówią:

  1. musi mieć typ std::nullptr_t;
  2. nie możesz wziąć jego adresu;
  3. można go bezpośrednio przekonwertować na wskaźnik, a wskaźnik na element członkowski;
  4. sizeof(std::nullptr_t) == sizeof(void*);
  5. jego konwersja na booljest false;
  6. jego wartość można przekonwertować na typ całkowy identycznie (void*)0, ale nie wstecz;

Jest to w zasadzie stała o takim samym znaczeniu jak (void*)0, ale ma inny typ. Znalazłem implementację std::nullptr_tna moim urządzeniu i wygląda ona następująco.

#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

Bardziej interesuje mnie jednak pierwsza część. Wydaje się, że spełnia kryteria 1-5, ale nie mam pojęcia, dlaczego ma podklasę __nat i wszystko, co się z tym wiąże. Chciałbym również wiedzieć, dlaczego nie udaje się to przy zintegrowanych konwersjach.

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){};
};
Fullfungo
źródło
2
nullptr_tjest typem podstawowym. Jak intrealizowany?
LF
9
Uwaga #ifdef _LIBCPP_HAS_NO_NULLPTR. Wydaje się, że jest to obejście problemu, gdy kompilator nie zapewnia nullptr.
Chris
5
@Fullfungo Standard mówi, że nullptr_tjest to typ podstawowy. Implementacja go jako typu klasy nie stanowi zgodnej implementacji. Zobacz komentarz Chris.
LF
1
@LF Czy standard wymaga, aby typ podstawowy nie był typem klasy?
eerorika
2
@eerorika: is_classi is_null_pointernie mogą być prawdziwe dla tego samego typu. Tylko jedna z funkcji kategorii typu podstawowego może zwrócić wartość true dla określonego typu.
Nicol Bolas,

Odpowiedzi:

20

Jestem ciekawy, jak działa nullptr.

Działa w najprostszy możliwy sposób: przez fiat . Działa, ponieważ standard C ++ mówi, że działa, i działa tak, jak działa, ponieważ standard C ++ mówi, że implementacje muszą działać w ten sposób.

Ważne jest, aby uznać, że niemożliwe jest wdrożenie std::nullptr_tprzy użyciu reguł języka C ++. Konwersja stałej typu pusty wskaźnik std::nullptr_tna wskaźnik nie jest konwersją zdefiniowaną przez użytkownika. Oznacza to, że możesz przejść od stałej zerowej wskaźnika do wskaźnika, a następnie poprzez konwersję zdefiniowaną przez użytkownika do innego typu, wszystko w jednej niejawnej sekwencji konwersji.

Nie jest to możliwe, jeśli zaimplementujesz nullptr_tjako klasę. Operatory konwersji reprezentują konwersje zdefiniowane przez użytkownika, a niejawne reguły sekwencji konwersji C ++ nie pozwalają na więcej niż jedną konwersję zdefiniowaną przez użytkownika w takiej sekwencji.

Więc kod, który opublikowałeś, jest dobrym przybliżeniem std::nullptr_t, ale to nic więcej. To nie jest legalna implementacja tego typu. Prawdopodobnie pochodziło to ze starszej wersji kompilatora (pozostawionej w celu zachowania kompatybilności wstecznej), zanim kompilator zapewnił odpowiednią obsługę std::nullptr_t. Widać to po tym, że tak #definejest nullptr, podczas gdy C ++ 11 mówi, że nullptrto słowo kluczowe , a nie makro.

C ++ nie może zaimplementować std::nullptr_t, tak jak C ++ nie może zaimplementować intlub void*. Tylko wdrożenie może zaimplementować te rzeczy. Właśnie dlatego jest to „typ podstawowy”; to część języka .


jego wartość można przekonwertować na typ całkowy identycznie na (void *) 0, ale nie wstecz;

Nie ma niejawnej konwersji ze stałej wskaźnika zerowego na typy całkowe. Istnieje konwersja z 0na typ integralny, ale to dlatego, że jest to całkowite zero dosłowne, które jest ... liczbą całkowitą.

nullptr_tmożna rzutować na typ całkowity (via reinterpret_cast), ale można go domyślnie przekonwertować tylko na wskaźniki i na bool.

Nicol Bolas
źródło
4
@Wyck: „ fiat
Nicol Bolas
Co oznacza „niemożliwe jest zaimplementowanie std :: nullptr_t przy użyciu reguł języka C ++”? Czy to oznacza, że ​​kompilatora C ++ nie można całkowicie napisać w samym C ++ (chyba nie)?
northerner
3
@northerner: Mam na myśli, że nie możesz napisać typu, który jest dokładnie równoważny wymaganemu zachowaniu std::nullptr_t. Tak jak nie możesz napisać typu, który jest dokładnie równoważny wymaganemu zachowaniu int. Możesz się zbliżyć, ale nadal będą znaczące różnice. I nie mówię o takich wykrywaczach cech, is_classktóre ujawniają, że twój typ jest zdefiniowany przez użytkownika. Są pewne rzeczy dotyczące wymaganego zachowania podstawowych typów, których po prostu nie można skopiować za pomocą reguł języka.
Nicol Bolas
1
Tylko spory sformułowanie. Kiedy mówisz „C ++ nie może zaimplementować nullptr_t”, mówisz zbyt szeroko. A powiedzenie „tylko implementacja może to zaimplementować” tylko myli sprawy. Masz na myśli to, że nullptr_tnie można go wdrożyć " w bibliotece C ++, ponieważ jest on częścią języka podstawowego.
Spencer
1
@Spencer: Nie, miałem na myśli dokładnie to, co powiedziałem: C ++ języka nie można użyć do zaimplementowania typu, który robi wszystko, co std::nullptr_tjest wymagane. Podobnie jak C ++, język nie może implementować typu, który robi wszystko, co intjest wymagane.
Nicol Bolas