C++ Logo

std-proposals

Advanced search

Re: Explicit using

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Mon, 25 Jan 2021 10:03:35 -0500
On Mon, Jan 25, 2021 at 5:21 AM BAILLY Yves via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Let me rephrase my proposal. In fact I begin to think the verb “to cast”
> is misleading, confusing in this context, as it suggests a kind of
> transformation, change of representation, whereas there’s none here. [...]
> When I declare:
>
> *using U = new T;*
>
> …then just as before, *U* is another “view” of the same ideal type as *T*,
> but this time we have:
>
> *a U can be seen as a T but a T can’t be seen as a U.* [...]
>
No “cast” involved: it’s only just a different view of the same thing.
>
>
Yes. I was using something like this as my mental model of your proposal,
so I agree and I like this way of explaining it.
However, I don't think this fixes any of the proposal's problems.

So a U object can be seen as a T object but not vice versa. Okay.
But what about the *type* U — can it be seen as the *type* T?
In the existing type-alias case, the answer is absolutely yes; there's one
Platonic type and both `T` and `U` are simply names for that Platonic type.
In your `new T` proposal, it can't work exactly that way, because `T` is
privileged over `U`.

For example:
    template<class A> void f(A a);
    f(T{}); // #1
    f(U{}); // #2
Does #1 call f<T>? (Yes, it must.)
Does #2 call f<U>? (I don't know but I think so.)
Is f<U> the same function as, or a different function from, f<T>? (I don't
know but I think so.)
You do imply that std::is_same<T,U> must be a different class from
std::is_same<T,T>, which means that f<T> and f<U> must be different as well.
So this is a big difference from plain old type aliases. With plain old
type aliases, T and U name the *same* type, and so f<T> and f<U> are two
names for the *same* function.
This disconnect reappears below.
I had originally written more responses here, but I think they're moot,
based on how it goes below.


Now back to your questions and remarks. You defined the context as follow:
>
> *using U = new T;*
>
> *void assign(T& dest, T& src);*
>
> *T t;*
>
> *U u;*
>
> [...]
>
*If* we make the assumption that your *assign()* function is actually
> something equivalent to an *operator=()* semantically, then indeed it may
> seem there’s some inconsistency. But I’d argue that’s *your* choice to
> create such a semantic inconsistency 😉 Give this function another,
> totally different semantic, or rename it to something like
> *displaySideBySide()* with the exact same signature, there’s no longer
> any *apparent* inconsistency. At least I don’t see any.
>

But if we rename `void assign(T&, T&)` to `friend T& operator+=(T&, const
T&)`, then the apparent inconsistency grows *massive*, doesn't it?

    t = u; // OK, U can be seen as a T
    t += u; // OK, U can be seen as a T
    u = t; // somehow not OK??
    u += t; // OK, the left-hand U can be seen as a T

This feels vaguely reminiscent of the problems you'd have if you tried to
make `operator=` a virtual function — and the reason value semantics don't
mesh well with classically polymorphic class hierarchies. In the
classic-OOP domain, the solution is *if you are writing a base class, then
don't provide the value-semantic special members*. But in your proposal,
the "base class" is just "any type at all that might one day be the target
of a `using = new` construct." There's no way for the type author even to
guess that they're writing the "base class" `T` and should therefore avoid
writing functions like `assign` or `operator+=`.

I think I see how the proposal lets us write

    using Height = new int;
    using Tilt = new int;
    void increaseAltitude(Height h);
    void increaseAttitude(Tilt t);
    ...
        Height h;
        increaseAttitude(h); // oops — but, since increaseAttitude takes a
Tilt, this happily won't compile!

However, it doesn't seem to protect us against

    Height h;
    Tilt t;
    t += h; // oops — and this silently calls the built-in candidate
operator+=(int&, int)


[...] About *std::hash<U>*, there’s no magic, just usual rules. When the
> compiler sees *std::hash<U>*, then possibly the process could be:
>
>
> - Do we have a specialized *std::hash<U>* defined at hand? If yes, use
> that. Else…
> - Do we have a type *T* which can be seen as a *U* and for which we
> have a specialized *std::hash<T>*? If yes, use that.
>
> That second bullet point is not the usual rules at all!

*class A { … };*
>
> *template<> std::hash<A> { … };*
>
> *using B = A;*
>
> …then in standard C++ the compiler can “magically” create * std::hash<B>* *because
> a B can be seen as a A* - so it will happily use *std::hash<A>*. Or my
> compiler is severely broken which would be very embarrassing 😊
>

In standard C++, `B` would be another name for `A`. (And maybe that same
Platonic type has other names too, such as `Foo<Bar>` and `int` and
`type_identity<A>::type`.) The compiler realizes that std::hash<B> is
another name for the type which you get by specializing the std::hash
template for that Platonic type (`A` a.k.a. `B`), and realizes that yes it
has been specialized, and instantiates it. There is no act of *creation*
going on there at all.

This is probably your fundamental disconnect.
In C++, `A` and `B` are different *names* but they are in absolutely no
sense different *types*.
They are two *names* for *the same type*.

HTH,
Arthur

Received on 2021-01-25 09:03:50