C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Extend std::type_info with more information

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Thu, 11 Apr 2024 12:14:21 -0400
On Thu, Apr 11, 2024 at 11:22 AM Mihail Mihaylov via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> On Thu, Apr 11, 2024 at 5:43 PM Jason McKesson via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> No, the only differences are that `any` has value semantics and is
>> type safe. In every other way, `any` does what `void*` does, and `any`
>> doesn't do what `void*` cannot do.
>
>
> That's like saying that the only difference between fish and birds is that birds can fly and fish can breathe underwater. Value semantics vs reference/pointer semantics is a very big difference, and so is type-safety. And really, void* cannot do anything by itself. The only thing that void* can do is to be relabeled as any other pointer. `Void*` is raw building material, `any` is a utility.

It is a big difference in terms of how you mechanically interface with
the object and which bugs can arise. But from the perspective of "why
I'm using this type/idiom", they are the same: you want to pass an
object from point A to point C through an intermediary B which is
generic and doesn't need to know the actual type you're passing
through it, and B can't just use a template.

Historically, you used `void*` and hoped that nobody got the types
wrong. Nowadays, unless you're using a C-like interface, you can use
`std::any` and actually detect errors when people get the types wrong.
The mechanisms are different; the purpose is the same.

>> Furthermore, even if we are to think of `any` as a generic
>> discriminated union, discriminated unions can't do this either. If you
>> have a `variant<T, U>`, you can't get a pointer to `B` which is a base
>> class of `T` directly. You must get the `T*` out of it, then manually
>> cast it to `B*`. If you have a `union {T t; U u};`, and you know that
>> `t` is active, you can't just cast the union *itself* to a `B*`. You
>> have to access `t` and perform the conversion on that.
>
> Yes, I agree that an open discriminated union is not an entirely correct characterization. A better description would be to say that `std::any` is a polymorphic type which can hold values of unrelated types. The important part is that the reasonable usage of `std::any` is to check whether the value is of a specific type that we know how to handle, and if it is, take the value out of it and handle it.
>
> And when we are asking the container, is the value in it of a given type, it is intuitive to expect that if the object inside it is of a derived type the answer would be positive, just like everywhere else in the language.

`std::same_as<D, B>` does not yield `true` even if `B` is a base class
of `D`. Asking "same as" is a different question from "derived from".
`any_cast` is a matter of "same as", not "derived from", and there's
no expectation in `any` that you would ever ask the latter.

It seems to me that you're searching for a way to describe `any`'s
meaning such that it makes sense to get a base class directly from it.
Indeed, the boost::any description of why the type exists is
particularly relevant:

> Discriminated types that contain values of different types but do not attempt conversion between them

This is what the type was made for, as written by the people who made
it. A lack of C++ conversions is a *feature,* not a bug.

Once you open the door to derived-to-base conversions, you now have to
ask why not others. Why not any standard conversion that C++ supports:
float-to-int, etc?

>> > The intended use of `any` is just like with any discriminated union - the receiver tests the discriminator against one or more options that it knows how to handle and performs a cast if it turns out to be one of them. And so it actually makes sense for a test against a base class to succeed when the `any` contains an instance of a derived class. Of course, as you pointed out, the actual cast should pass through the derived type, before it gets to the base type.
>>
>> That's the point: if the receiving code knows the proper derived type
>> it was given, then there's no need for a feature. As such, the only
>> time you would want what the OP is asking for is if the receiving code
>> doesn't know what the derived type is.
>
>
> The whole point of using `any` instead of `variant` is to support the use case where the receiver of the container doesn't know all possible values that can be stored in it. For `any` to be actually useful, you need to either store in it unrelated types, or be able to get a base type when the `any` contains an instance of a derived type, even if the derived type is unknown to the receiver.

Why do you *need* to be able to get a base type from `any`? Or more
specifically, why doesn't the receiver know what the derived type is?
`any` seems plenty capable (within its narrow use cases) without this
functionality.

Note that `boost::any` lacks this functionality despite having existed
for 20+ years. If it were really that important, wouldn't they have
added it by now?

>> What the OP wants is for the receiving code to get any class derived
>> from a particular base class. `void*` can't do that regardless of
>> type-safety questions, so `any` shouldn't either. `any` isn't the
>> right tool for this job, especially considering how value-centric it
>> is.
>
>
> If `any` isn't the right tool for the job, what is?

In the standard library, there isn't one. If one were to exist, it
would need to have a different interface. In particular, `any_cast` is
value-oriented; it *wants* to return a copy of its contained value. To
make it return a pointer, you have to explicitly get a pointer to the
`any`. And it can't return a reference *at all*.

Compare this to `std::get` for tuples or variants; they always return
a reference.

If you're making a generic discriminated union where it is part of the
interface that you can extract base class references from derived
class contents, then you need casting infrastructure that is
reference-oriented. That way, `any_cast<B>(a)` would work since that
function would always return a reference.

Though personally, I argue that this functionality just isn't
important enough to standardize. It's not that you can't imagine a
circumstance where it's useful; it's just that this is not a
meaningful subset of a type whose use cases are already countable on
one hand.

`any` is a very specialized type for very particular needs. And that's
good; we *want* compile-time typing, and we should discourage more
error-prone idioms. But there are circumstances where you need type
erasure, and `any` provides that.

There are times when you need to pass an object of a known type
through an intermediary that handles multiple such operations and
therefore doesn't need to know what type you're passing. Callbacks,
signal processing, things like that. In those cases, the sender and
the receiver really ought to be on the same page as to what is being
sent and received.

*Precisely* what is being sent and received.

I don't think it's good code to send an object of one type through
this interface, but have the receiver be blind to exactly what that
type was. Especially if that type uses *value semantics* the way `any`
does.

Received on 2024-04-11 16:14:34