How to use default spaceship operator with a std::string member - c++

I have a struct with a few POD types and one string member. I want to use the default spaceship operator to allow equality operations of my struct, but I'm having some issues with the string member. As far as I can tell, there should be support for <=> for std::string, but in practice it seems like this isn't the case.
I have a minimal reproducing example, and the resulting warning that I get from clang (version 14) below. Strangely, in godbolt this example produces no warning/error, even going as far back as clang 12 (https://godbolt.org/z/b65s9oMGf).
I'd really appreciate some pointers in where my misunderstanding is, because I'm pretty confused about this one.
#include <compare>
#include <string>
enum class MyEnum
{
ZERO = 0,
ONE = 1
};
struct MyStruct
{
float a{};
int b{};
std::string c{};
MyEnum d{};
auto operator<=>(const MyStruct&) const = default;
};
int main()
{
MyStruct my_struct;
MyStruct my_other_struct;
if(my_struct == my_other_struct)
{
return 0;
}
return 1;
}
$ clang --std=c++20 -Werror test.cpp
test.cpp:16:10: error: explicitly defaulted three-way comparison operator is implicitly deleted [-Werror,-Wdefaulted-function-deleted]
auto operator<=>(const MyStruct&) const = default;
^
test.cpp:13:17: note: defaulted 'operator<=>' is implicitly deleted because there is no viable three-way comparison function for member 'c'
std::string c{};
^
1 error generated.
$ clang --version
Apple clang version 14.0.0 (clang-1400.0.29.202)

Based on the comments, it appears that this version of clang along with its standard library:
does implement the language feature for <=>, but
does not yet implement the library additions for <=> (specifically adding <=> for std::string).
When you write:
struct MyStruct
{
float a{};
int b{};
std::string c{};
MyEnum d{};
auto operator<=>(const MyStruct&) const = default;
};
What a defaulted <=> means is to do a member-wise comparison using <=>, returning the value for the first member that compares unequal. When you use auto, that is asking the compiler to deduce the comparison category for you - but unlike typical use of auto (where all the return types have to be the same, and auto deduces to that one type), here we take the minimum of all the comparison categories. For example, if we had one member whose <=> returned weak_ordering but another whose <=> returned strong_ordering, a defaulted <=> would return weak_ordering (rather than not compile). Basically - it does what you probably want it to do, in the way you'd expect.
But - sometimes defaulting <=> isn't sufficient, if one of your members don't actually provide <=> yet (as in this case). In that situation, it'd be tedious to have to manually write all the comparisons out. So for that reason, there's another way that you can default <=> (from P1186): you can explicitly provide a comparison category:
struct MyStruct
{
float a{};
int b{};
std::string c{};
MyEnum d{};
std::strong_ordering operator<=>(const MyStruct&) const = default;
};
What this means is, rather than simply doing member-wise <=>, we do something a little more involved:
if the member has <=>, use it. And it has to meet the comparison category requirement (in this case, it would happen to fail because floats compare with partial_ordering, which doesn't meet the strong_ordering requirement - so it would have to return partial_ordering).
if the member doesn't provide <=>, we synthesize a three-way comparison from == and <.
So in this case:
struct MyStruct
{
float a{};
int b{};
std::string c{};
MyEnum d{};
std::partial_ordering operator<=>(const MyStruct&) const = default;
};
would behave like:
std::partial_ordering operator<=>(const MyStruct& rhs) const {
// a and b provide <=>, so use it
if (auto cmp = a <=> rhs.a; cmp != 0) return cmp;
if (auto cmp = b <=> rhs.b; cmp != 0) return cmp;
// c doesn't yet, so synthesize one
if (auto cmp =
(c == rhs.c ? partial_ordering::equivalent :
c < rhs.c ? partial_ordering::less :
c < rhs.c ? partial_ordering::greater :
partial_ordering::unordered); cmp != 0) return cmp;
// and d does, so use it
return d <=> rhs.d;
}
The advantage of this language feature (as I laid out in that paper) is that once std::string does provide <=>, your implementation will pick it up automatically and be more efficient (cause the above really isn't a great implementation for string, you'd want to do c.compare(rhs.c) <=> 0), without you having to do all the work yourself.

Related

Why should I use the three-way comparison operator (<=>) instead of the two-way comparison operators? Does this have an advantage?

#include <compare>
#include <iostream>
int main()
{
auto comp1 = 1.1 <=> 2.2;
auto comp2 = -1 <=> 1;
std::cout << typeid(comp1).name()<<"\n"<<typeid(comp2).name();
}
Output:
struct std::partial_ordering
struct std::strong_ordering
I know that if the operands have an integral type, the operator returns a PRvalue of type std::strong_ordering. I also know if the operands have a floating-point type, the operator yields a PRvalue of type std::partial_ordering.
But why should I use a three-way comparison operator instead of two-way operators (==, !=, <, <=, >, >=)? Is there an advantage this gives me?
It makes it possible to determine the ordering in one operation.
The other operators require two comparisons.
Summary of the other operators:
If a == b is false, you don't know whether a < b or a > b
If a != b is true, you don't know whether a < b or a > b
If a < b is false, you don't know whether a == b or a > b
If a > b is false, you don't know whether a == b or a < b
If a <= b is true, you don't know whether a == b or a < b
If a >= b is true, you don't know whether a == b or a > b
A neat side effect is that all the other operators can be implemented in terms of <=>, and a compiler can generate them for you.
Another side effect is that people might be confused by the use of <=> as the equivalence arrow in mathematics, which it has been pretty much since typewriters got those three symbols.
(I'm personally pretty miffed by how a <=> b is "truthy" if and only if a and b are not equivalent.)
The main advantage (at least for me) is the fact that this operator can be defaulted for the class, which will automatically support all possible comparisons for your class. I.e.
#include <compare>
struct foo {
int a;
float b;
auto operator<=>(const foo& ) const = default;
};
// Now all operations used before are defined for you automatically!
auto f1(const foo& l, const foo& r) {
return l < r;
}
auto f2(const foo& l, const foo& r) {
return l > r;
}
auto f3(const foo& l, const foo& r) {
return l == r;
}
auto f4(const foo& l, const foo& r) {
return l >= r;
}
auto f5(const foo& l, const foo& r) {
return l <= r;
}
auto f6(const foo& l, const foo& r) {
return l != r;
}
Previously, all those operations would have to be defined within the class, which is cumbersome and error-prone - as one would have to remember to revisit those whenever new members are added to the class.
Use your own judgment.
The point of the spaceship operator is not specifically for comparing objects. The main point of it is permitting the compiler to synthesize the other comparison operators from the spaceship operator.
If you don't specifically need to answer the question less-than, equal-to, or greater-than, then you don't need to invoke it directly. Use the operator that makes sense for you.
But should you need to make a type comparable, you only have to write 2 functions (spaceship and equality) rather than 6. And when writing such a function, you can use the spaceship operator on the individual types in question (should they be comparable in such a way). That makes it even easier to implement such functions.
The other useful thing that the spaceship operator allows you to do is tell what kind of ordering a comparison will provide. Partial, strong, or whatever. This can theoretically be useful, but it's fairly rare overall.
The spaceship operator was proposed by Herb Sutter and was adopted by the committee to be implemented with C++ 20, detailed report can be consulted here, or you if are more into lectures, here you can see a video of the man himself making the case for it. In pages 3/4 of the report you can see the main use case:
The comparison operators implementation, needed for pre C++20, in the following class:
class Point
{
int x;
int y;
public:
friend bool operator==(const Point &a, const Point &b) { return a.x == b.x && a.y == b.y; }
friend bool operator<(const Point &a, const Point &b) { return a.x < b.x || (a.x == b.x && a.y < b.y); }
friend bool operator!=(const Point &a, const Point &b) { return !(a == b); }
friend bool operator<=(const Point &a, const Point &b) { return !(b < a); }
friend bool operator>(const Point &a, const Point &b) { return b < a; }
friend bool operator>=(const Point& a, const Point& b) { return !(a < b); }
// ... non-comparisonfunctions ...
};
Would be replaced by:
class Point
{
int x;
int y;
public:
auto operator<=>(const Point &) const = default;
// ... non-comparison functions ...
};
So to answer your question, overloading operator<=> as class member allows you to use all comparison operators for the class objects without having to implement them, defaulting it also defaults operator==, if it's not otherwise declared, which in term automatically implements operator!=, making all comparison operations available with a single expression. This functionality is the main use case.
I would like to point out that the spaceship operator would not be possible without the introduction of the default comparison feature with C++20.
Defaulted three-way comparison
[...]
Let R be the return type, each pair of subobjects a, b is compared as follows:
[...]
... if R is std::strong_ordering, the result is:
a == b ? R::equal : a < b ? R::less : R::greater
Otherwise, if R is std::weak_ordering, the result is:
a == b ? R::equivalent : a < b ? R::less : R::greater
Otherwise (R is std::partial_ordering), the result is:
a == b ? R::equal : a < b ? R::less : b < a ? R::greater : R::unordered
Per the rules for any operator<=> overload, a defaulted <=> overload will also allow the type to be compared with <, <=, >, and >=.
If operator<=> is defaulted and operator== is not declared at all, then operator== is implicitly defaulted.
It also allows to default operator == only, which will implement operator !=, albeit not as versatile as the former, it's also an interesting possibility.

(Why) are std::chrono literals lvalues?

Basically the example says it all: assigning to a std::chrono literal - does not give an error message.
I tried on gcc, clang and vc, (c++17) all these compilers accept it.
Is this intended behaviour?
#include <chrono>
using namespace std::chrono_literals;
int main()
{
10ms += 1ms; // would expect error here
12ms = 10ms; // and here
// 3 = 4; // like here
return 0;
}
are std::chrono literals lvalues?
No. std::chrono_literals are prvalues.
Is this intended behaviour?
It is as the standard has specified.
These values are of class types. And rvalues of class types can be assigned1 as long as the class is assignable, and their assignment operator overload is not lvalue ref qualified. Assignment operators of classes, like all member functions, are not lvalue ref qualified by default, and hardly any standard class has such qualified overload2 - I don't know of any but if there are, std::chrono::duration isn't one of them.
Note that the literal isn't modified. The left hand operand is a temporary object which in this case is discarded.
Another simple example of assigning an rvalue:
std::string{"temporary"} = "this is pointless, but legal";
1 This is actually a useful feature, although there aren't many use cases. Here is a useful example:
std::vector<bool> vec{true, false, true};
vec[1] = true; // vec[1] is an rvalue
2 Prior to C++11, there were no ref qualifiers, and thus all rvalues of assignable class types could be assigned. To maintain backward compatibility, the default cannot be changed, nor can pre-existing classes be changed (at least not without a deprecation period). std::chrono::duration was added in C++11, so it could have had qualified assignment operator, but standard authors appear to prefer not qualifying their assignment operators.
The std::chrono literals are not lvalues. They may appear as such, but what you are seeing is actually invocation of assignment operators by r-values. Why is this allowed? Specifically, because e.g. the member assignment operator
constexpr duration& operator+=(const duration& d)
as specified by [time.duration.arithmetic]/9, does not use any ref-qualifiers, meaning it does not reject being invoked by r-values.
Compare with the following example (implemented as duration above)
struct Foo {
unsigned long long i;
constexpr Foo& operator+=(const Foo& d) { return *this; }
};
constexpr Foo operator"" _Foo (unsigned long long i) {
return Foo{i};
}
int main() {
1_Foo += 2_Foo; // OK!
}
as compared to the following example, which explicitly makes use of a & ref-qualifier to implicitly ban invocation on r-values:
struct Foo {
unsigned long long i;
constexpr Foo& operator+=(const Foo& d) & { return *this; }
};
constexpr Foo operator"" _Foo (unsigned long long i) {
return Foo{i};
}
int main() {
1_Foo += 2_Foo; // error: no viable overloaded '+='
}
or, the following example, which explicitly deletes the r-value overload:
struct Foo {
unsigned long long i;
constexpr Foo& operator+=(const Foo& d) & { return *this; }
constexpr Foo& operator+=(const Foo& d) && = delete;
};
constexpr Foo operator"" _Foo (unsigned long long i) {
return Foo{i};
}
int main() {
1_Foo += 2_Foo; // error: overload resolution selected deleted operator '+='
}
For details regarding r-value qualifiers and how they could make sense to apply in order to ban to-rvalue-assignment, see e.g. the following Q&A:
What does the & (ampersand) at the end of member function signature mean?

Assignment vs constructor in C++ [duplicate]

I wrote some code S s; ... s = {};, expecting it to end up the same as S s = {};. However it didn't. The following example reproduces the problem:
#include <iostream>
struct S
{
S(): a(5) { }
S(int t): a(t) {}
S &operator=(int t) { a = t; return *this; }
S &operator=(S const &t) = default;
int a;
};
int main()
{
S s = {};
S t;
t = {};
std::cout << s.a << '\n';
std::cout << t.a << '\n';
}
The output is:
5
0
My questions are:
Why is operator=(int) selected here, instead of "ambiguous" or the other one?
Is there a tidy workaround, without changing S?
My intent is s = S{}; . Writing s = {}; would be convenient if it worked. I'm currently using s = decltype(s){}; however I'd prefer to avoid repeating the type or the variable name.
Why is operator=(int) selected here, instead of "ambiguous" or the other one?
{} to int is the identity conversion ([over.ics.list]/9). {} to S is a user-defined conversion ([over.ics.list]/6) (technically, it's {} to const S&, and goes through [over.ics.list]/8 and [over.ics.ref] first before coming back to [over.ics.list]/6).
The first wins.
Is there a tidy workaround?
A variation of the trick std::experimental::optional pulls to make t = {} always make t empty.
The key is to make operator=(int) a template. If you want to accept int and only int, then it becomes
template<class Int, std::enable_if_t<std::is_same<Int, int>{}, int> = 0>
S& operator=(Int t) { a = t; return *this; }
Different constraints can be used if you want to enable conversions (you'd probably also want to take the argument by reference in that case).
The point is that by making the right operand's type a template parameter, you block t = {} from using this overload - because {} is a non-deduced context.
...without changing S?
Does template<class T> T default_constructed_instance_of(const T&) { return {}; } and then s = default_constructed_instance_of(s);count?
First of all, the case has nothing to do with the "int" version of the assignment operator, you can just delete it. You can actually delete the other assignment operator too as it will be generated by the compiler. IE this kind of type automatically receives copy/move constructors and the assignment operator. (ie they are not prohibited and you are just repeating what the compiler does automatically with explicit notation)
The first case
uses copy initialization:
S s = {}; // the default constructor is invoked
That is a post-construction copy assignment, yet compilers optimize such simple cases. You should use direction initialization instead:
S s{}; // the default constructor is invoked (as you have it)
Note, you can also write:
S s; // the default constructor is invoked if you have it
The second case
What you should write is direct initialization of the right hand side of the copy assignment:
t = S{};
This notation will invoke the default constructor (if there is one), or value initialization for the members (as long as the type is an aggregate). Here is the relevant info: http://en.cppreference.com/w/cpp/language/value_initialization

std::tuple for non-copyable and non-movable object

I have a class with copy & move ctor deleted.
struct A
{
A(int a):data(a){}
~A(){ std::cout << "~A()" << this << " : " << data << std::endl; }
A(A const &obj) = delete;
A(A &&obj) = delete;
friend std::ostream & operator << ( std::ostream & out , A const & obj);
int data;
};
And I want to create a tuple with objects of this class. But the following does not compile:
auto p = std::tuple<A,A>(A{10},A{20});
On the other hand, the following does compile, but gives a surprising output.
int main() {
auto q = std::tuple<A&&,A&&>(A{100},A{200});
std::cout << "q created\n";
}
Output
~A()0x22fe10 : 100
~A()0x22fe30 : 200
q created
It means that dtor for objects is called as soon as tuple construction line ends. So, what is significance of a tuple of destroyed objects?
This is bad:
auto q = std::tuple<A&&,A&&>(A{100},A{200});
you are constructing a tuple of rvalue references to temporaries that get destroyed at the end of the expression, so you're left with dangling references.
The correct statement would be:
std::tuple<A, A> q(100, 200);
However, until quite recently, the above was not supported by the standard. In N4296, the wording around the relevant constructor for tuple is [tuple.cnstr]:
template <class... UTypes>
constexpr explicit tuple(UTypes&&... u);
Requires: sizeof...(Types) == sizeof...(UTypes). is_constructible<Ti, Ui&&>::value is true
for all i.
Effects: Initializes the elements in the tuple with the corresponding value in std::forward<UTypes>(u).
Remark: This constructor shall not participate in overload resolution unless each type in UTypes is
implicitly convertible to its corresponding type in Types.
So, this constructor was not participating in overload resolution because int is not implicitly convertible to A. This has been resolved by the adoption of Improving pair and tuple, which addressed precisely your use-case:
struct D { D(int); D(const D&) = delete; };
std::tuple<D> td(12); // Error
The new wording for this constructor is, from N4527:
Remarks: This constructor shall not participate in overload resolution unless sizeof...(Types) >= 1 and is_constructible<Ti, Ui&&>::value is true for all i. The constructor is explicit if and only
if is_convertible<Ui&&, Ti>::value is false for at least one i.
And is_constructible<A, int&&>::value is true.
To present the difference another way, here is an extremely stripped down tuple implementation:
struct D { D(int ) {} D(const D& ) = delete; };
template <typename T>
struct Tuple {
Tuple(const T& t)
: T(t)
{ }
template <typename U,
#ifdef USE_OLD_RULES
typename = std::enable_if_t<std::is_convertible<U, T>::value>
#else
typename = std::enable_if_t<std::is_constructible<T, U&&>::value>
#endif
>
Tuple(U&& u)
: t(std::forward<U>(u))
{ }
T t;
};
int main()
{
Tuple<D> t(12);
}
If USE_OLD_RULES is defined, the first constructor is the only viable constructor and hence the code will not compile since D is noncopyable. Otherwise, the second constructor is the best viable candidate and that one is well-formed.
The adoption was recent enough that neither gcc 5.2 nor clang 3.6 actually will compile this example yet. So you will either need a newer compiler than that (gcc 6.0 works) or come up with a different design.
Your problem is that you explicitly asked for a tuple of rvalue references, and a rvalue reference is not that far from a pointer.
So auto q = std::tuple<A&&,A&&>(A{100},A{200}); creates two A objects, takes (rvalue) references to them, build the tuple with the references... and destroys the temporary objects, leaving you with two dangling references
Even if it is said to be more secure than good old C and its dangling pointers, C++ still allows programmer to write wrong programs.
Anyway, the following would make sense (note usage of A& and not A&&):
int main() {
A a(100), b(100); // Ok, a and b will leave as long as main
auto q = tuple<A&, A&>(a, b); // ok, q contains references to a and b
...
return 0; // Ok, q, a and b will be destroyed
}

Copy-initialisation and explicit constructor - compiler discrepancy

I've discovered a discrepancy between the Microsoft Visual C++ compiler, and gcc-4.8.1 (as provided by ideone.com). Consider the following SSCCE:
struct S
{
int x;
};
class A
{
public:
int x;
A(const S& s) : x(s.x) {}
};
class B
{
int x, y;
public:
template <typename T> explicit B(const T& t) : x(t.x), y(t.y) {}
B(const A& a) : x(a.x), y(0) {}
};
int main() {
S s = {1};
B b1 = s; // Compiles OK on MSVC++;
// Fails on gcc - conversion from ‘S’ to non-scalar type ‘B’ requested
B b2(s); // Fails on both - Error: y is not a member of S in B::B<S>(const T &)
}
I understand why the line B b2(s); fails - the explicit constructor matches so it's tried; but t.y doesn't exist. Fine.
But I can't work out whether MSVC++ is correct in allowing B b1 = s;, or whether gcc is correct in rejecting it. MSVC++ is constructing a temporary from A::A(const S&), and using that to initialise b1 via B::B(const A&); I'm not sure why gcc errors.
Which compiler's right?
(As an after note, if I remove the explicit both compilers reject B b1 = s; - presumably because the templated constructor is now fair game for the implicit construction of the temporary.)
Edit: From the comments, it appears the MSVC++ also rejects the B b1 = s; line in Visual Studio 2012, so the consensus seems to be it really is an error. In which case - what is the nature of the error? What does that error message mean?
Stolen from this answer, the standard says:
12.3 Conversions [class.conv]
4 At most one user-defined conversion (constructor or conversion function) is implicitly applied to a single value.
You're trying to perform two in one step, S which needs to be converted for B's constructor that accepts an A, and then another for A's constructor that accepts an S. The solution is to first cast S into an A:
B b1 = static_cast<A>(s);