UniversE
UniversE

Reputation: 2507

Why is type convertibility in C++ not transitive?

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:

  1. Does is_convertible really captures the intuition of convertibility?
  2. If not, what else does it capture? Or phrased differently: is my intuition of convertibility not suitable?
  3. If we understand 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

Answers (2)

AndyG
AndyG

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

  • lvalue to rvalue conversion (Lvalue Transformation), int const& to int, followed by
  • Identity conversion (reference binding), int 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

463035818_is_not_an_ai
463035818_is_not_an_ai

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

Related Questions