On Thursday, 25 July 2024 at 13:07:03 UTC, Jonathan M Davis wrote:
> On Thursday, July 25, 2024 6:00:58 AM MDT Dom DiSc via Digitalmars-d-learn wrote:
> > And no, in general, you don't want to be casting away const or immutable. There are cases where it can work (e.g. if the cast does a copy, which it would with an integer type)
But a parameter given by value is ALWAYS a copy.
So if a copy is done, the copy should be mutable. I can't see why
that should ever be a problem. The compiler should never create
new values const or immutable unless explicitly advised to do so.
It has to be a full, independent copy. If you're talking about integer types, that's a non-issue, but if you're talking about types with any indirections, it becomes a big issue. If the type contains any pointers, dynamic arrays, class references, etc. then the copy can't be mutable unless you're dealing with a struct with a copy constructor that does a deep copy of all of the member variables. So, in the general case, you cannot copy a value and expect to get a mutable result.
> E.g.
void fun(myType x) { }
called with immutable object should create a mutable x, because
myType x = immutableObject;
will also create a mutable x. If I want an immutable x, i can do
immutable myType = immutableObject;
void fun(T)(T x) if (is(Unqual!T==myType)) { }
should also create a mutable x, for the same reason, because if I want an immutable x, I can do
void fun(T)(immutable(T) x) if (is(Unqual!T==myType)) { }
Therefore I consider the current behaviour a bug.
It's most definitely not a bug that IFTI (Implicit Function Template Instantiation) instantiates the template with the exact type that it's given. In the general case, that's what you want - particularly since many, many types (probably the vast majority of types) cannot be const or immutable and then result in a mutable copy when they're copied. That really only works with very simple types with no indirections. In the general case, there is no expectation whatsoever that it's going to be possible to get a mutable copy from a const or immutable variable, and generic code that expects that to work is very likely to run into problems very quickly.
Now, the fact that code such as
void fun(T : const U, U)(U x)
if(is(immutable T == immutable U))
{
...
}
or code like
void fun(T)(Unqual!T x)
{
}
doesn't work with IFTI and requires explicit instantation is definitely a deficiency in IFTI's current capabilities, but in the general case, we very much want
void fun(T)(T x) {}
to be instantiated as fun!(const Foo)
if it's passed a const Foo
.
For some types, we would like the ability to tell the compiler to implicitly instantiate function templates with a tail-const version similar to what we get with dynamic arrays, but that really only makes sense for certain types such as ranges.
But either way, it would cause quite a few problems if IFTI started instantiating templates in general with mutable instead of the actual type that it's given, because it's extremely common that const and immutable types cannot be converted to mutable.
It’s wild how C++ and D disagree on this one. In C++, a copy constructor must produce a mutable copy. Now, C++ and D disagree on what const
means and therefore what counts as a mutable copy. But, and this is the key takeaway, C++ never infers const
on a copy. Never. It does infer const
on a reference, of course.
Nothing of this is out of reach for D. A type for which const objects can’t be copied to initialize a mutable one simply isn’t copyable and therefore can’t be passed by value. A type for which const rvalues can’t be used to initialize a mutable variable aren’t movable.
C++ solves this by binding values by universal reference:
template<typename T>
void f(T&&);
This f
eats anything. For rvalues of type X
, T
is inferred X
, the rvalue is materialized in the caller and passed by (rvalue) reference to f
. For lvalues, T
is inferred X&
so that the parameter becomes of type (T&)&&
which is the same as T&
, thus f
ends up binding the value by (lvalue) reference.
D doesn’t have that. In that regard, D is approximately where C++ was before C++11. D’s ref
can’t bind lvalues, which is probably a good thing. In one of my recent DIP Ideas posts, I outlined @universal ref
and @rvalue ref
.
The best D can do today is this:
void f(T)(auto ref T);
For lvalues, that’s great. Nothing to complain. Rvalues should be moved into the parameter. For that, the type must support moves, which most types do, but the compiler doesn’t check if the move could give you a mutable object. Even if it can, the compiler will give you a qualified parameter.
Because of D’s transitive qualifiers, a mutable copy may not be possible. In that case, we’d have two options: Say the type isn’t copyable, or say “here’s your ‘copy,’ but it’s const.” However, copying a type for which there’s a copy constructor that can give you a mutable object is a no-no.