Document number: N3680
Date: 2013-04-14
Author: Daniel Krügler
Project: Programming Language C++, Library Working Group
Reply-to: Daniel Krügler

Improving pair and tuple

Addressed issues: LWG 2051.

Introduction

For many programmers it is a surprise to find out, that the following code is rejected by their compiler:

std::tuple<int, int> pixel_coordinates() 
{
  return {10, -15};  // Oops: Error
}

struct NonCopyable { NonCopyable(int); NonCopyable(const NonCopyable&) = delete; };

std::pair<NonCopyable, double> pmd{42, 3.14};  // Oops: Error

What is wrong with this? Why doesn't this just work?

This paper explains the reason for the current specification of pair and tuple and suggests changes to the working paper to fix these and some other bothersome constraints of these general type wrappers.

Looking backwards

At the time when N3240 was proposed, several driving forces defined the constraints of resolving a bunch of library issues and NB comments.

One notable intention was to prevent that the type wrappers tuple and pair should allow implicit conversions for wrapped types that are not implicitly convertible, as expressed by LWG 1324 and DE 15.

Another relevant requirement was to keep backward-compatibility to C++03 in regard to null pointer literals expressed as integral null constants as described by LWG 811.

At that time there was a strong resistence to add further constructors especially to std::pair. At some point in time there did exist a very large number of such constructors due to allocator support. One important consequence of this pair simplification was the acceptance of N3059.

Thus with the previous acceptance of proposal N3240 the specification provides the following advantages for pair and tuple:

  1. Heterogenous template constructors are constrained upon the criterion that the element-wise source types are implicitly convertible to the element-wise destination types.

    struct B { explicit B(bool); };
    
    std::tuple<B> tb = std::tuple<bool>(); // Error
    
  2. The non-template contructor tuple(const Types&...) and the corresponding template-constructor are explicit. This prevents that a single-element tuple from being copy-initialized by an argument object and has only an explicit constructor for this construction.

    struct X { X(); explicit X(const X&); } x;
    
    std::tuple<X> tx = x; // Error
    
    struct E { explicit E(int); };
    
    std::tuple<E> te = 42; // Error
    
  3. Non-template constructors accepting a sequence of elements, such as explicit tuple(const Types&...) and pair(const T1& x, const T2& y), are still kept to support the special conversion scenario where a pointer(-to-member) element type is initialized with the null pointer constant 0 instead of nullptr.

    class C;
    
    std::tuple<int*> tpi(0); // OK
    std::tuple<int C::*> tpmi(0); // OK
    

Discussion

Notwithstanding the good motivation behind the current specification of pair and tuple, it turns out to have some unfortunate consequences:

  1. It means that tuple objects cannot be returned from a function as simple as this:

    std::tuple<int, int> foo_tuple() 
    {
      return {1, -1};  // Error
    }
    
    std::pair<int, int> foo_pair() 
    {
      return {1, -1};  // OK
    }
    
  2. It means that tuple or pair objects cannot be constructed for element types that cannot be copied:

    struct D { D(int); D(const D&) = delete; };
    
    std::tuple<D> td(12); // Error
    
  3. It even means that tuple or pair objects cannot be direct-constructed for element types via an explicit conversion:

    struct Y { explicit Y(int); };
    
    std::tuple<Y> ty(12); // Error
    
  4. It has been observed by Johannes Schaub that there exists a defect with tuple in regard to the non-template constructor explicit tuple(const Types&...): The current specification has the effect that the instantiation of tuple<> would be required to be ill-formed because it has two conflicting default constructors.

Starting with the last point: This is indeed a simple oversight that slipped in during the tuple standardization. The TR1 did have the following specification:

template <class T1 = unspecified ,
          class T2 = unspecified ,
          ...,
          class TM = unspecified >
class tuple
{
public:
  tuple();
  explicit tuple(P1, P2, ..., PN); // iff N > 0
  […]
};

When the variadic form of tuples was proposed via N2151 and its successors, the size constraint inadvertently got lost.

The other three problems are all caused by (A) constructors that are always explicit and (B) by constrained constructor templates that impose implicit convertible constraints on the element types.

This proposal is intending to solve all these problems by a simple procedure that still ensures that all positive aspects of the current specification are conserved.

"Perfect initialization"

Before explaining the general outline of this proposal it is more helpful to start with a simple, but useful programming idiom.

Consider the following class template A that is intended to be used as a wrapper for some other type T:

#include <type_traits>
#include <utility>

