2062. Effect contradictions w/o no-throw guarantee of std::function swaps

Section: 22.10.17.3 [func.wrap.func], 22.10.17.3.3 [func.wrap.func.mod] Status: C++17 Submitter: Daniel Krügler Opened: 2011-05-28 Last modified: 2020-09-06

Priority: 2

View all other issues in [func.wrap.func].

View all issues with C++17 status.

Discussion:

Howard Hinnant observed in reflector message c++std-lib-30841 that 22.10.17.3 [func.wrap.func] makes the member swap noexcept, even though the non-member swap is not noexcept.

The latter was an outcome of the discussions during the Batavia meeting and the Madrid meeting involving LWG 1349, which seems to indicate that the remaining noexcept specifier at the member swap is incorrect and should be removed.

But if we allow for a potentially throwing member swap of std::function, this causes another conflict with the exception specification for the following member function:

template<class F> function& operator=(reference_wrapper<F> f) noexcept;

Effects: function(f).swap(*this);

Note that in this example the sub-expression function(f) does not cause any problems, because of the nothrow-guarantee given in 22.10.17.3.2 [func.wrap.func.con] p. 10. The problem is located in the usage of the swap which could potentially throw given the general latitude.

So, either the Madrid meeting decision need to be revised (and both member and free swap of std::function should be noexcept), or this function needs to be adapted as well, e.g. by taking the exception-specification away or by changing the semantics.

One argument for "swap-may-throw" would be to allow for small-object optimization techniques where the copy of the target may throw. But given the fact that the swap function has been guaranteed to be "Throws: Nothing" from TR1 on, it seems to me that that there would still be opportunities to perform small-object optimizations just restricted to the set of target copies that cannot throw.

In my opinion member swap of std::function has always been intended to be no-throw, because otherwise there would be no good technical reason to specify the effects of several member functions in terms of the "construct-swap" idiom (There are three functions that are defined this way), which provides the strong exception safety in this case. I suggest to enforce that both member swap and non-member swap of std::function are nothrow functions as it had been guaranteed since TR1 on.

[ 2011 Bloomington ]

Dietmar: May not be swappable in the first place.

Alisdair: This is wide contact. Then we should be taking noexcept off instead of putting it on. This is preferred resolution.

Pablo: This is bigger issue. Specification of assignment in terms of swap is suspect to begin with. It is over specification. How this was applied to string is a better example to work from.

Pablo: Two problems: inconsistency that should be fixed (neither should have noexcept), the other issues is that assignment should not be specified in terms of swap. There are cases where assignment should succeed where swap would fail. This is easier with string as it should follow container rules.

Action Item (Alisdair): There are a few more issues found to file.

Dave: This is because of allocators? The allocator makes this not work.

Howard: There is a type erased allocator in shared_ptr. There is a noexcept allocator in shared_ptr.

Pablo: shared_ptr is a different case. There are shared semantics and the allocator does move around. A function does not have shared semantics.

Alisdair: Function objects think they have unique ownership.

Howard: In function we specify semantics with copy construction and swap.

Action Item (Pablo): Write this up better (why assignment should not be defined in terms of swap)

Howard: Not having trouble making function constructor no throw.

Dietmar: Function must allocate memory.

Howard: Does not put stuff that will throw on copy or swap in small object optimization. Put those on heap. Storing allocator, but has to be no throw copy constructable.

Pablo: Are you allowed to or required to swap or move allocators in case or swap or move.

Dave: An allocator that is type erased should be different...

Pablo: it is

Dave: Do you need to know something about allocator types? But only at construction time.

Pablo: You could have allocators that are different types.

Dave: Swap is two ended operation.

Pablo: Opinion is that both have to say propagate on swap for them to swap.

John: It is not arbitrary. If one person says no. No is no.

Howard: Find noexcept swap to be very useful. Would like to move in that direction and bring container design along.

Dave: If you have something were allocator must not propagate you can detect that at construction time.

...

Pablo: Need to leave this open and discuss in smaller group.

Alisdair: Tried to add boost::any as TR2 proposal and ran into this issue. Only the first place where we run into issues with type erased allocators. Suggest we move it to open.

