Implementing comparison operators via 'tuple' and 'tie', a good idea?
This is certainly going to make it easier to write a correct operator than rolling it yourself. I'd say only consider a different approach if profiling shows the comparison operation to be a time-consuming part of your application. Otherwise the ease of maintaining this should outweigh any possible performance concerns.
using make_tuple for comparison
The declaration of std::tuple
comparison operators states that:
Compares lhs and rhs lexicographically, that is, compares the first elements, if they are equivalent, compares the second elements, if those are equivalent, compares the third elements, and so on.
So what you are doing, other than having the possibility of creating unneeded temporaries (probably optimized away), seems ok to me.
Note that equivalent means !( lhs < rhs ) && !( rhs < lhs )
and not lhs == rhs
, that's equality. Assuming equivalent and equality mean the same for your class, this would be ok. Note that this is no different than, for instance, accessing a set
/map
by key.
In order to avoid the temporaries, you can use std::tie
which makes a tuple of lvalue references to its arguments:
std::tie( lhs.date_, lhs.time_, lhs.id_ ) <
std::tie( rhs.date_, rhs.time_, rhs.id_ );
In a similar fashion, std::forward_as_tuple
makes a tuple of rvalue references.
Is using std::tuple to implement operator<, = etc. efficient and correct?
It's correct, but it's not efficient, because the constructor of std::tuple
will copy all the values. This also means you can't use it to compare non-copyable types.
Instead you could use std::tie
, which needs a bit less code and doesn't copy the values, but creates a tuple of references.
Your operator<
would then become:
bool operator<(const Customer& rop) const {
return std::tie(name, age) < std::tie(rop.name, rop.age);
}
c++11 combining std::tuple and std::tie for efficient ordering
I just discovered that the problem with the 3rd lambda was that I was not correctly comparing the elements correctly for equivalence and lexicographical comparison. The correct approach is outlined in bullet 3) for tuple comparison where it indicates that I should use the following approach.
3) (bool)(std::get<0>(lhs) < std::get<0>(rhs)) ||
(!(bool)(std::get<0>(rhs) < std::get<0>(lhs)) && lhstail < rhstail),
where lhstail is lhs without its first element, and rhstail is rhs
without its first element. For two empty tuples, returns false.
The fixed up lambda comparator for sorting firsty sorts according to the filename() temporary and then uses the efficient std::tie for the other elements in the tuple
std::cout << "Order[mPath.filename(), elem1, intVal]" << std::endl;
std::cout << "======================================" << std::endl;
std::sort(aStructs.begin(), aStructs.end(),
[](const StructA& lhs, const StructA& rhs){
// attempt at efficiency - but not quite right
// AHA, I think I figured it out - see tuple operator_cmp
// return value documentation which states
// (bool)(std::get<0>(lhs) < std::get<0>(rhs)) ||
// (!(bool)(std::get<0>(rhs) < std::get<0>(lhs)) &&
// lhstail < rhstail), where lhstail is lhs without
// its first element, and rhstail is rhs without its
// first element. For two empty tuples, returns false.
// --------------------------------------------------------
// edit thanks to @Praetorian for suggesting the following:
// --------------------------------------------------------
auto f1 = lhs.mPath.filename(); auto f2 = rhs.mPath.filename();
return std::tie(f1, lhs.elem1, lhs.intVal) < std::tie(f2, rhs.elem1, rhs.intVal);
});
Doing so made the first set of results identical to the 3rd - not incredibly efficient for the filename() temporary but at least I don't take the std::make_tuple hit for all elements in my struct. The updated live example is here
Implementing other comparison operators in terms of operator< in one call
a > b == b < a
a <= b == !(b < a)
a >= b == !(a < b)
It's even possible to implement equality in terms of less than (Kind of abusing my meta-syntax here):
(a == b) == (!(a < b) && !(b < a))
(a != b) == (a < b || b < a)
Although I wouldn't suggest doing so in practice, since it requires two comparisons and can generally be implemented more efficiently directly.
Implementing the '<' operator
You could use std::tie
.
class Person
{
public:
bool valid = false;
std::string name = "";
std::string id = "";
bool operator<(const Person& r) const
{
return std::tie(valid, name, id) < std::tie(r.valid, r.name, r.id);
}
}
Explanation:
std::tie(xs...)
creates an std::tuple
of references to the passed xs...
arguments. Comparison between two std::tuple
instances works by lexicographically comparing the elements, thus providing an ordering for your type.
More information here on cppsamples and in this question.
Compare tuples of different sizes
operator==
requires the tuples to be of equal lengths.
§ 20.4.2.7 [tuple.rel]:
template<class... TTypes, class... UTypes>
constexpr bool operator==(const tuple<TTypes...>& t, const tuple<UTypes...>& u);1 Requires: For all
i
, where0 <= i
andi < sizeof...(TTypes)
,get<i>(t) == get<i>(u)
is a valid expression returning a type that is convertible tobool
.sizeof...(TTypes) == sizeof...(UTypes)
.
If you want two tuples of different lengths to be considered unequal, you'd need to implement this logic yourself:
template <typename... Ts, typename... Us>
auto compare(const std::tuple<Ts...>& t1, const std::tuple<Us...>& t2)
-> typename std::enable_if<sizeof...(Ts) == sizeof...(Us), bool>::type
{
return t1 == t2;
}
template <typename... Ts, typename... Us>
auto compare(const std::tuple<Ts...>& t1, const std::tuple<Us...>& t2)
-> typename std::enable_if<sizeof...(Ts) != sizeof...(Us), bool>::type
{
return false;
}
DEMO
This way, the code comparing two tuples, t1 == t2
, is instantiated only when the lengths of tuples match each other. In your scenario, a compiler is unable to compile your code, since there is no predefined operator==
for such a case.
Related Topics
Why Does Integer Overflow on X86 With Gcc Cause an Infinite Loop
Error "Undefined Reference to 'Std::Cout'"
Cmake Cannot Find Library Using "Link_Directories"
Why Does C++11'S Lambda Require "Mutable" Keyword For Capture-By-Value, by Default
How to Efficiently Perform Double/Int64 Conversions With Sse/Avx
Remove_If Equivalent For Std::Map
Why Is the Template Argument Deduction Not Working Here
Why Doesn't Std::Queue::Pop Return Value.
Create an Array When the Size Is a Variable Not a Constant
Why Can't Variable Names Start With Numbers
How to Make My Program Watch For File Modification in C++
How to Link Opencv in Qtcreator and Use Qt Library
Why Are C++ Inline Functions in the Header
Is There Any Real Risk to Deriving from the C++ Stl Containers
Why Doesn't Delete Set the Pointer to Null
Atomic Double Floating Point or Sse/Avx Vector Load/Store on X86_64