diff --git a/docs/source/api/usertype.rst b/docs/source/api/usertype.rst
index db746c49..5847100e 100644
--- a/docs/source/api/usertype.rst
+++ b/docs/source/api/usertype.rst
@@ -187,6 +187,16 @@ If you don't specify anything at all and the type is `destructible`_, then a des
 usertype regular function options
 +++++++++++++++++++++++++++++++++
 
+If you don't specify anything at all and the type ``T`` supports ``operator <``, ``operator <=``, or ``operator==`` (``const`` or non-``const`` qualified):
+
+* for ``operator <`` and ``operator <=`` 
+	- These two ``sol::meta_function::less_than(_or_equal_to)`` are generated for you and overriden in Lua.
+* for ``operator==``
+	- An equality operator will always be generated, doing pointer comparison if ``operator==`` on the two value types is not supported or doing a reference comparison and a value comparison if ``operator==`` is supported
+* heterogenous operators cannot be supported for equality, as Lua specifically checks if they use the same function to do the comparison: if they do not, then the equality method is not invoked; one way around this would be to write one ``int super_equality_function(lua_State* L) { ... }``, pull out arguments 1 and 2 from the stack for your type, and check all the types and then invoke ``operator==`` yourself after getting the types out of Lua (possibly using :ref:`sol::stack::get<stack-get>` and :ref:`sol::stack::check_get<stack-check-get>`)
+
+Otherwise, the following is used to specify functions to bind on the specific usertype for ``T``.
+
 * ``"{name}", &free_function``
 	- Binds a free function / static class function / function object (lambda) to ``"{name}"``. If the first argument is ``T*`` or ``T&``, then it will bind it as a member function. If it is not, it will be bound as a "static" function on the lua table
 * ``"{name}", &type::function_name`` or ``"{name}", &type::member_variable``
@@ -209,7 +219,7 @@ usertype arguments - simple usertype
 * ``sol::simple``
 	- Only allowed as the first argument to the usertype constructor, must be accompanied by a ``lua_State*``
 	- This tag triggers the :doc:`simple usertype<simple_usertype>` changes / optimizations
-	- Only supported when directly invoking the constructor (e.g. not when calling ``sol::table::new_usertype`` or ``sol::table::new_simple_usertype``) 
+	- Only supported when directly invoking the constructor (e.g. not when calling ``sol::table::new_usertype`` or ``sol::table::new_simple_usertype``)
 
 
 
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 098d3639..b8d77038 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -61,7 +61,7 @@ author = 'ThePhD'
 # The short X.Y version.
 version = '2.12'
 # The full version, including alpha/beta/rc tags.
-release = '2.12.0'
+release = '2.12.1'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/sol/container_usertype_metatable.hpp b/sol/container_usertype_metatable.hpp
index 8ac22d9f..9a958d5a 100644
--- a/sol/container_usertype_metatable.hpp
+++ b/sol/container_usertype_metatable.hpp
@@ -67,17 +67,17 @@ namespace sol {
 
 	}
 
