2549. Tuple EXPLICIT constructor templates that take tuple parameters end up taking references to temporaries and will create dangling references

Section: 23.5.3.1 [tuple.cnstr] Status: C++17 Submitter: Ville Voutilainen Opened: 2015-10-11 Last modified: 2017-07-30

Priority: 2

View other active issues in [tuple.cnstr].

View all other issues in [tuple.cnstr].

View all issues with C++17 status.

Discussion:

Consider this example:

#include <utility>
#include <tuple>

struct X {
  int state; // this has to be here to not allow tuple
             // to inherit from an empty X.
  X() {
  }

  X(X const&) {
  }

  X(X&&) {
  }

  ~X() {
  }
};

int main()
{
  X v;
  std::tuple<X> t1{v};
  std::tuple<std::tuple<X>&&> t2{std::move(t1)}; // #1
  std::tuple<std::tuple<X>> t3{std::move(t2)}; // #2
}

The line marked with #1 will use the constructor template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u); and will construct a temporary and bind an rvalue reference to it. The line marked with #2 will move from a dangling reference.

In order to solve the problem, the constructor templates taking tuples as parameters need additional SFINAE conditions that SFINAE those constructor templates away when Types... is constructible or convertible from the incoming tuple type and sizeof...(Types) equals one. libstdc++ already has this fix applied.

There is an additional check that needs to be done, in order to avoid infinite meta-recursion during overload resolution, for a case where the element type is itself constructible from the target tuple. An example illustrating that problem is as follows:

#include <tuple>

struct A
{
  template <typename T>
  A(T)
  {
  }

  A(const A&) = default;
  A(A&&) = default;
  A& operator=(const A&) = default;
  A& operator=(A&&) = default;
  ~A() = default;
};

int main()
{
  auto x = A{7};
  std::make_tuple(x);
}

I provide two proposed resolutions, one that merely has a note encouraging trait-based implementations to avoid infinite meta-recursion, and a second one that avoids it normatively (implementations can still do things differently under the as-if rule, so we are not necessarily overspecifying how to do it.)

[2016-02-17, Ville comments]

It was pointed out at gcc bug 69853 that the fix for LWG 2549 is a breaking change. That is, it breaks code that expects constructors inherited from tuple to provide an implicit base-to-derived conversion. I think that's just additional motivation to apply the fix; that conversion is very much undesirable. The example I wrote into the bug report is just one example of a very subtle temporary being created.

Previous resolution from Ville [SUPERSEDED]:

  1. Alternative 1:

    1. In 23.5.3.1 [tuple.cnstr]/17, edit as follows:

      template <class... UTypes> EXPLICIT constexpr tuple(const tuple<UTypes...>& u);
      

      […]

      -17- Remarks: This constructor shall not participate in overload resolution unless

      • is_constructible<Ti, const Ui&>::value is true for all i., and

      • sizeof...(Types) != 1, or is_convertible<const tuple<UTypes...>&, Types...>::value is false and is_constructible<Types..., const tuple<UTypes...>&>::value is false.

      [Note: to avoid infinite template recursion in a trait-based implementation for the case where UTypes... is a single type that has a constructor template that accepts tuple<Types...>, implementations need to additionally check that remove_cv_t<remove_reference_t<const tuple<UTypes...>&>> and tuple<Types...> are not the same type. — end note]

      The constructor is explicit if and only if is_convertible<const Ui&, Ti>::value is false for at least one i.

    2. In 23.5.3.1 [tuple.cnstr]/20, edit as follows:

      template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u);
      

      […]

      -20- Remarks: This constructor shall not participate in overload resolution unless

      • is_constructible<Ti, Ui&&>::value is true for all i., and

      • sizeof...(Types) != 1, or is_convertible<tuple<UTypes...>&&, Types...>::value is false and is_constructible<Types..., tuple<UTypes...>&&>::value is false.

      [Note: to avoid infinite template recursion in a trait-based implementation for the case where UTypes... is a single type that has a constructor template that accepts tuple<Types...>, implementations need to additionally check that remove_cv_t<remove_reference_t<tuple<UTypes...>&&>> and tuple<Types...> are not the same type. — end note]

      The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value is false for at least one i.

  2. Alternative 2 (do the sameness-check normatively):

    1. In 23.5.3.1 [tuple.cnstr]/17, edit as follows:

      template <class... UTypes> EXPLICIT constexpr tuple(const tuple<UTypes...>& u);
      

      […]

      -17- Remarks: This constructor shall not participate in overload resolution unless

      • is_constructible<Ti, const Ui&>::value is true for all i., and

      • sizeof...(Types) != 1, or is_convertible<const tuple<UTypes...>&, Types...>::value is false and is_constructible<Types..., const tuple<UTypes...>&>::value is false and is_same<tuple<Types...>, remove_cv_t<remove_reference_t<const tuple<UTypes...>&>>>::value is false.

      The constructor is explicit if and only if is_convertible<const Ui&, Ti>::value is false for at least one i.

    2. In 23.5.3.1 [tuple.cnstr]/20, edit as follows:

      template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u);
      

      […]

      -20- Remarks: This constructor shall not participate in overload resolution unless

      • is_constructible<Ti, Ui&&>::value is true for all i., and

      • sizeof...(Types) != 1, or is_convertible<tuple<UTypes...>&&, Types...>::value is false and is_constructible<Types..., tuple<UTypes...>&&>::value is false and is_same<tuple<Types...>, remove_cv_t<remove_reference_t<tuple<UTypes...>&&>>>::value is false.

      The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value is false for at least one i.

