Reputation: 2507
Consider the following static asserts:
static_assert(std::is_convertible_v<int const&, int const>);
static_assert(std::is_convertible_v<int const, int>);
static_assert(std::is_convertible_v<int, int &&>);
static_assert(std::is_convertible_v<int const&, int &&>);
The above three asserts pass, but the last assert fails.
This means that type convertibility in C++ is not transitive in general, which I think is very counterintuitive.
I searched the standard and the cppreference website to find any evidence that this is intended behavior, but I have not been successful so far.
Interestingly, for lvalue-references, everything is fine, because std::is_convertible_v<int, int&>
is false. I would also expect that for rvalue-references.
I assume it has something to do with how is_convertible
is defined. In the definition, the To
argument appears as a return type of an imaginary function. From my understanding, a fresh value of any type is a temporary and thus convertible to an rvalue-reference. Therefor std::is_convertible_v<T, T&&>
holds for any type T
.
So more specifically I ask the following questions:
is_convertible
really captures the intuition of convertibility?is_convertible
as a binary relation, shouldn't it be a preorder, i.e. transitive? Why not?Intuitively, imho, convertiblity should mean: whenever a type To
is required, you can also use type From
. And this would imply transitivity.
In particular, T
should not be convertible to T&&
, because you cannot use T
where T&&
is required (you may not move from a T
for instance, but you may move from a T&&
).
Am I getting something seriously wrong here?
Upvotes: 2
Views: 443
Reputation: 41092
This means that type convertibility in C++ is not transitive in general, which I think is very counterintuitive.
Generally speaking, you don't want to convert const T&
to T&&
. This could have disastrous implications. You'd want a compiler error so that you don't accidentally std::move
the caller's data from them (or alternatively, create an unintended copy that looks like no copy was made)
Now, what does the standard have to say about this? [conv]
A standard conversion sequence is a sequence of standard conversions in the following order:
- Zero or one conversion from the following set: lvalue-to-rvalue conversion, array-to-pointer conversion, and function-to-pointer conversion.
- Zero or one conversion from the following set: integral promotions, floating-point promotion, integral conversions, floating-point conversions, floating-integral conversions, pointer conversions, pointer-to-member conversions, and boolean conversions.
- Zero or one function pointer conversion.
- Zero or one qualification conversion.
So, we can implicitly convert int const&
to int
via lvalue-to-rvalue conversion (on non-class types it removes cv qualification).
And we can also implicitly convert int
to int&&
via an Identity converstion (no conversion required because we can perform reference binding of a prvalue to an rvalue reference).
But we cannot implicitly convert int const&
to int&&
because that would require
int const&
to int
, followed byint
to int const&
This is because we cannot mix an Identity conversion with other conversions in that way, according to [over.ics.scs]:
a standard conversion sequence either is the Identity conversion by itself (that is, no conversion) or consists of one to three conversions from the other four categories.
(The categories being defined by the table in [over.ics.scs])
Upvotes: 1
Reputation: 122133
Intuitively, imho, convertiblity should mean: whenever a type To is required, you can also use type From....
And that is what it does mean.
...And this would imply transitivity.
No this is not correct. Not every binary relation must be transitive. From cppreferene on implicit conversions:
Implicit conversion sequence consists of the following, in this order:
1) zero or one standard conversion sequence;
2) zero or one user-defined conversion;
3) zero or one standard conversion sequence.
When considering the argument to a constructor or to a user-defined conversion function, only one standard conversion sequence is allowed (otherwise user-defined conversions could be effectively chained). When converting from one built-in type to another built-in type, only one standard conversion sequence is allowed.
The exact rules are rather involved, but consider "zero or one user-defined conversion;" so when you have user defined conversions from Foo
to Bar
and from Bar
to Baz
then this does not necessarily imply that a Foo
converts to a Baz
!
It is not std::is_convertible
that has a weird notion of convertible, but the rules in C++ about what is convertible are not transitive to begin with.
Upvotes: 3