Comment #0 by andrej.mitrovich — 2020-08-04T04:15:01Z
-----
import std.algorithm;
import std.stdio;
struct S
{
int i;
int[] arr;
}
void main ()
{
enum q1 = [
S(2, [0, 0]),
];
enum q2 = [
S(2, [0, 0]),
];
enum q3 = [
S(2, [1, 1]),
];
foreach (idx; 0 .. 10) // 10 iterations
{
const unique_count = [q1, q2, q3].sort.uniq.count;
writefln("Uniques: %s", unique_count);
}
}
-----
This produces:
```
$ dmd -run test.d
> Uniques: 2
Uniques: 2
Uniques: 2
Uniques: 2
Uniques: 2
Uniques: 3
Uniques: 2
Uniques: 2
Uniques: 2
Uniques: 2
```
Notice how in one run the number of uniques was 3!
If I change `enum` to `auto` the bug goes away.
Comment #1 by andrej.mitrovich — 2020-08-04T04:17:58Z
Using DMD v2.093.0.
Comment #2 by andrej.mitrovich — 2020-08-04T04:19:11Z
I've confirmed the issue with ldc2 as well. So it looks like it's a front-end bug.
Comment #3 by andrej.mitrovich — 2020-08-04T04:23:51Z
I think this is related to `sort`. It's possible that sort compares by pointers if two structs are otherwise equivalent.
And then using `enum` has a different effect on arrays, possibly optimizing and storing the same pointer in two structs (making them equal).
Comment #4 by andrej.mitrovich — 2020-08-04T04:33:30Z
(In reply to Andrej Mitrovic from comment #3)
> I think this is related to `sort`. It's possible that sort compares by
> pointers if two structs are otherwise equivalent.
>
> And then using `enum` has a different effect on arrays, possibly optimizing
> and storing the same pointer in two structs (making them equal).
Sorry the actual reasoning should be: `enum` always allocates a new array, therefore the pointer addresses change each time.
It's possible opCmp does a value comparison first, and then a pointer comparison.
But I don't think pointers should be compared at all.. at least not when dealing with arrays and not naked pointers in a struct.
Comment #5 by boris2.9 — 2020-08-04T09:42:04Z
There is no 'opCmp' defined for the struct so the runtime just use a memcmp for the comparison.
The code itself calls '__cmp' from core/internal/array/comparison.d (because the element are an array '[S(2, [0, 0])]') and there the 3 'static if' conditions fail but if you define 'opCmp' the second path is taken.
So the following works:
struct S
{
int[] arr;
int opCmp(ref const S s) const { return arr < s.arr; }
}
There is a comment on clone.d file that says:
"Essentially, a struct which does not define opCmp is not comparable."
This concise phrase should be on the spec.
Comment #6 by andrej.mitrovich — 2020-08-07T08:14:18Z
(In reply to Boris Carvajal from comment #5)
> There is no 'opCmp' defined for the struct so the runtime just use a memcmp
> for the comparison.
> The code itself calls '__cmp' from core/internal/array/comparison.d (because
> the element are an array '[S(2, [0, 0])]') and there the 3 'static if'
> conditions fail but if you define 'opCmp' the second path is taken.
>
> So the following works:
>
> struct S
> {
> int[] arr;
> int opCmp(ref const S s) const { return arr < s.arr; }
> }
>
> There is a comment on clone.d file that says:
>
> "Essentially, a struct which does not define opCmp is not comparable."
>
> This concise phrase should be on the spec.
Hmm.
In the past the compiler would also do memcmp when a struct didn't define `opEquals`, but we changed it so it does member-by-member `==` comparison.
I wonder if we can do something similar for opCmp, or if that wouldn't make sense..
Comment #7 by simen.kjaras — 2020-08-07T08:50:57Z
We can reduce this issue down to this code:
struct S {
int[] arr;
}
unittest {
import std.stdio;
foreach (idx; 0 .. 10) {
writeln([S([0])] < [S([0])]);
}
}
I'm not convinced that being able to compare two arrays like this should be possible at all - the rule that for any T where T.init < T.init does not compile, [T.init] < [T.init] should not compile seems sensible to me.
If we need it to work, it should definitely do a member-by-member comparison. Not least, this should be documented.
Comment #8 by robert.schadek — 2024-12-13T19:10:34Z