-	template <typename T, typename C = void>
+	template <typename Raw, typename C = void>
 	struct container_usertype_metatable {
-		typedef meta::unqualified_t<T> U;
+		typedef meta::unqualified_t<Raw> T;
 		typedef std::size_t K;
-		typedef typename U::value_type V;
-		typedef typename U::iterator I;
+		typedef typename T::value_type V;
+		typedef typename T::iterator I;
 		struct iter {
-			U& source;
+			T& source;
 			I it;
 
-			iter(U& source, I it) : source(source), it(std::move(it)) {}
+			iter(T& source, I it) : source(source), it(std::move(it)) {}
 		};
 
 		static auto& get_src(lua_State* L) {
@@ -224,18 +224,18 @@ namespace sol {
 		}
 	};
 
-	template <typename T>
-	struct container_usertype_metatable<T, std::enable_if_t<meta::has_key_value_pair<T>::value>> {
-		typedef meta::unqualified_t<T> U;
-		typedef typename U::value_type KV;
+	template <typename Raw>
+	struct container_usertype_metatable<Raw, std::enable_if_t<meta::has_key_value_pair<meta::unqualified_t<Raw>>::value>> {
+		typedef meta::unqualified_t<Raw> T;
+		typedef typename T::value_type KV;
 		typedef typename KV::first_type K;
 		typedef typename KV::second_type V;
-		typedef typename U::iterator I;
+		typedef typename T::iterator I;
 		struct iter {
-			U& source;
+			T& source;
 			I it;
 
-			iter(U& source, I it) : source(source), it(std::move(it)) {}
+			iter(T& source, I it) : source(source), it(std::move(it)) {}
 		};
 
 		static auto& get_src(lua_State* L) {
diff --git a/sol/simple_usertype_metatable.hpp b/sol/simple_usertype_metatable.hpp
index eaa2f942..127be267 100644
--- a/sol/simple_usertype_metatable.hpp
+++ b/sol/simple_usertype_metatable.hpp
@@ -110,6 +110,9 @@ namespace sol {
 			typedef simple_usertype_metatable<T> umt_t;
 			
 			static int push(lua_State* L, umt_t&& umx) {
+				bool hasequals = false;
+				bool hasless = false;
+				bool haslessequals = false;
 				for (std::size_t i = 0; i < 3; ++i) {
 					// Pointer types, AKA "references" from C++
 					const char* metakey = nullptr;
@@ -128,25 +131,53 @@ namespace sol {
 					luaL_newmetatable(L, metakey);
 					stack_reference t(L, -1);
 					for (auto& kvp : umx.registrations) {
-						switch (i) {
-						case 0:
-							if (kvp.first.template is<std::string>() && kvp.first.template as<std::string>() == "__gc") {
-								continue;
+						if (kvp.first.template is<std::string>()) {
+							std::string regname = kvp.first.template as<std::string>();
+							if (regname == name_of(meta_function::equal_to)) {
+								hasequals = true;
 							}
-							break;
-						case 1:
-							if (kvp.first.template is<std::string>() && kvp.first.template as<std::string>() == "__gc") {
-								stack::set_field(L, kvp.first, detail::unique_destruct<T>, t.stack_index());
-								continue;
+							else if (regname == name_of(meta_function::less_than)) {
+								hasless = true;
+							}
+							else if (regname == name_of(meta_function::less_than_or_equal_to)) {
+								haslessequals = true;
+							}
+							switch (i) {
+							case 0:
+								if (regname == name_of(meta_function::garbage_collect)) {
+									continue;
+								}
+								break;
+							case 1:
+								if (regname == name_of(meta_function::garbage_collect)) {
+									stack::set_field(L, kvp.first, detail::unique_destruct<T>, t.stack_index());
+									continue;
+								}
+								break;
+							case 2:
+							default:
+								break;
 							}
-							break;
-						case 2:
-						default:
-							break;
 						}
 						stack::set_field(L, kvp.first, kvp.second, t.stack_index());
 					}
-
+					luaL_Reg opregs[4]{};
+					int opregsindex = 0;
+					if (!hasless) {
+						const char* name = name_of(meta_function::less_than).c_str();
+						usertype_detail::make_reg_op<T, std::less<>, meta::supports_op_less<T>>(opregs, opregsindex, name);
+					}
+					if (!haslessequals) {
+						const char* name = name_of(meta_function::less_than_or_equal_to).c_str();
+						usertype_detail::make_reg_op<T, std::less_equal<>, meta::supports_op_less_equal<T>>(opregs, opregsindex, name);
+					}
+					if (!hasequals) {
+						const char* name = name_of(meta_function::equal_to).c_str();
+						usertype_detail::make_reg_op<T, std::conditional_t<meta::supports_op_equal<T>::value, std::equal_to<>, usertype_detail::no_comp>, std::true_type>(opregs, opregsindex, name);
+					}
+					t.push();
+					luaL_setfuncs(L, opregs, 0);
+					t.pop();
 					// Metatable indexes itself
 					stack::set_field(L, meta_function::index, t, t.stack_index());
 
diff --git a/sol/traits.hpp b/sol/traits.hpp
index e2b44fa1..8d7195fd 100644
--- a/sol/traits.hpp
+++ b/sol/traits.hpp
@@ -262,37 +262,54 @@ namespace sol {
 				static const bool value = sizeof(test<Derived>(0)) == sizeof(yes);
 			};
 
+			struct has_begin_end_impl {
+				template<typename T, typename U = unqualified_t<T>,
+					typename B = decltype(std::declval<U&>().begin()),
+					typename E = decltype(std::declval<U&>().end())>
+					static std::true_type test(int);
+
+				template<typename...>
+				static std::false_type test(...);
+			};
+
+			struct has_key_value_pair_impl {
+				template<typename T, typename U = unqualified_t<T>,
+					typename V = typename U::value_type,
+					typename F = decltype(std::declval<V&>().first),
+					typename S = decltype(std::declval<V&>().second)>
+					static std::true_type test(int);
+
+				template<typename...>
+				static std::false_type test(...);
+			};
+
+			template <typename T, typename U = T, typename = decltype(std::declval<T&>() < std::declval<U&>())>
+			std::true_type supports_op_less_test(const T&);
+			std::false_type supports_op_less_test(...);
+			template <typename T, typename U = T, typename = decltype(std::declval<T&>() == std::declval<U&>())>
+			std::true_type supports_op_equal_test(const T&);
+			std::false_type supports_op_equal_test(...);
+			template <typename T, typename U = T, typename = decltype(std::declval<T&>() <= std::declval<U&>())>
+			std::true_type supports_op_less_equal_test(const T&);
+			std::false_type supports_op_less_equal_test(...);
+
 		} // meta_detail
 
+		template <typename T>
+		using supports_op_less = decltype(meta_detail::supports_op_less_test(std::declval<T&>()));
+		template <typename T>
+		using supports_op_equal = decltype(meta_detail::supports_op_equal_test(std::declval<T&>()));
+		template <typename T>
+		using supports_op_less_equal = decltype(meta_detail::supports_op_less_equal_test(std::declval<T&>()));
+
 		template<typename T>
 		struct is_callable : boolean<meta_detail::is_callable<T>::value> {};
 
-		struct has_begin_end_impl {
-			template<typename T, typename U = unqualified_t<T>,
-				typename B = decltype(std::declval<U&>().begin()),
-				typename E = decltype(std::declval<U&>().end())>
-				static std::true_type test(int);
-
-			template<typename...>
-			static std::false_type test(...);
-		};
+		template<typename T>
+		struct has_begin_end : decltype(meta_detail::has_begin_end_impl::test<T>(0)) {};
 
 		template<typename T>
-		struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};
-
-		struct has_key_value_pair_impl {
-			template<typename T, typename U = unqualified_t<T>,
-				typename V = typename U::value_type,
-				typename F = decltype(std::declval<V&>().first),
-				typename S = decltype(std::declval<V&>().second)>
-				static std::true_type test(int);
-
-			template<typename...>
-			static std::false_type test(...);
-		};
-
-		template<typename T>
-		struct has_key_value_pair : decltype(has_key_value_pair_impl::test<T>(0)) {};
+		struct has_key_value_pair : decltype(meta_detail::has_key_value_pair_impl::test<T>(0)) {};
 
 		template <typename T>
 		using is_string_constructible = any<std::is_same<unqualified_t<T>, const char*>, std::is_same<unqualified_t<T>, char>, std::is_same<unqualified_t<T>, std::string>, std::is_same<unqualified_t<T>, std::initializer_list<char>>>;
diff --git a/sol/usertype_metatable.hpp b/sol/usertype_metatable.hpp
index bec8dcb8..dbfea5d3 100644
--- a/sol/usertype_metatable.hpp
+++ b/sol/usertype_metatable.hpp
@@ -33,8 +33,14 @@
 #include "deprecate.hpp"
 
 namespace sol {
-
 	namespace usertype_detail {
+		struct no_comp {
+			template <typename A, typename B>
+			bool operator()(A&&, B&&) {
+				return false;
+			}
+		};
+
 		inline bool is_indexer(string_detail::string_shim s) {
 			return s == name_of(meta_function::index) || s == name_of(meta_function::new_index);
 		}
@@ -88,6 +94,37 @@ namespace sol {
 				return luaL_error(L, "sol: attempt to index (set) nil value \"%s\" on userdata (bad (misspelled?) key name or does not exist)", accessor.data());
 		}
 
+		template <typename T, typename Op>
+		inline int operator_wrap(lua_State* L) {
+			auto maybel = stack::check_get<T>(L, 1);
+			if (maybel) {
+				auto mayber = stack::check_get<T>(L, 2);
+				if (mayber) {
+					auto& l = *maybel;
+					auto& r = *mayber;
+					if (std::is_same<no_comp, Op>::value) {
+						return stack::push(L, detail::ptr(l) == detail::ptr(r));
+					}
+					else {
+						Op op;
+						return stack::push(L, (detail::ptr(l) == detail::ptr(r)) || op(detail::deref(l), detail::deref(r)));
+					}
+				}
+			}
+			return stack::push(L, false);
+		}
+
+		template <typename T, typename Op, typename Supports, typename Regs, meta::enable<Supports> = meta::enabler>
+		inline void make_reg_op(Regs& l, int& index, const char* name) {
+			l[index] = { name, &operator_wrap<T, Op> };
+			++index;
+		}
+
+		template <typename T, typename Op, typename Supports, typename Regs, meta::disable<Supports> = meta::enabler>
+		inline void make_reg_op(Regs&, int&, const char*) {
+			// Do nothing if there's no support
+		}
+
 		struct add_destructor_tag {};
 		struct check_destructor_tag {};
 		struct verified_tag {} const verified{};
@@ -136,7 +173,7 @@ namespace sol {
 	struct usertype_metatable<T, std::index_sequence<I...>, Tn...> : usertype_detail::registrar {
 		typedef std::make_index_sequence<sizeof...(I) * 2> indices;
 		typedef std::index_sequence<I...> half_indices;
-		typedef std::array<luaL_Reg, sizeof...(Tn) / 2 + 1> regs_t;
+		typedef std::array<luaL_Reg, sizeof...(Tn) / 2 + 1 + 3> regs_t;
 		typedef std::tuple<Tn...> RawTuple;
 		typedef std::tuple<clean_type_t<Tn> ...> Tuple;
 		template <std::size_t Idx>
@@ -155,6 +192,9 @@ namespace sol {
 		void* baseclasscast;
 		bool mustindex;
 		bool secondarymeta;
+		bool hasequals;
+		bool hasless;
+		bool haslessequals;
 
 		template <std::size_t Idx, meta::enable<std::is_same<lua_CFunction, meta::unqualified_tuple_element<Idx + 1, RawTuple>>> = meta::enabler>
 		inline lua_CFunction make_func() {
@@ -178,6 +218,18 @@ namespace sol {
 		}
 
 		int finish_regs(regs_t& l, int& index) {
+			if (!hasless) {
+				const char* name = name_of(meta_function::less_than).c_str();
+				usertype_detail::make_reg_op<T, std::less<>, meta::supports_op_less<T>>(l, index, name);
+			}
+			if (!haslessequals) {
+				const char* name = name_of(meta_function::less_than_or_equal_to).c_str();
+				usertype_detail::make_reg_op<T, std::less_equal<>, meta::supports_op_less_equal<T>>(l, index, name);
+			}
+			if (!hasequals) {
+				const char* name = name_of(meta_function::equal_to).c_str();
+				usertype_detail::make_reg_op<T, std::conditional_t<meta::supports_op_equal<T>::value, std::equal_to<>, usertype_detail::no_comp>, std::true_type>(l, index, name);
+			}
 			if (destructfunc != nullptr) {
 				l[index] = { name_of(meta_function::garbage_collect).c_str(), destructfunc };
 				++index;
@@ -216,6 +268,15 @@ namespace sol {
 			// Returnable scope
 			// That would be a neat keyword for C++
 			// returnable { ... };
+			if (reg.name == name_of(meta_function::equal_to)) {
+				hasequals = true;
+			}
+			if (reg.name == name_of(meta_function::less_than)) {
+				hasless = true;
+			}
+			if (reg.name == name_of(meta_function::less_than_or_equal_to)) {
+				haslessequals = true;
+			}
 			if (reg.name == name_of(meta_function::garbage_collect)) {
 				destructfunc = reg.func;
 				return;
@@ -241,7 +302,8 @@ namespace sol {
 		indexbase(&core_indexing_call<true>), newindexbase(&core_indexing_call<false>),
 		indexbaseclasspropogation(walk_all_bases<true>), newindexbaseclasspropogation(walk_all_bases<false>),
 		baseclasscheck(nullptr), baseclasscast(nullptr), 
-		mustindex(contains_variable() || contains_index()), secondarymeta(contains_variable()) {
+		mustindex(contains_variable() || contains_index()), secondarymeta(contains_variable()),
+		hasequals(false), hasless(false), haslessequals(false) {
 		}
 
 		template <std::size_t I0, std::size_t I1, bool is_index>
@@ -414,9 +476,9 @@ namespace sol {
 				(void)detail::swallow{ 0, (um.template make_regs<(I * 2)>(value_table, lastreg, std::get<(I * 2)>(um.functions), std::get<(I * 2 + 1)>(um.functions)), 0)... };
 				um.finish_regs(value_table, lastreg);
 				value_table[lastreg] = { nullptr, nullptr };
-				bool hasdestructor = !value_table.empty() && name_of(meta_function::garbage_collect) == value_table[lastreg - 1].name;
 				regs_t ref_table = value_table;
 				regs_t unique_table = value_table;
+				bool hasdestructor = !value_table.empty() && name_of(meta_function::garbage_collect) == value_table[lastreg - 1].name;
 				if (hasdestructor) {
 					ref_table[lastreg - 1] = { nullptr, nullptr };
 					unique_table[lastreg - 1] = { value_table[lastreg - 1].name, detail::unique_destruct<T> };
diff --git a/test_operators.cpp b/test_operators.cpp
new file mode 100644
index 00000000..b8e48e78
--- /dev/null
+++ b/test_operators.cpp
@@ -0,0 +1,83 @@
+#define SOL_CHECK_ARGUMENTS
+
+#include <sol.hpp>
+#include <catch.hpp>
+
+TEST_CASE("operators/default", "test that generic equality operators and all sorts of equality tests can be used") {
+	sol::state lua;
+	lua.open_libraries(sol::lib::base);
+
+	struct T {};
+	struct U {
+		int a;
+		U(int x = 20) : a(x) {}
+		bool operator==(const U& r) {
+			return a == r.a;
+		}
+	};
+	struct V {
+		int a;
+		V(int x = 20) : a(x) {}
+		bool operator==(const V& r) const {
+			return a == r.a;
+		}
+	};
+	lua.new_usertype<T>("T");
+	lua.new_usertype<U>("U");
+	lua.new_usertype<V>("V");
+
+	T t1;
+	T& t2 = t1;
+	T t3;
+	U u1;
+	U u2{ 30 };
+	U u3;
+	U v1;
+	U v2{ 30 };
+	U v3;
+	lua["t1"] = &t1;
+	lua["t2"] = &t2;
+	lua["t3"] = &t3;
+	lua["u1"] = &u1;
+	lua["u2"] = &u2;
+	lua["u3"] = &u3;
+	lua["v1"] = &v1;
+	lua["v2"] = &v2;
+	lua["v3"] = &v3;
+
+	// Can only compare identity here
+	REQUIRE_NOTHROW({
+		lua.script("assert(t1 == t1)");
+		lua.script("assert(t2 == t2)");
+		lua.script("assert(t3 == t3)");
+	});
+	REQUIRE_NOTHROW({
+		lua.script("assert(t1 == t2)");
+		lua.script("assert(not (t1 == t3))");
+		lua.script("assert(not (t2 == t3))");
+	});
+	// Object should compare equal to themselves
+	// (and not invoke operator==; pointer test should be sufficient)
+	REQUIRE_NOTHROW({
+		lua.script("assert(u1 == u1)");
+		lua.script("assert(u2 == u2)");
+		lua.script("assert(u3 == u3)");
+	});
+	REQUIRE_NOTHROW({
+		lua.script("assert(not (u1 == u2))");
+		lua.script("assert(u1 == u3)");
+		lua.script("assert(not (u2 == u3))");
+	});
+	// Object should compare equal to themselves
+	// (and not invoke operator==; pointer test should be sufficient)
+	REQUIRE_NOTHROW({
+		lua.script("assert(v1 == v1)");
+		lua.script("assert(v2 == v2)");
+		lua.script("assert(v3 == v3)");
+	});
+	REQUIRE_NOTHROW({
+		lua.script("assert(not (v1 == v2))");
+		lua.script("assert(v1 == v3)");
+		lua.script("assert(not (v2 == v3))");
+	});
+}
\ No newline at end of file