[2016-03, Jacksonville]

STL provides a simplification of Ville's alternative #2 (with no semantic changes), and it's shipping in VS 2015 Update 2.

Proposed resolution:

This wording is relative to N4567.

  1. This approach is orthogonal to the Proposed Resolution for LWG 2312:

    1. Edit 23.5.3.1 [tuple.cnstr] as indicated:

      template <class... UTypes> EXPLICIT constexpr tuple(const tuple<UTypes...>& u);
      

      […]

      -17- Remarks: This constructor shall not participate in overload resolution unless

      • is_constructible<Ti, const Ui&>::value is true for all i, and

      • sizeof...(Types) != 1, or (when Types... expands to T and UTypes... expands to U) !is_convertible_v<const tuple<U>&, T> && !is_constructible_v<T, const tuple<U>&> && !is_same_v<T, U> is true.

      The constructor is explicit if and only if is_convertible<const Ui&, Ti>::value is false for at least one i.

      template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u);
      

      […]

      -20- Remarks: This constructor shall not participate in overload resolution unless

      • is_constructible<Ti, Ui&&>::value is true for all i, and

      • sizeof...(Types) != 1, or (when Types... expands to T and UTypes... expands to U) !is_convertible_v<tuple<U>, T> && !is_constructible_v<T, tuple<U>> && !is_same_v<T, U> is true.

      The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value is false for at least one i.

  2. This approach is presented as a merge with the parts of the Proposed Resolution for LWG 2312 with overlapping modifications in the same paragraph, to provide editorial guidance if 2312 would be accepted.

    1. Edit 23.5.3.1 [tuple.cnstr] as indicated:

      template <class... UTypes> EXPLICIT constexpr tuple(const tuple<UTypes...>& u);
      

      […]

      -17- Remarks: This constructor shall not participate in overload resolution unless

      • sizeof...(Types) == sizeof...(UTypes), and

      • is_constructible<Ti, const Ui&>::value is true for all i, and

      • sizeof...(Types) != 1, or (when Types... expands to T and UTypes... expands to U) !is_convertible_v<const tuple<U>&, T> && !is_constructible_v<T, const tuple<U>&> && !is_same_v<T, U> is true.

      The constructor is explicit if and only if is_convertible<const Ui&, Ti>::value is false for at least one i.

      template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u);
      

      […]

      -20- Remarks: This constructor shall not participate in overload resolution unless

      • sizeof...(Types) == sizeof...(UTypes), and

      • is_constructible<Ti, Ui&&>::value is true for all i, and

      • sizeof...(Types) != 1, or (when Types... expands to T and UTypes... expands to U) !is_convertible_v<tuple<U>, T> && !is_constructible_v<T, tuple<U>> && !is_same_v<T, U> is true.

      The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value is false for at least one i.