isInputRange is false for non-copyable types

Shachar Shemesh shachar at weka.io
Mon Apr 16 04:15:36 UTC 2018


On 16/04/18 01:28, Jonathan M Davis wrote:
> The fact that foreach copies the range that it's given makes using ref with
> anything other than arrays a bit of an iffy proposition. It will compile
> regardless of whether front returns by ref or not (which arguably is a bug),
> but it will only affect the original elements if copying the range doesn't
> copy the elements

You keep referring to ranges that contain the data, but I am racking my 
mind and failing to find examples.

You can have ranges over arrays,linked lists, trees (though you'd 
probably use opApply there), and any other container, as well as 
external data sources (files, sockets, pipes). In none of those cases 
would your range contain actual data. I need to see the use case for 
ranges *with* embedded data.

Plus, as I've said before, if ranges are expected to contain data, 
copying them seems like an incredibly bad idea. The whole idea behind 
casually copying the range about is that it's cheap to do.

> which in the case of a basic input range rather than a
> forward range

I don't see how that statement is true.

> is generally true, otherwise, it would be a forward range.

A forward range is an input range.

> But as it stands, using ref with foreach in generic code is not going to go
> well. If anything, it's actually a code smell.

You are conflating ref with owning data, which I find unsubstantiated.

>> If non-copyable types were allowed, then functions that accept a range
>> would fail to compile on non-copyable types if they do a copy.
>>
>> But the flip side is that if non-copyable would be allowed, a lot of
>> functions that current copy would not. There are far too many
>> unnecessary copies in the code that serve no purpose, but they are
>> invisible, so they are not fixed.
> 
> And if a templated function fails to compile if it's given a particular
> argument, and that failure is not due to the template constraint, that
> usually means that the template constraint is incomplete.

I do not accept this argument outright. In fact, our experience at Weka 
has drove us to the exact opposite direction.

We deliberately set up functions to include invalid cases and then 
either let them fail because of lacking constraints, or even set up a 
static assert to fail the function generation rather than fail matching.

We found that the error messages produced by the alternative are simply 
too difficult to parse out and figure what went wrong.

With that said, there is some merit to setting up the function 
constraints so that it matches what the function actually need. It 
allows overloading the missing combinations by another module. I don't 
think it is a good argument in this particular case, however.

I think we should set up to document which phobos functions require 
copying. It will flesh out bugs and unnecessary copying in phobos, as 
well as force phobos developers to think about this scenario when they 
write code. It shouldn't even take that much time to do this run: just 
go over all UTs and add instantiation over a non-copyable type. The UTs 
that fail need work.

> it would mean that _every_ range-based function then has to worry about
> non-copyable elements

No, just those in libraries. The same way they have to worry about big O 
complexity, type safety, @nogc and a bunch of other things.

When you write a library, you have to think about your users. This 
doesn't change anything about that.

> So, allowing
> non-copyable types complicates range-based code across the board.

Like I said above, it is more likely to find bugs than to introduce 
needless complications. If you code cannot be easily implemented over 
non-copyable types, exclude them. Don't shut me out of the entire game.

> Not all algorithms can call front just once without
> copying it

Huh? Where did that come from?

Phobos already assumes that calling front needs to be an O(1) operation. 
You can call front as many times as you like. Why would you assume 
otherwise?

> and many ranges (e.g. map) calculate front on each call, meaning
> that repeated calls to front can be more expensive than just calling it once
> and copying the result.

Not by a lot, and definitely not in any way the generic code has any 
right to assume. I reject this argument outright.

> The result of all of this is that in generic code, you have no clue
> whatsoever what the semantics are of copying a range around, because it
> depends entirely on the copying semantics of whatever range that code is
> currently being instantiated with. As such, generic code basically has to
> assume that once you copy a range, you can't mutate the original (since it
> may or may not be affected by mutating the copy and may or may not be
> cleanly affected by mutating the copy),

Jonathan, I'm sorry, but what you are describing is called "a bug".

Input ranges provide no guarantees about copying the range itself. If 
you do it and expect *anything* (which it seems you do), you have a bug. 
If you need to copy the range itself, you absolutely need to require a 
forward range.

A forward range with ref front absolutely promises that both iterations 
reference the same elements.

I find myself sincerely hoping that you are making these examples up in 
order to win this discussion turned into an argument for some reason, 
instead of the way phobos is actually written, because if you're right 
phobos is *broken*.

The good news is that if Phobos is broken, this is a great way to flush 
out the places it's broken. Let's add UTs calling all Phobos functions 
accepting input ranges (as opposed to forward ranges) with non-copyable 
ranges (i.e. - the range itself is non-copyable) and fix the compilation 
errors. This shouldn't take more than a few days to do.

Hell, let's write UTs to pass forward ranges that are non-copyable (but 
return a copy via "save") and flush out those bugs as well.

The point of "development" is to make our code better, not defend the 
status-quo. Especially since we've just identified a fairly simple way 
to find the places that need work and easily work on them.

> But of course, since _most_ ranges act just like save when they're copied,
> save gets frequently forgotten, making it a bit of a thorn in our side.

Then rejoice: I just suggested a way to *easily* find and fix those places.

> require that forward ranges act like value types (at
> least insofar as copying them copies the iteration state and thus acts like
> save).

The brackets are important. Just because I'm returning a reference to 
elements does not mean I don't support the range itself restarting a scan.

> So, some redesign of ranges would be great.

Yes, but please note that I don't think it is *necessary*. Yes, save is 
annoying, but it *is* workable as is, and since we can harness the 
compiler to locate where we're deviating from the righteous way, not 
that expensively if we could just be bothered to do it.

>> And allowing ranges with uncopyable members would turn this potential
>> deadly run-time bugs into easy to find compile time errors. Sound like a
>> great reason to allow it, to me.
> 
> Ranges have to be copyable.

And why would a range iterating over uncopyable objects not be copyable? 
How are the two related?

Shachar


More information about the Digitalmars-d mailing list