template<class T>
struct A {
  template<class U,
    typename std::enable_if<
      std::is_constructible<T, U>::value &&
      std::is_convertible<U, T>::value
    , bool>::type = false
  >
  A(U&& u) : t(std::forward<U>(u)) {}

 template<class U,
    typename std::enable_if<
      std::is_constructible<T, U>::value &&
      !std::is_convertible<U, T>::value
    , bool>::type = false
  >
  explicit A(U&& u) : t(std::forward<U>(u)) {}
  
  T t;
};

The shown constructors both use perfect forwarding and they have essentially the same signatures except for one being explicit, the other one not. Furthermore, they are mutually exclusively constrained. In other words: This combination behaves for any destination type T and any argument type U like a single constructor that is either explicit or non-explicit (or no constructor at all). Attempts to construct a A<T> from some value of type U will reflect the allowed initialization forms of the wrapped type T:

struct Im{ Im(int){} };
struct Ex{ explicit Ex(int){} };

A<Im> ai1(1); // OK
A<Im> ai2{2}; // OK

A<Im> ai3 = 3;   // OK
A<Im> ai4 = {4}; // OK

A<Ex> ae1(1); // OK
A<Ex> ae2{2}; // OK

A<Ex> ae3 = 3;   // Error
A<Ex> ae4 = {4}; // Error

This technique can easily be extended to the variadic template case, and when doing so can be considered as a key to solving the problems of tuple and pair.

It should be noted here, that for the general case the std::is_constructible<T, U>::value requirement for the non-explicit constructor which is constrained on std::is_convertible<U, T>::value is not redundant, because it is possible to create types that can be copy-initialized but not direct-initialized:

struct Odd {
  explicit Odd(int) = delete;
  Odd(long){}
};

Odd o2 = 1; // OK
Odd o1(1);  // Error

Outline

As shown above, the current overconstraining restrictions of the pair and tuple constructors are due to unconditional usage of explicit and implicitly convertible requirements.

The general approach of this proposal is to require "perfect initialization" semantics for pair and tuple constructors extended to the variadic case. Albeit this seemingly doubles the number of constructor declarations in the draft, it does not change the effective number of these for a particular combination of element type and source type of some initialization due to their mutual exclusion property.

In theory the same technique could be applied to the piecewise_construct_t of pair. This proposal does not propose this, because this constructor is specifically asked for by the corresponding tag and there are no further constraint except the is_constructible requirements.

In addition, this proposal fixes the specification problem of tuple<>'s default constructors.

The wording is intentionally chosen, so that an implementation is not required (but allowed) to use the "perfect initialization" idiom.

This is done by taking advantage of the already existing nomenclature "This function does not participate in overload resolution unless […]". Its worth emphasizing that even though this phrase is usually used to describe constrained templates in the Library specification, the actual wording of this doesn't necessarily imply to "sfinae out" template functions. Many library implementations solve this problem by providing a specialization for the empty tuple case that does not provide the additional default constructor, for example. This is also a valid way to ensure that functions don't participate in overload resolution.

Why not explicit for single argument constructors only?

In C++03 explicit constructors had no behavioural difference, unless they had been single-argument constructors, so one might suggest to restrict adding the explicit keyword to constructors that take exactly one argument.