Action Item: Move to open.

Action Item (Pablo works with Howard and Daniel): Address the more fundamental issue (which may be multiple issues) and write up findings.

Previous resolution [SUPERSEDED]:

This wording is relative to the FDIS.

  1. Modify the header <functional> synopsis in 22.10 [function.objects] as indicated:

    namespace std {
      […]
    
      template<class R, class... ArgTypes>
      void swap(function<R(ArgTypes...)>&, function<R(ArgTypes...)>&) noexcept;
    
      […]
    }
    
  2. Modify the class template function synopsis in 22.10.17.3 [func.wrap.func] as indicated:

    namespace std {
      […]
    
      // [func.wrap.func.alg], specialized algorithms:
      template<class R, class... ArgTypes>
      void swap(function<R(ArgTypes...)>&, function<R(ArgTypes...)>&) noexcept;
    
      […]
    }
    
  3. Modify 22.10.17.3.8 [func.wrap.func.alg] as indicated:

    template<class R, class... ArgTypes>
    void swap(function<R(ArgTypes...)>& f1, function<R(ArgTypes...)>& f2) noexcept;
    

    -1- Effects: f1.swap(f2);

[2014-02-28 (Post Issaquah), Pablo provides more information]

For cross-referencing purposes: The resolution of this issue should be harmonized with any resolution to LWG 2370, which addresses inappropriate noexcepts in some function constructors.

We have the following choices:

  1. swap() does not throw

    Discussion: This definition is desirable, and allows assignment to be implemented with the strong exception guarantee, but it does have consequences: The implementation cannot use the small-object optimization for a function-object F unless F is NothrowMovable (nothrow-swappable is unimportant because F is not swapped with another F). Note that many functors written before C++11 will not have move constructors decorated with noexcept, so this limitation could affect a lot of code.

    It is not clear what other implementation restrictions might be needed. Allocators are required not to throw on move or copy. Is that sufficient?

  2. swap() can throw

    Discussion: This definition gives maximum latitude to implementation to use small-object optimization. However, the strong guarantee on assignment is difficult to achieve. Should we consider giving up on the strong guarantee? How much are we willing to pessimize code for exceptions?

  3. swap() will not throw if both functions have NoThrowMoveable functors

    Discussion: This definition is similar to option 2, but gives slightly stronger guarantees. Here, swap() can throw, but the programmer can theoretically prevent that from happening. This should be straight-forward to implement and gives the implementation a lot of latitude for optimization. However, because this is a dynamic decision, the program is not as easy to reason about. Also, the strong guarantee for assignment is compromized as in option 2.

[2016-08-02, Ville, Billy, and Billy comment and reinstantiate the original P/R]

We (Ville, Billy, and Billy) propose to require that function's swap is noexcept in all cases.

Moreover, many of the concerns that were raised by providing this guarantee are no longer applicable now that P0302 has been accepted, which removes allocator support from std::function.

Therefore we are re-proposing the original resolution above.

[2016-08 Chicago]

Tues PM: Move to Tentatively Ready

Proposed resolution:

This wording is relative to N4606.

  1. Modify the header <functional> synopsis in 22.10 [function.objects] as indicated:

    namespace std {
      […]
    
      template<class R, class... ArgTypes>
      void swap(function<R(ArgTypes...)>&, function<R(ArgTypes...)>&) noexcept;
    
      […]
    }
    
  2. Modify the class template function synopsis in 22.10.17.3 [func.wrap.func] as indicated:

    namespace std {
      […]
    
      // [func.wrap.func.alg], specialized algorithms:
      template<class R, class... ArgTypes>
      void swap(function<R(ArgTypes...)>&, function<R(ArgTypes...)>&) noexcept;
    
      […]
    }
    
  3. Modify 22.10.17.3.8 [func.wrap.func.alg] as indicated:

    template<class R, class... ArgTypes>
    void swap(function<R(ArgTypes...)>& f1, function<R(ArgTypes...)>& f2) noexcept;
    

    -1- Effects: As if by: f1.swap(f2);