7

I've got the following implementation of the c++ concept move_constructible from cppreference

template<typename _Tp>
concept move_constructible =
    constructible_from<_Tp, _Tp> &&
    convertible_to<_Tp, _Tp>;

I don't get why this works. I presume any type can be converted to itself, so the second requirement is pointless (God, I must be very wrong about something). Also, for the first requirement I would have expected something like constructible_from<_Tp, _Tp&&> to check if the type can be constructed from rvalue-ref (thus, moved).

Please explain how this implementation works.

Enlico
  • 23,259
  • 6
  • 48
  • 102
DeltA
  • 564
  • 4
  • 12
  • On top of the selected answer, I want to add that it’s not true that any type can be converted to itself. Any type can be converted to a reference to itself, but T to T conversion requires a copy (implemented via copy or move construction), with actual copy/move ellided from the final code (feature called “copy ellision”). – oliora Dec 05 '21 at 09:34

1 Answers1

5

Most traits/concepts automatically add && to the types of "source" arguments (things that are passed to functions, as in std::is_invocable, or constructed from, as in std::is_constructible).

I.e. constructible_from<A, B> is equivalent to constructible_from<A, B &&> (&& is automatically added to the second argument, but not to the first), and convertible_to<A, B> is equivalent to convertible_to<A &&, B>.

Note that if a type already includes &, adding && to it has no effect. So, while T and T && are equivalent here, T & is not.


This can be inferred from those traits/concepts being defined in terms of std::declval<T>(), which returns T &&.

For the reason why std::declval<T &>() returns T &, see reference collapsing.


NOTE: There is one exception: passing an incomplete type (such as a class that was declared but not defined) as a non-first argument to is_constructible (and constructible_from) is UB, and some compilers reject it. Appending && manually works around this rule, so in generic code, you should always prefer passing T && to those traits.

But specifically for move_constructible there's no difference, since it makes no sense for incomplete types in the first place.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 2
    Why are both `convertible_to` and `constructible_from` needed? – Alex Guteniev Nov 22 '21 at 11:34
  • @AlexGuteniev I'm not sure. `constructible_from` looks redundant, but maybe it somehow improves concept subsumption. – HolyBlackCat Nov 22 '21 at 17:18
  • To forbidden non-implicit move constructor like `explicit A(A&&) = default;`? – 康桓瑋 Nov 23 '21 at 02:29
  • @康桓瑋 I think `convertible_to` alone would be enough for that, since it seems to check both implicit and explicit conversions. – HolyBlackCat Nov 23 '21 at 17:53
  • Wouldn't it be strange if *only* `convertible_to` was used to build`move_constructible`? `constructible_from` is a necessary semantic requirement. – 康桓瑋 Nov 24 '21 at 01:47
  • Reading the references, it seems there are some little differences between `convertible_to` and `constructible_from`. For example, `constructible_from` requires the type to be an object or reference type. I presume `convertible_to` works for other types of expressions. – DeltA Nov 25 '21 at 05:33
  • Does `constructible_from` really add `&&` internally? In libstdc++ [I don't think it does](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/concepts#L152-L153). I guess that's because a *move-constructible* type can be initialized using its copy constructor unless its move constructor is explicitly deleted. I can only find [`is_move_constructible`](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/type_traits#L3256) which adds `&&`. – Dean Seo Aug 07 '23 at 11:10
  • @DeanSeo It forward to `is_constructible_v`, which should in turn add `&&`. – HolyBlackCat Aug 07 '23 at 11:16
  • @HolyBlackCat [I don't think it does](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/type_traits#L1038-L1050) and that's okay, no? Because *move_constructible* is distinct to *move-only*. – Dean Seo Aug 08 '23 at 10:07
  • 1
    @DeanSeo What am I supposed to see there? If we don't see `&&` being added in then library code, then it's implicitly added in the `__is_constructible` builtin. It's easy to check: http://coliru.stacked-crooked.com/a/5625df8f4cea973c *"move-constructible type can be initialized using its copy constructor"* It's implicitly deleted if a move constructor is present. While all (sane) copyable types are movable, the opposite isn't true, and many movable types are not copyable (e.g. `unique_ptr`). *"move_constructible is distinct to move-only."* Yes, and? – HolyBlackCat Aug 08 '23 at 21:02
  • 1
    @DeanSeo Cppreference says that [`std::constructible_from`](https://en.cppreference.com/w/cpp/concepts/constructible_from) calls [`std::is_constructible_v`](https://en.cppreference.com/w/cpp/types/is_constructible), which in turn checks whether `T obj(std::declval()...);` compiles, and [`std::declval()`](https://en.cppreference.com/w/cpp/utility/declval) returns `T &&`. – HolyBlackCat Aug 08 '23 at 21:12
  • @HolyBlackCat Great, thanks. The builtin `__is_constructible` would do then add `&&` indeed. For some corner cases, the builtin function by the compiler seems essential as [the implementation of `std::is_constructible` is *tricky* and can't be portable.](https://stackoverflow.com/a/70836853/921070). – Dean Seo Aug 09 '23 at 16:21