diff --git a/include/tscpp/util/Comparable.h b/include/tscpp/util/Comparable.h new file mode 100644 index 00000000000..2f55141d94b --- /dev/null +++ b/include/tscpp/util/Comparable.h @@ -0,0 +1,196 @@ +/** @file + + Given a class T with a compare function that returns ordering information, create the standard + comparison operators. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one or more contributor license + agreements. See the NOTICE file distributed with this work for additional information regarding + copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with the License. You may obtain + a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under the License + is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied. See the License for the specific language governing permissions and limitations under + the License. + */ + +#pragma once + +#include +#include +#include "tscpp/util/ts_meta.h" + +namespace ts +{ +/** This is a manual override for comparison. + * + * @tparam T Left hand side operand type. + * @tparam U Right hand side operand type. + * + * Specialize this for @a T and @a U if the normal mechanisms yield bad results. This has the highest + * priority for selecting a compare function. + */ +template struct ComparablePolicy { +}; + +namespace detail +{ + // This is an ordered set of ways to call the compare function for two types, @a T and @a U. + // One of these is called only if the operator overload detects at least one operand that + // is a @c Comparable. Therefore no checks for appropriate types are needed, and this sequence + // only probes the target types for supported free functions and methods. + + /// Use @c ComparePolicy if it has been specialized to @a T and @a U. + template + auto + ComparableFunction(T const &lhs, U const &rhs, meta::CaseTag<4> const &) -> decltype(ComparablePolicy()(lhs, rhs), int()) + { + return static_cast(ComparablePolicy()(lhs, rhs)); + } + + /// Use the free function @c cmp(T,U). + template + auto + ComparableFunction(T const &lhs, U const &rhs, meta::CaseTag<3> const &) -> decltype(cmp(lhs, rhs), int()) + { + return static_cast(cmp(lhs, rhs)); + } + + /// Use the free function @c cmp(U,T) and flip the result. + template + auto + ComparableFunction(T const &lhs, U const &rhs, meta::CaseTag<2> const &) -> decltype(cmp(rhs, lhs), int()) + { + return static_cast(cmp(rhs, lhs)) * -1; + } + + /// Use the @c T::cmp method. + template + auto + ComparableFunction(T const &lhs, U const &rhs, meta::CaseTag<1> const &) -> decltype(lhs.cmp(rhs), int()) + { + return static_cast(lhs.cmp(rhs)); + } + + /// Use @c U::cmp and flip the result. + template + auto + ComparableFunction(T const &lhs, U const &rhs, meta::CaseTag<0> const &) -> decltype(rhs.cmp(lhs), int()) + { + return static_cast(rhs.cmp(lhs)) * -1; + } + +} // namespace detail + +/** Create standard comparison operators given a compare function. + * + * The operators supported are @c == @c != @c \< @c \> @c \<= @c \>= + * + * To successfully use this mixin, there are two requirements. + * - There must be a ternary comparison that returns an int in the standard ternary compare style. + * - The class must inherit from this mixin. + * + * The standard ternary compare must return an @c int which is + * - negative if @a lhs is smaller than @a rhs + * - 0 if @a lhs is equal to @a rhs + * - positive if @a lhs is greater than @a rhs + * + * If a comparison operator is used and at least one of the operands inherits from @c Comparable + * then the types are probed for ternary comparisons. The order is + * + * - @c ts::ComparablePolicy specialization + * - function @c cmp(lhs,rhs) + * - function @c cmp(rhs,lhs) + * - method @c lhs::cmp(rhs) + * - method @c rhs::cmp(lhs) + * + * Once a class inherits from this mixin, then comparisons are supported against any type for + * which a ternary compare can be found. + * + * @code + * class T : public ts::Comparable + * @endcode + * + * To provide self comparison operators, it would suffice to have + * + * @code + * int cmp(T const& that) const; + * @endcode + * + * If comparison operators against @c std::string_view should supported, this could be done by adding + * + * @code + * int cmp(std::string_view that) { return strcmp(text, that); } + * @endcode + * + * If both of these are present, then the following are now valid + * @code + * T t, t1, t2; + * if ("walt"sv == t) {...} + * if ("walt" == t) {...} // because string literals convert to string_view + * if (t1 < t2) {...} + * @endcode + */ +struct Comparable { +}; + +// --- +// For each comparison operator, a template overload is provided. These overloads are enabled for +// overload resolution iff at least one operand inherits from @c ts::Comparable. In that case the +// @c ComparableFunction machinery is engaged to probe for a ternary comparison. All of this should +// optimize away after compilation. + +template +auto +operator==(T const &lhs, U const &rhs) -> + typename std::enable_if::value || std::is_base_of::value, bool>::type +{ + return 0 == detail::ComparableFunction(lhs, rhs, meta::CaseArg); +} + +template +auto +operator!=(T const &lhs, U const &rhs) -> + typename std::enable_if::value || std::is_base_of::value, bool>::type +{ + return 0 != detail::ComparableFunction(lhs, rhs, meta::CaseArg); +} + +template +auto +operator<(T const &lhs, U const &rhs) -> + typename std::enable_if::value || std::is_base_of::value, bool>::type +{ + return detail::ComparableFunction(lhs, rhs, meta::CaseArg) < 0; +} + +template +auto +operator<=(T const &lhs, U const &rhs) -> + typename std::enable_if::value || std::is_base_of::value, bool>::type +{ + return detail::ComparableFunction(lhs, rhs, meta::CaseArg) <= 0; +} + +template +auto +operator>(T const &lhs, U const &rhs) -> + typename std::enable_if::value || std::is_base_of::value, bool>::type +{ + return detail::ComparableFunction(lhs, rhs, meta::CaseArg) > 0; +} + +template +auto +operator>=(T const &lhs, U const &rhs) -> + typename std::enable_if::value || std::is_base_of::value, bool>::type +{ + return detail::ComparableFunction(lhs, rhs, meta::CaseArg) >= 0; +} + +} // end namespace ts diff --git a/src/tscpp/util/Makefile.am b/src/tscpp/util/Makefile.am index 67ca3be9763..6922626ef50 100644 --- a/src/tscpp/util/Makefile.am +++ b/src/tscpp/util/Makefile.am @@ -37,6 +37,7 @@ test_tscpputil_CXXFLAGS = -Wno-array-bounds $(AM_CXXFLAGS) test_tscpputil_LDADD = libtscpputil.la test_tscpputil_SOURCES = \ unit_tests/unit_test_main.cc \ + unit_tests/test_Comparable.cc \ unit_tests/test_MemSpan.cc \ unit_tests/test_PostScript.cc \ unit_tests/test_TextView.cc \ diff --git a/src/tscpp/util/unit_tests/test_Comparable.cc b/src/tscpp/util/unit_tests/test_Comparable.cc new file mode 100644 index 00000000000..72b350d2325 --- /dev/null +++ b/src/tscpp/util/unit_tests/test_Comparable.cc @@ -0,0 +1,205 @@ +/** @file + + Unit tests for ts::Comparable. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one or more contributor license + agreements. See the NOTICE file distributed with this work for additional information regarding + copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with the License. You may + obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. + */ + +#include + +#include "tscpp/util/Comparable.h" +#include "tscpp/util/TextView.h" + +#include "catch.hpp" + +struct Alpha : public ts::Comparable { + explicit Alpha(int x) : _n(x) {} + int _n{0}; +}; + +int +cmp(Alpha const &lhs, Alpha const &rhs) +{ + return lhs._n - rhs._n; +} + +int +cmp(Alpha const &lhs, int rhs) +{ + return lhs._n - rhs; +} + +int +cmp(int lhs, Alpha const &rhs) +{ + return lhs - rhs._n; +} + +struct Bravo : public ts::Comparable { + explicit Bravo(float x) : _f(x) {} + float _f{0}; + + float + cmp(Bravo const &that) const + { + return _f - that._f; + } +}; + +int +cmp(Alpha const &lhs, Bravo const &rhs) +{ + return lhs._n < rhs._f ? -1 : lhs._n > rhs._f ? 1 : 0; +} + +int +cmp(Bravo const &lhs, Alpha const &rhs) +{ + return lhs._f < rhs._n ? -1 : lhs._f > rhs._n ? 1 : 0; +} + +struct Charlie : public ts::Comparable { + explicit Charlie(intmax_t x) : _n(x) {} + + intmax_t _n{0}; + + intmax_t + cmp(Charlie const &that) const + { + return _n - that._n; + } + int + cmp(int x) const + { + return _n - x; + } +}; + +struct Delta : public ts::Comparable { + explicit Delta(std::string_view const &s) : _s(s) {} + + std::string _s; + + int + cmp(std::string_view const &x) const + { + return ts::strcmp(_s, x); + } + + // Verify we can override the use of `cmp`. + int + self_cmp(Delta const &that) const + { + return ts::strcmp(_s, that._s); + } +}; + +// Tell Comparable to use self_cmp instead of cmp. +template <> struct ts::ComparablePolicy { + int + operator()(Delta const &lhs, Delta const &rhs) const + { + return lhs.self_cmp(rhs); + } +}; + +// Test inheritance +struct Echo : public Charlie { + explicit Echo(intmax_t x) : Charlie{x} {} +}; + +int +cmp(Echo const &lhs, float x) +{ + return lhs._n < x ? -1 : lhs._n > x ? 1 : 0; +} + +// More inheritance testing, this time using virtual base classes. +struct Foxtrot : public virtual ts::Comparable { + Foxtrot(unsigned n) : _n(n) {} + + unsigned _n{0}; +}; + +int +cmp(Foxtrot const &lhs, Foxtrot const &rhs) +{ + return lhs._n < rhs._n ? -1 : lhs._n > rhs._n ? 1 : 0; +} + +struct Golf : public Foxtrot, public virtual ts::Comparable { + Golf(unsigned n) : Foxtrot(n) {} +}; + +TEST_CASE("Comparable", "[meta][comparable]") +{ + Alpha a1{1}; + Alpha a2{2}; + Bravo b1{1.5}; + Charlie c1{3}; + Charlie c2{5}; + Delta d1{"sepideh"}; + Delta d2{"persia"}; + Echo e1{4}; + Foxtrot f1{10}; + Golf g1{9}; + + REQUIRE(a1 == a1); + REQUIRE(a1 == 1); + REQUIRE(1 == a1); + REQUIRE(a1 != a2); + REQUIRE(a1 < a2); + REQUIRE(a2 > a1); + + REQUIRE(c1 == c1); + REQUIRE(c1 != c2); + REQUIRE(c1 < c2); + REQUIRE(c2 > c1); + REQUIRE(c1 == 3); + REQUIRE(3 == c1); + + // check that we didn't break the non-overloaded operators. + REQUIRE(1 != 3); + REQUIRE(3 != 1); + + REQUIRE(b1 < a2); + REQUIRE(b1 > a1); + REQUIRE(a2 > b1); + REQUIRE(a1 < b1); + + REQUIRE(d1 < "zephyr"); + REQUIRE(d1 > "alpha"); + REQUIRE(d1 == "sepideh"); + REQUIRE(d1 == std::string_view{"sepideh"}); + // Verify the flip side. + REQUIRE("zephyr" > d1); + REQUIRE("alpha" < d1); + REQUIRE("sepideh" == d1); + REQUIRE(std::string_view{"sepideh"} == d1); + REQUIRE(ts::TextView{"sepideh"} == d1); + + REQUIRE(d1 != d2); + REQUIRE(d1 > d2); + REQUIRE(d2 < d1); + + REQUIRE(e1 > 3.5); + REQUIRE(e1 > 3); + REQUIRE(e1 > c1); + + REQUIRE(f1 == f1); + REQUIRE(f1 > g1); + REQUIRE(g1 <= f1); +}