To my surprise, I ran into another snag like C++20 behaviour breaking existing code with equality operator?.
Consider a simple case-insensitive key type, to be used with, e.g., std::set
or std::map
:
// Represents case insensitive keys
struct CiKey : std::string {
using std::string::string;
using std::string::operator=;
bool operator<(CiKey const& other) const {
return boost::ilexicographical_compare(*this, other);
}
};
Simple tests:
using KeySet = std::set<CiKey>;
using Mapping = std::pair<CiKey, int>; // Same with std::tuple
using Mappings = std::set<Mapping>;
int main()
{
KeySet keys { "one", "two", "ONE", "three" };
Mappings mappings {
{ "one", 1 }, { "two", 2 }, { "ONE", 1 }, { "three", 3 }
};
assert(keys.size() == 3);
assert(mappings.size() == 3);
}
Using C++17, both asserts pass (Compiler Explorer).
Switching to C++20, the second assert fails (Compiler Explorer)
output.s: ./example.cpp:28: int main(): Assertion `mappings.size() == 3' failed.
Obvious Workaround
An obvious work-around is to conditionally supply operator<=>
in C++20 mode: Compile Explorer
#if defined(__cpp_lib_three_way_comparison)
std::weak_ordering operator<=>(CiKey const& other) const {
if (boost::ilexicographical_compare(*this, other)) {
return std::weak_ordering::less;
} else if (boost::ilexicographical_compare(other, *this)) {
return std::weak_ordering::less;
}
return std::weak_ordering::equivalent;
}
#endif
Question
It surprises me that I ran into another case of breaking changes - where C++20 changes behaviour of code without diagnostic.
On my reading of std::tuple::operator<
it should have worked:
3-6) Compares
lhs
andrhs
lexicographically byoperator<
, 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. For non-empty tuples, (3) is equivalent toif (std::get<0>(lhs) < std::get<0>(rhs)) return true; if (std::get<0>(rhs) < std::get<0>(lhs)) return false; if (std::get<1>(lhs) < std::get<1>(rhs)) return true; if (std::get<1>(rhs) < std::get<1>(lhs)) return false; ... return std::get<N - 1>(lhs) < std::get<N - 1>(rhs);
I understand that technically these don't apply since C++20, and it gets replaced by:
Compares
lhs
andrhs
lexicographically by synthesized three-way comparison (see below), 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
Together with
The <, <=, >, >=, and != operators are synthesized from
operator<=>
andoperator==
respectively. (since C++20)
The thing is,
my type doesn't define
operator<=>
noroperator==
,and as this answer points out providing
operator<
in addition would be fine and should be used when evaluating simple expressions likea < b
.
- Is the behavior change in C++20 correct/on purpose?
- Should there be a diagnostic?
- Can we use other tools to spot silent breakage like this? It feels like scanning entire code-bases for usage of user-defined types in
tuple
/pair
doesn't scale well. - Are there other types, beside
tuple
/pair
that could manifest similar changes?
std::string
does, making it a candidate due to the drived-to-base conversion. I believe all standard library types that support comparison had their members overhauled. – StoryTeller - Unslander Monica