C++ Logo

std-discussion

Advanced search

Potential defect in the behaviour of the rvalue constructors for std::tuple and std::pair.

From: Bryan Wong <wongjengyan_at_[hidden]>
Date: Fri, 10 Nov 2023 23:00:40 +0000
Hi all,

I think I've identified a potential defect in the rvalue constructors for
`std::tuple` covered under `22.4.4.1 tuple.cnstr p.20-23` on the latest
draft and wanted to ask about it before making a formal report (may also
need guidance here if possible, as I've never done it before). I'm also not
sure if this issue has been raised before.

When using the tuple rvalue constructors, unexpected behaviour occurs with
non-movable but copyable types, e.g.

struct foo {
    foo() = default;
    foo(foo const&) = default;
    foo& operator=(foo const&) = default;
    foo(foo&&) = delete;
    foo& operator=(foo&&) = delete;
};

Given the existing requirements in the standard, the following function is
well-formed:

tuple<foo,foo> create(tuple<foo&&,foo&&> t) {
    return tuple<foo,foo>(move(t));
}

which is quite unexpected as `foo` is non-movable. Which makes me think
that this isn't an intended behaviour.

Upon looking deeper, this is what I found:

template<class... UTypes> constexpr explicit(/* expr */) tuple(const
tuple<UTypes...>& u); #1
template<class... UTypes> constexpr explicit(/* expr */)
tuple(tuple<UTypes...>&& u); #2
template<class... UTypes> constexpr explicit(/* expr */) tuple(const
tuple<UTypes...>&& u); #3

Both constructors #2 and #3 are disabled according to the constraint set in
`tuple.cnstr p.22` since `foo` is non-movable. However, since the lvalue
constructor exists, the rvalue reference is able to implicitly convert to
`const tuple<foo&&,foo&&>&`. As `get<i>(FWD(u))` resolves to `foo&` in this
case, the constraint is on constructor #1 met and the element gets
copy-constructed unintentionally.

After more investigating, I found that this also affects other components:
* rvalue pair constructors of `std::tuple` [`tuple.cnstr p.24-27`]
* `std::tuple`'s assignment operators [`tuple.assign p.21-26; p.33-28`]
* `std::tuple`'s allocator-extended constructors [`tuple.cnstr p.32-33`]
* rvalue constructors, assingment operators, and piecewise constructors of
`std::pair` [`pairs.pair`]

You can see this behaviour on a couple of compilers on this godbolt page:
https://godbolt.org/z/dd818sY1z

A possible fix may be simply deleting rvalue overloads if there exists an
element in the tuple not constructible with the result of `get<i>(FWD(u))`.

template<typename... UTypes>
using constructible = conjunction<is_constructible<Types, UTypes>...>;

template<class... UTypes>
requires
    (sizeof...(Types) == sizeof...(UTypes)) &&
    (!constructible<UTypes&&...>)
constexpr explicit(/* expr */) tuple(tuple<UTypes...>&& u) = delete;

This would need to be applied to all the affected overloads for tuple and
pair.

What are your thoughts? I think it better aligns with the expected
semantics if the fix was applied and the tuple/pair didn't cause
unintentional copy-constructions.

Kind regards,
Bryan

Received on 2023-11-10 23:01:18