I think this is idea is flawed (unless I'm using specifically tagged constructors like the piecewise-one of pair). Consider the following example:

#include <tuple>
#include <chrono>

using hms_t = std::tuple<std::chrono::hours, std::chrono::minutes, std::chrono::seconds>;

void measure(hms_t times);

int main()
{
  using namespace std;
  measure(make_tuple(3, 4, 5)); // very scary
  measure({3, 4, 5});           // even scarier
  using namespace std::chrono;
  measure(make_tuple(hours(3), minutes(4), seconds(5))); // Perfectly clear!
  measure({hours(3), minutes(4), seconds(5)});           // Also clear!
  measure(hms_t{3, 4, 5});                               // And this, too
}

If the former two calls to function measure where possible, this would directly subvert the intended explicitness of the std::duration constructor and would make using the time-utility types much more unsafe.

Editorial Representation

During the writeup of this proposal I had the idea of replacing the prototype declaration pairs by a single one expressed by some pseudo-macro that looks like a single declaration. For example

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

could instead be declared as follows:

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

This form of representation still means that EXPLICIT(see below) needs to be defined somewhere and somehow, but only once. I would like to get feedback before attempting to change the current proposed wording.

Proposed resolution

The proposed wording changes refer to N3485.

  1. namespace std {
      template <class T1, class T2>
      struct pair {
        typedef T1 first_type;
        typedef T2 second_type;
    
        T1 first;
        T2 second;
        pair(const pair&) = default;
        pair(pair&&) = default;
        constexpr pair();
        constexpr pair(const T1& x, const T2& y);
        explicit constexpr pair(const T1& x, const T2& y);
        template<class U, class V> constexpr pair(U&& x, V&& y);
        template<class U, class V> explicit constexpr pair(U&& x, V&& y);
        template<class U, class V> constexpr pair(const pair<U, V>& p);
        template<class U, class V> explicit constexpr pair(const pair<U, V>& p);
        template<class U, class V> constexpr pair(pair<U, V>&& p);
        template<class U, class V> explicit constexpr pair(pair<U, V>&& p);
        template <class... Args1, class... Args2>
        pair(piecewise_construct_t,
          tuple<Args1...> first_args, tuple<Args2...> second_args);
    
        pair& operator=(const pair& p);
        template<class U, class V> pair& operator=(const pair<U, V>& p);
        pair& operator=(pair&& p) noexcept(see below);
        template<class U, class V> pair& operator=(pair<U, V>&& p);
    
        void swap(pair& p) noexcept(see below);
      };
    }
    

  2. Change 20.3.2 [pairs.pair] around p5 as indicated:

    constexpr pair(const T1& x, const T2& y);
    explicit constexpr pair(const T1& x, const T2& y);
    

    -5- Requires: is_copy_constructible<first_type>::value is true and is_copy_constructible<second_type>::value is true.

    -6- Effects: The constructor initializes first with x and second with y.

    -?- Remarks: The non-explicit constructor shall not participate in overload resolution unless

    • is_copy_constructible<first_type>::value is true and is_copy_constructible<second_type>::value is true, and

    • const first_type& is implicitly convertible to first_type and const second_type& is implicitly convertible to second_type

    The explicit constructor shall not participate in overload resolution unless

    • is_copy_constructible<first_type>::value is true and is_copy_constructible<second_type>::value is true, and

    • const first_type& is not implicitly convertible to first_type or const second_type& is not implicitly convertible to second_type

  3. Change 20.3.2 [pairs.pair] around p7 as indicated:

    template<class U, class V> constexpr pair(U&& x, V&& y);
    template<class U, class V> explicit constexpr pair(U&& x, V&& y);
    

    -7- Requires: is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true.

    -8- Effects: The constructor initializes first with std::forward<U>(x) and second with std::forward<V>(y).

    -9- Remarks: If U is not implicitly convertible to first_type or V is not implicitly convertible to second_type this constructor shall not participate in overload resolution. The non-explicit constructor shall not participate in overload resolution unless

    • is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true, and

    • U is implicitly convertible to first_type and V is implicitly convertible to second_type

    The explicit constructor shall not participate in overload resolution unless

    • is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true, and

    • U is not implicitly convertible to first_type or V is not implicitly convertible to second_type

  4. Change 20.3.2 [pairs.pair] around p10 as indicated:

    template<class U, class V> constexpr pair(const pair<U, V>& p);
    template<class U, class V> explicit constexpr pair(const pair<U, V>& p);
    

    -10- Requires: is_constructible<first_type, const U&>::value is true and is_constructible<second_type, const V&>::value is true.

    -11- Effects: Initializes members from the corresponding members of the argument.

    -12- Remarks: This constructor shall not participate in overload resolution unless const U& is implicitly convertible to first_type and const V& is implicitly convertible to second_type. The non-explicit constructor shall not participate in overload resolution unless

    • is_constructible<first_type, const U&>::value is true and is_constructible<second_type, const V&>::value is true, and

    • const U& is implicitly convertible to first_type and const V& is implicitly convertible to second_type

    The explicit constructor shall not participate in overload resolution unless

    • is_constructible<first_type, const U&>::value is true and is_constructible<second_type, const V&>::value is true, and

    • const U& is not implicitly convertible to first_type or const V& is not implicitly convertible to second_type

  5. Change 20.3.2 [pairs.pair] around p13 as indicated:

    template<class U, class V> constexpr pair(pair<U, V>&& p);
    template<class U, class V> explicit constexpr pair(pair<U, V>&& p);
    

    -13- Requires: is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true.

    -14- Effects: The constructor initializes first with std::forward<U>(p.first) and second with std::forward<V>(p.second).

    -15- Remarks: This constructor shall not participate in overload resolution unless U is implicitly convertible to first_type and V is implicitly convertible to second_type. The non-explicit constructor shall not participate in overload resolution unless

    • is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true, and

    • U is implicitly convertible to first_type and V is implicitly convertible to second_type

    The explicit constructor shall not participate in overload resolution unless

    • is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true, and

    • U is not implicitly convertible to first_type or V is not implicitly convertible to second_type

  6. Change 20.4.2 [tuple.tuple], class template tuple synposis as indicated. The intent is to declare the set of "conditionally explicit" constructors.

    namespace std {
      template <class... Types>
      class tuple {
      public:
        // 20.4.2.1, tuple construction
        constexpr tuple();
        constexpr tuple(const Types&...);          // only if sizeof...(Types) >= 1
        explicit constexpr tuple(const Types&...); // only if sizeof...(Types) >= 1
        template <class... UTypes>
          constexpr tuple(UTypes&&...);            // only if sizeof...(Types) >= 1
        template <class... UTypes>
          explicit constexpr tuple(UTypes&&...);   // only if sizeof...(Types) >= 1
        
        tuple(const tuple&) = default;
        tuple(tuple&&) = default;
      
        template <class... UTypes>
          constexpr tuple(const tuple<UTypes...>&);
        template <class... UTypes>
          explicit constexpr tuple(const tuple<UTypes...>&);
        template <class... UTypes>
          constexpr tuple(tuple<UTypes...>&&);
        template <class... UTypes>
          explicit constexpr tuple(tuple<UTypes...>&&);
        template <class U1, class U2>
          constexpr tuple(const pair<U1, U2>&);          // only if sizeof...(Types) == 2
        template <class U1, class U2>
          explicit constexpr tuple(const pair<U1, U2>&); // only if sizeof...(Types) == 2
        template <class U1, class U2>
          constexpr tuple(pair<U1, U2>&&);               // only if sizeof...(Types) == 2
        template <class U1, class U2>
          explicit constexpr tuple(pair<U1, U2>&&);      // only if sizeof...(Types) == 2
      
        template <class Alloc>
          tuple(allocator_arg_t, const Alloc& a);
        template <class Alloc>
          tuple(allocator_arg_t, const Alloc& a, const Types&...);
        template <class Alloc, class... UTypes>
          tuple(allocator_arg_t, const Alloc& a, UTypes&&...);
        template <class Alloc>
          tuple(allocator_arg_t, const Alloc& a, const tuple&);
        template <class Alloc>
          tuple(allocator_arg_t, const Alloc& a, tuple&&);
        template <class Alloc, class... UTypes>
          tuple(allocator_arg_t, const Alloc& a, const tuple<UTypes...>&);
        template <class Alloc, class... UTypes>
          tuple(allocator_arg_t, const Alloc& a, tuple<UTypes...>&&);
        template <class Alloc, class U1, class U2>
          tuple(allocator_arg_t, const Alloc& a, const pair<U1, U2>&);
        template <class Alloc, class U1, class U2>
          tuple(allocator_arg_t, const Alloc& a, pair<U1, U2>&&);
    
        [..]
      };
    }
    
  7. Change 20.4.2.1 [tuple.cnstr] around p6 as indicated:

    constexpr tuple(const Types&...);
    explicit constexpr tuple(const Types&...);
    

    -6- Requires: is_copy_constructible<Ti>::value is true for all i.

    -7- Effects: Initializes each element with the value of the corresponding parameter.

    -?- Remarks: The non-explicit constructor shall not participate in overload resolution unless

    • sizeof...(Types) >= 1,

    • is_copy_constructible<Ti>::value is true for all i, and

    • is_convertible<const Ti&, Ti>::value is true for all i

    The explicit constructor shall not participate in overload resolution unless

    • sizeof...(Types) >= 1,

    • is_copy_constructible<Ti>::value is true for all i, and

    • is_convertible<const Ti&, Ti>::value is false for at least one i

  8. Change 20.4.2.1 [tuple.cnstr] around p8 as indicated:

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

    -8- Requires: sizeof...(Types) == sizeof...(UTypes). is_constructible<Ti, Ui&&>::value is true for all i.

    -9- Effects: Initializes the elements in the tuple with the corresponding value in std::forward<UTypes>(u).

    -10- Remarks: This constructor shall not participate in overload resolution unless each type in UTypes is implicitly convertible to its corresponding type in Types. The non-explicit constructor shall not participate in overload resolution unless

    • sizeof...(Types) >= 1,

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

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

    The explicit constructor shall not participate in overload resolution unless

    • sizeof...(Types) >= 1,

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

    • is_convertible<Ui&&, Ti>::value is false for at least one i

  9. Change 20.4.2.1 [tuple.cnstr] around p15 as indicated:

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

    -15- Requires: sizeof...(Types) == sizeof...(UTypes). is_constructible<Ti, const Ui&>::value is true for all i.

    -16- Effects: Constructs each element of *this with the corresponding element of u.

    -17- Remarks: This constructor shall not participate in overload resolution unless const Ui& is implicitly convertible to Ti for all i. The non-explicit constructor shall not participate in overload resolution unless

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

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

    The explicit constructor shall not participate in overload resolution unless

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

    • is_convertible<const Ui&, Ti>::value is false for at least one i

  10. Change 20.4.2.1 [tuple.cnstr] around p18 as indicated:

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

    -18- Requires: sizeof...(Types) == sizeof...(UTypes). is_constructible<Ti, Ui&&>::value is true for all i.

    -19- Effects: For all i, initializes the ith element of *this with std::forward<Ui>(get<i>(u)).

    -20- Remarks: This constructor shall not participate in overload resolution unless each type in UTypes is implicitly convertible to its corresponding type in Types. The non-explicit constructor shall not participate in overload resolution unless

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

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

    The explicit constructor shall not participate in overload resolution unless

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

    • is_convertible<Ui&&, Ti>::value is false for at least one i

  11. Change 20.4.2.1 [tuple.cnstr] around p21 as indicated:

    template <class U1, class U2> constexpr tuple(const pair<U1, U2>& u);
    template <class U1, class U2> explicit constexpr tuple(const pair<U1, U2>& u);
    

    -21- Requires: sizeof...(Types) == 2. is_constructible<T0, const U1&>::value is true for the first type T0 in Types and is_constructible<T1, const U2&>::value is true for the second type T1 in Types.

    -22- Effects: Constructs the first element with u.first and the second element with u.second.

    -23- Remarks: This constructor shall not participate in overload resolution unless const U1& is implicitly convertible to T0 and const U2& is implicitly convertible to T1. The non-explicit constructor shall not participate in overload resolution unless

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

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

    The explicit constructor shall not participate in overload resolution unless

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

    • is_convertible<const Ui&, Ti>::value is false for at least one i

  12. Change 20.4.2.1 [tuple.cnstr] around p24 as indicated:

    template <class U1, class U2> constexpr tuple(pair<U1, U2>&& u);
    template <class U1, class U2> explicit constexpr tuple(pair<U1, U2>&& u);
    

    -24- Requires: sizeof...(Types) == 2. is_constructible<T0, U1&&>::value is true for the first type T0 in Types and is_constructible<T1, U2&&>::value is true for the second type T1 in Types.

    -25- Effects: Initializes the first element with std::forward<U1>(u.first) and the second element with std::forward<U2>(u.second).

    -26- Remarks: This constructor shall not participate in overload resolution unless U1 is implicitly convertible to T0 and U2 is implicitly convertible to T1. The non-explicit constructor shall not participate in overload resolution unless

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

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

    The explicit constructor shall not participate in overload resolution unless

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

    • is_convertible<Ui&&, Ti>::value is false for at least one i

Implementation Hint

The following example presents how to constrain even non-template functions such as the constructors that directly take the element types.

template<class T1, class T2>
struct pair {

  […]

  template<class U1 = T1, class U2 = T2,
    typename enable_if<
      is_copy_constructible<U1>::value && is_copy_constructible<U2>::value &&
      is_convertible<const U1&, U1>::value && is_convertible<const U2&, U2>::value
    , bool>::type = false
  >
  constexpr pair(const T1&, const T2&);

  template<class U1 = T1, class U2 = T2,
    typename enable_if<
      is_copy_constructible<U1>::value && is_copy_constructible<U2>::value &&
      !(is_convertible<const U1&, U1>::value && is_convertible<const U2&, U2>::value)
    , bool>::type = false
  >
  explicit constexpr pair(const T1&, const T2&);
};

Acknowledgements

I would like to thank Howard Hinnant for his very helpful discussions and comments during reviews of this paper and for his motivating example. Thanks also to Jonathan Wakely for his review that improved this proposal to a large extend.