diff --git a/README.md b/README.md
index e21a2e11d..b567fd82b 100644
--- a/README.md
+++ b/README.md
@@ -425,19 +425,6 @@ auto version = registry.version(entity);
auto curr = registry.current(entity);
```
-Finally, there is also a sort of _null identifier_ made available to users. It's
-treated as if it were a _null pointer_ that doesn't identify any entity. A
-registry will reject this identifier in all cases because it isn't considered
-valid.
-The rules that define a _null identifier_ are a bit tricky to explain. However,
-being `Entity` the type of the entities (for example, `std::uint32_t`), users
-can easily construct a _null identifier_ by flipping all the bits of the _zero_:
-
-```cpp
-using Entity = std::uint32_t;
-const auto null = ~Entity{};
-```
-
Components can be assigned to or removed from entities at any time with a few
calls to member functions of the registry. As for the entities, the registry
offers also a set of functionalities users can use to work with the components.
@@ -1136,6 +1123,34 @@ A dependency can easily be broken by means of the same function template:
entt::dependency(entt::break_t{}, registry.construction());
```
+### Null entity
+
+In `EnTT`, there exists a sort of _null entity_ made available to users that is
+accessible via the `entt::null` variable.
+The framework guarantees that the following expression always returns false:
+
+```cpp
+registry.valid(entt::null);
+```
+
+In other terms, a registry will reject the null entity in all cases because it
+isn't considered valid. It means that the null entity cannot own components or
+tags for obvious reasons.
+The type of the null entity is internal and should not be used for any purpose
+other than defining the null entity itself. However, there exist implicit
+conversions from the null entity to identifiers of any allowed type:
+
+```cpp
+typename entt::DefaultRegistry::entity_type null = entt::null;
+```
+
+Similarly, the null entity can be compared to any other identifier:
+
+```cpp
+const auto entity = registry.create();
+const bool null = (entity == entt::null);
+```
+
## View: to persist or not to persist?
First of all, it is worth answering an obvious question: why views?
diff --git a/src/entt/entity/entity.hpp b/src/entt/entity/entity.hpp
new file mode 100644
index 000000000..0e24a4238
--- /dev/null
+++ b/src/entt/entity/entity.hpp
@@ -0,0 +1,87 @@
+#ifndef ENTT_ENTITY_ENTITY_HPP
+#define ENTT_ENTITY_ENTITY_HPP
+
+
+#include "../config/config.h"
+#include "entt_traits.hpp"
+
+
+namespace entt {
+
+
+namespace internal {
+
+
+/**
+ * @cond TURN_OFF_DOXYGEN
+ * Internal details not to be documented.
+ */
+
+
+template
+static constexpr auto null = ~typename entt_traits::entity_type{};
+
+
+struct Null {
+ explicit constexpr Null() = default;
+
+ template
+ constexpr operator Entity() const ENTT_NOEXCEPT {
+ return null;
+ }
+
+ constexpr bool operator==(Null) const ENTT_NOEXCEPT {
+ return true;
+ }
+
+ constexpr bool operator!=(Null) const ENTT_NOEXCEPT {
+ return false;
+ }
+
+ template
+ constexpr bool operator==(const Entity entity) const ENTT_NOEXCEPT {
+ return entity == null;
+ }
+
+ template
+ constexpr bool operator!=(const Entity entity) const ENTT_NOEXCEPT {
+ return entity != null;
+ }
+};
+
+
+template
+constexpr bool operator==(const Entity entity, Null null) ENTT_NOEXCEPT {
+ return null == entity;
+}
+
+
+template
+constexpr bool operator!=(const Entity entity, Null null) ENTT_NOEXCEPT {
+ return null != entity;
+}
+
+
+/**
+ * Internal details not to be documented.
+ * @endcond TURN_OFF_DOXYGEN
+ */
+
+
+}
+
+
+/**
+ * @brief Null entity.
+ *
+ * There exist implicit conversions from this variable to entity identifiers of
+ * any allowed type. Similarly, there exist comparision operators between the
+ * null entity and any other entity identifier.
+ */
+constexpr auto null = internal::Null{};
+
+
+}
+
+
+#endif // ENTT_ENTITY_ENTITY_HPP
diff --git a/src/entt/entt.hpp b/src/entt/entt.hpp
index 78c86d9e3..b9df9341f 100644
--- a/src/entt/entt.hpp
+++ b/src/entt/entt.hpp
@@ -3,6 +3,7 @@
#include "core/hashed_string.hpp"
#include "core/ident.hpp"
#include "entity/actor.hpp"
+#include "entity/entity.hpp"
#include "entity/entt_traits.hpp"
#include "entity/helper.hpp"
#include "entity/prototype.hpp"
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index c958f1f23..697a7d3a3 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -53,6 +53,7 @@ ADD_ENTT_TEST(ident entt/core/ident.cpp)
# Test entity
ADD_ENTT_TEST(actor entt/entity/actor.cpp)
+ADD_ENTT_TEST(entity entt/entity/entity.cpp)
ADD_ENTT_TEST(helper entt/entity/helper.cpp)
ADD_ENTT_TEST(prototype entt/entity/prototype.cpp)
ADD_ENTT_TEST(registry entt/entity/registry.cpp)
diff --git a/test/entt/entity/entity.cpp b/test/entt/entity/entity.cpp
new file mode 100644
index 000000000..ac9d3954d
--- /dev/null
+++ b/test/entt/entity/entity.cpp
@@ -0,0 +1,27 @@
+#include
+#include
+#include
+#include
+
+template
+struct S {};
+
+TEST(Traits, Null) {
+ entt::DefaultRegistry registry{};
+
+ const auto entity = registry.create();
+ registry.assign(entity, 42);
+
+ ASSERT_TRUE(~typename entt::DefaultRegistry::entity_type{} == entt::null);
+
+ ASSERT_TRUE(entt::null == entt::null);
+ ASSERT_FALSE(entt::null != entt::null);
+
+ ASSERT_FALSE(entity == entt::null);
+ ASSERT_FALSE(entt::null == entity);
+
+ ASSERT_TRUE(entity != entt::null);
+ ASSERT_TRUE(entt::null != entity);
+
+ ASSERT_FALSE(registry.valid(entt::null));
+}