Movatterモバイル変換


[0]ホーム

URL:


Document number: N4387
Date: 2015-03-14
Revises: N4064
Authors: Daniel Krügler, Geoffrey Romer, Ville Voutilainen
Project: Programming Language C++, Library Working Group
Reply-to:
Daniel Krügler

Improvingpair andtuple, revision 3

Addressed issues:

Revision History

Changes sinceN4064 (revision 2):

Changes sinceN3739 (revision 1):

Changes sinceN3680:

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 ofpair andtuple and suggestschanges to the working paper to fix these and some other bothersome constraints of these general type wrappers.

Looking backwards

At the time whenN3240 wasproposed, 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 wrapperstuple andpair should allowimplicit conversions for wrapped types that are not implicitly convertible, as expressed byLWG 1324 andDE 15.

Another relevant requirement was to keep backward-compatibility to C++03 in regard to null pointer literalsexpressed as integral null constants as described byLWG 811.

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

Thus with the previous acceptance of proposalN3240the specification provides the followingadvantages forpair andtuple:

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

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

    struct X { X(); explicit X(const X&); } x;std::tuple<X> tx = x;// Errorstruct E { explicit E(int); };std::tuple<E> te = 42;// Error
  3. Non-template constructors accepting a sequence of elements, such asexplicit tuple(const Types&...)andpair(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 thenull pointer constant0 instead ofnullptr.

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

Discussion

We propose thatpair/tuple obey the same initialization rules as their underlying element types. Unless we have such "perfect initialization",pair andtuple exhibit confusing and unintuitive behavior as illustrated by the examples below.

  1. It means thattuple objects cannot bereturned 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 thattuple orpair objects cannot beconstructed 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 thattuple orpair objects cannot bedirect-constructed for element types via an explicit conversion:

    struct Y { explicit Y(int); };std::tuple<Y> ty(12);// Error
  4. It has been observed byJohannes Schaub that there exists a defect withtuple in regard to the non-template constructorexplicit tuple(const Types&...): The current specification has the effect thatthe instantiation oftuple<> 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 thetuple standardization. TheTR1 document 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 viaN2151and its successors, the highlighted size constraint inadvertently got lost.

The other three problems are all caused by (A) constructors that are alwaysexplicit and (B) by constrained constructortemplates that imposeimplicit convertible constraints on the element types.

So, notwithstanding the good motivation behind the current specification ofpair andtuple, it turns out to havesome unfortunate consequences.

This proposal is intending to solve all these problems by a simple procedure that still ensures that all positive aspects of the currentspecification 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 templateA that is intended to be used as a wrapper for some other typeT:

#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 useperfect forwardingand they have essentially the same signatures except for one being explicit, the other one not. Furthermore, they aremutually exclusively constrained. In other words: This combination behaves for any destination typeT and any argument typeU like asingle constructor that iseither explicit or non-explicit (or no constructorat all). Attempts to construct aA<T> from some value of typeU will reflect the allowed initialization forms of the wrapped typeT:

struct Im{ Im(int){} };struct Ex{ explicit Ex(int){} };A<Im> ai1(1); // OKA<Im> ai2{2}; // OKA<Im> ai3 = 3;   // OKA<Im> ai4 = {4}; // OKA<Ex> ae1(1); // OKA<Ex> ae2{2}; // OKA<Ex> ae3 = 3;   // ErrorA<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 solvingthe problems oftuple andpair.

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

struct Odd {  explicit Odd(int) = delete;  Odd(long){}};Odd o2 = 1; // OKOdd o1(1);  // Error

Technically it would be possible to apply the same technique of creating element-dependent explicit or non-explicit defaultconstructors. This application was shortly considered during the write-up of this proposal, but rejected because for the current C++ rules there is no longer any observable difference for anexplicit default constructor that cannot be invoked with more than zero arguments and one that is notexplicit.

The technique cannot be applied to copy/move operators in the same way as for the other constructors (because these specialmember functions cannot be templates) and given the very rare request for such a support the idea was no further investigated by the author. A second argument against providing this support is based on the consistency with the core language rulesthat theexplicit-character of these constructors is not conserved for the implicitly declared versions in classes that contain corresponding sub-objects with such explicit constructors.

Outline

As shown above, the current over-constraining restrictions of thepair andtuple constructors are due tounconditional usage ofexplicit andimplicitly convertible requirements.

The general approach of this proposal is to require "perfect initialization" semantics forpair andtuple constructors extended to the variadic case. Albeit this seemingly doubles the number of constructor declarations in the draft, it does not change theeffective 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 thepiecewise_construct_t ofpair. This proposal does not propose this, because this constructor is specifically asked for by the corresponding tag and there are no furtherconstraint except theis_constructible requirements.

In addition, this proposal fixes the specification problem oftuple<>'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 emptytuple 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++03explicit constructors had no behavioural difference, unless they had been single-argument constructors, soone might suggest to restrict adding theexplicit 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 ofpair). Consider the following example:

#include <tuple>#include <chrono>#include <iostream>using hms_t = std::tuple<std::chrono::hours, std::chrono::minutes, std::chrono::seconds>;void launch_rocket_at(std::chrono::seconds s){  std::cout << "launching rocket in " << s.count() << " seconds!\n";}void launch_rocket_at(hms_t times){  using namespace std;  launch_rocket_at(get<0>(times) + get<1>(times) + get<2>(times));}int main(){  using namespace std;launch_rocket_at(make_tuple(1, 2, 3)); // #1: very scarylaunch_rocket_at({1, 2, 3});           // #2: even scarier  using namespace std::chrono;launch_rocket_at(make_tuple(hours(1), minutes(2), seconds(3))); // #3: Perfectly clear!launch_rocket_at({hours(1), minutes(2), seconds(3)});           // #4: Also clear!launch_rocket_at(hms_t{1, 2, 3});                               // #5: And this, too}

which should output:

launching rocket in 3723 seconds!launching rocket in 3723 seconds!launching rocket in 3723 seconds!

If the former two calls to functionlaunch_rocket_at where possible, this would directly subvert theintended explicitness of thestd::duration constructor and would make using the time-utilitytypes much more unsafe. Why? Consider the following scenario:

If the client believed that the order of the units wasseconds, minutes, hours, and input 3, 2, 1, — intending 3 seconds, 2 minutes, and 1 hour — the rocket would launch in 10,921 seconds instead of the intended 3,723 seconds. This mistake can indeed easily happen, if you look again at the lines marked with#1 and#2.

Due to our intentionally conserved constraints to be explicit here we catch that mistake at compile-time, instead of having to shoot the rocket down...

Editorial Representation

During the write-up of this proposal I had the idea of replacing the prototype declaration pairs bya 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 constexpr tuple(UTypes&&...);

This form of representation still means thatEXPLICIT needs to be defined somewhereand somehow, but only once.

There was a strong preference during the Rapperswil 2014 LEWG discussion to use the "macro" instead of the individual declarations.

This paper does not use a previously suggested form ofEXPLICIT(see below) instead ofEXPLICIT, because there is nothing specific to explain below again (The different SFINAE conditions are not relevant at that point,because they appear as usual with the normal, detailed prototype specifications).

In any way, the final editorial decision is being handed over to the project editor.

Proposed resolution

The proposed wording changes refer toN4296.

  1. Add an additional new paragraph at the end of 17.5.2.2 [functions.within.classes] and move the current second paragraph to the end of the firstparagraph:

    -1- For the sake of exposition, Clauses 18 through 30 and Annex D do not describe copy/move constructors,assignment operators, or (non-virtual) destructors with the same apparent semantics as those that can begenerated by default (12.1, 12.4, 12.8).It is unspecified whether the implementation provides explicit definitions for such member function signatures, or for virtual destructors that can be generated by default.

    -2- It is unspecified whether the implementation provides explicit definitions for such member function signatures,or for virtual destructors that can be generated by default.

    -?- For the sake of exposition, the library clauses sometimes annotate constructors withEXPLICIT. Such a constructor is conditionally declared as either explicit or non-explicit (12.3.1 [class.conv.ctor]). [Note: This is typically implemented by declaring two such constructors, of which at most one participates in overload resolution —end note]

  2. Change 20.3.2 [pairs.pair], class templatepair synopsis, as indicated:

    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();EXPLICIT constexpr pair(const T1& x, const T2& y);    template<class U, class V>EXPLICIT constexpr pair(U&& x, V&& y);    template<class U, class V>EXPLICIT constexpr pair(const 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);  };}
  3. Change 20.3.2 [pairs.pair] around p5 as indicated:

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

    -5-Requires:is_copy_constructible<first_type>::value istrue andis_copy_constructible<second_type>::value istrue.

    -6-Effects: The constructor initializesfirst withx andsecond withy.

    -?-Remarks: This constructor shall not participate in overload resolution unlessis_copy_constructible<first_type>::value istrue andis_copy_constructible<second_type>::value istrue. The constructor is explicit if and only ifis_convertible<const first_type&, first_type>::value isfalse oris_convertible<const second_type&, second_type>::value isfalse.

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

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

    -7-Requires:is_constructible<first_type, U&&>::value istrue andis_constructible<second_type, V&&>::value istrue.

    -8-Effects: The constructor initializesfirst withstd::forward<U>(x) andsecond withstd::forward<V>(y).

    -9-Remarks:IfU is not implicitly convertible tofirst_type orV is not implicitly convertible tosecond_type this constructor shall not participate in overload resolution.This constructor shall not participate in overload resolution unlessis_constructible<first_type, U&&>::value istrue andis_constructible<second_type, V&&>::value istrue. The constructor is explicit if and only ifis_convertible<U&&, first_type>::value isfalse oris_convertible<V&&, second_type>::value isfalse.

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

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

    -10-Requires:is_constructible<first_type, const U&>::value istrue andis_constructible<second_type, const V&>::value istrue.

    -11-Effects:The constructor initializesInitializes members from the corresponding members of the argument.

    -12-Remarks: This constructor shall not participate in overload resolution unlessconst U& is implicitly convertible tofirst_type andconst V& is implicitly convertible tosecond_typeis_constructible<first_type, const U&>::value istrue andis_constructible<second_type, const V&>::value istrue. The constructor is explicit if and only ifis_convertible<const U&, first_type>::value isfalse oris_convertible<const V&, second_type>::value isfalse.

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

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

    -13-Requires:is_constructible<first_type, U&&>::value istrue andis_constructible<second_type, V&&>::value istrue.

    -14-Effects: The constructor initializesfirst withstd::forward<U>(p.first) andsecond withstd::forward<V>(p.second).

    -15-Remarks: This constructor shall not participate in overload resolution unlessU is implicitly convertible tofirst_type andV is implicitly convertible tosecond_typeis_constructible<first_type, U&&>::value istrue andis_constructible<second_type, V&&>::value istrue. The constructor is explicit if and only ifis_convertible<U&&, first_type>::value isfalse oris_convertible<V&&, second_type>::value isfalse.

  7. Change 20.4.2 [tuple.tuple], class templatetuple synopsis, as indicated. The intent is to declarethe set of"conditionally explicit" constructors and to fix themultiple default constructorproblem for empty tuples.

    namespace std {  template <class... Types>  class tuple {  public:    // 20.4.2.1, tupleconstruction    constexpr tuple();EXPLICIT constexprexplicit tuple(const Types&...);// only if sizeof...(Types) >= 1    template <class... UTypes>EXPLICIT constexprexplicit tuple(UTypes&&...);// only if sizeof...(Types) >= 1        tuple(const tuple&) = default;    tuple(tuple&&) = default;      template <class... UTypes>EXPLICIT constexpr tuple(const tuple<UTypes...>&);    template <class... UTypes>EXPLICIT constexpr tuple(tuple<UTypes...>&&);    template <class U1, class U2>EXPLICIT constexpr tuple(const pair<U1, U2>&);// only if sizeof...(Types) == 2    template <class U1, class U2>EXPLICIT constexpr tuple(pair<U1, U2>&&);// only if sizeof...(Types) == 2// allocator-extended constructors    template <class Alloc>      tuple(allocator_arg_t, const Alloc& a);    template <class Alloc>EXPLICIT tuple(allocator_arg_t, const Alloc& a, const Types&...);    template <class Alloc, class... UTypes>EXPLICIT 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>EXPLICIT tuple(allocator_arg_t, const Alloc& a, const tuple<UTypes...>&);    template <class Alloc, class... UTypes>EXPLICIT tuple(allocator_arg_t, const Alloc& a, tuple<UTypes...>&&);    template <class Alloc, class U1, class U2>EXPLICIT tuple(allocator_arg_t, const Alloc& a, const pair<U1, U2>&);    template <class Alloc, class U1, class U2>EXPLICIT tuple(allocator_arg_t, const Alloc& a, pair<U1, U2>&&);    [..]  };}
  8. Change 20.4.2.1 [tuple.cnstr] around p6 as indicated:

    EXPLICIT constexprexplicit tuple(const Types&...);

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

    -7-Effects:The constructor initializesInitializes each element with the value of the corresponding parameter.

    -?-Remarks: This constructor shall not participate in overload resolution unlesssizeof...(Types) >= 1 andis_copy_constructible<Ti>::value istrue for alli. The constructor is explicit if and only ifis_convertible<constTi&,Ti>::value isfalse for at least onei.

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

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

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

    -9-Effects:The constructor initializesInitializes the elements in the tuple with the corresponding value instd::forward<UTypes>(u).

    -10-Remarks: This constructor shall not participate in overload resolution unlesseach type inUTypes is implicitly convertible to its corresponding type inTypessizeof...(Types) >= 1 andis_constructible<Ti,Ui&&>::value istrue for alli. The constructor is explicit if and only ifis_convertible<Ui&&,Ti>::value isfalse for at least onei.

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

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

    -15-Requires:sizeof...(Types) == sizeof...(UTypes).is_constructible<Ti, constUi&>::value is true for alli.

    -16-Effects:The constructor initializesConstructs each element of*this with the corresponding element ofu.

    -17-Remarks: This constructor shall not participate in overload resolution unlessconstUi& is implicitly convertible toTi for alliis_constructible<Ti, constUi&>::value istrue for alli. The constructor is explicit if and only ifis_convertible<constUi&,Ti>::value isfalse for at least onei.

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

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

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

    -19-Effects: For alli,the constructor initializes theith element of*this withstd::forward<Ui>(get<i>(u)).

    -20-Remarks: This constructor shall not participate in overload resolution unlesseach type inUTypes is implicitly convertible to its corresponding type inTypesis_constructible<Ti,Ui&&>::value istrue for alli. The constructor is explicit if and only ifis_convertible<Ui&&,Ti>::value isfalse for at least onei.

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

    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 typeT0 inTypes andis_constructible<T1, const U2&>::value is true for the second typeT1 inTypes.

    -22-Effects:The constructor initializesConstructs the first element withu.first and the second element withu.second.

    -23-Remarks: This constructor shall not participate in overload resolution unlessconst U1& is implicitly convertible toT0 andconst U2& is implicitly convertible toT1is_constructible<T0, const U1&>::value istrue andis_constructible<T1, const U2&>::value istrue. The constructor is explicit if and only ifis_convertible<const U1&,T0>::value isfalse oris_convertible<const U2&,T1>::value isfalse.

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

    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 typeT0 inTypes andis_constructible<T1, U2&&>::value is true for the second typeT1 inTypes.

    -25-Effects:The constructor iInitializes the first element withstd::forward<U1>(u.first) and the second element withstd::forward<U2>(u.second).

    -26-Remarks: This constructor shall not participate in overload resolution unlessU1 is implicitly convertible toT0 andU2 is implicitly convertible toT1is_constructible<T0, U1&&>::value istrue andis_constructible<T1, U2&&>::value istrue. The constructor is explicit if and only ifis_convertible<U1&&,T0>::value isfalse oris_convertible<U2&&,T1>::value isfalse.

  14. Change 20.4.2.1 [tuple.cnstr] around p27 as indicated:

    template <class Alloc>  tuple(allocator_arg_t, const Alloc& a);template <class Alloc>EXPLICIT tuple(allocator_arg_t, const Alloc& a, const Types&...);template <class Alloc, class... UTypes>EXPLICIT 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>EXPLICIT tuple(allocator_arg_t, const Alloc& a, const tuple<UTypes...>&);template <class Alloc, class... UTypes>EXPLICIT tuple(allocator_arg_t, const Alloc& a, tuple<UTypes...>&&);template <class Alloc, class U1, class U2>EXPLICIT tuple(allocator_arg_t, const Alloc& a, const pair<U1, U2>&);template <class Alloc, class U1, class U2>EXPLICIT tuple(allocator_arg_t, const Alloc& a, pair<U1, U2>&&);

    -27-Requires:Alloc shall meet the requirements for anAllocator (17.6.3.5).

    -28-Effects: Equivalent to the preceding constructors except that each element is constructed with uses-allocatorconstruction (20.7.7.2).

Implementation Hint

The following example presents how to constrain even non-template functions such as the constructorsthat 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 forhis motivating example. Thanks also to Jonathan Wakely for his review that improved this proposal to a large extend.Thanks as well go to Mike Spertus for helping to improve the rationale.


[8]ページ先頭

©2009-2025 Movatter.jp