2153. Narrowing of the non-member swap contract

Section: 23.2.2 [utility.swap], 20.5.3.2 [swappable.requirements], 26.2.1 [container.requirements.general] Status: LEWG Submitter: Robert Shearer Opened: 2012-04-13 Last modified: 2016-08-06

Priority: 2

View other active issues in [utility.swap].

View all other issues in [utility.swap].

View all issues with LEWG status.

Discussion:

Sub-clause 23.2.2 [utility.swap] defines a non-member 'swap' function with defined behavior for all MoveConstructible and MoveAssignable types. It does not guarantee constant-time complexity or noexcept in general, however this definition does render all objects of MoveConstructible and MoveAssignable type swappable (by the unary definition of sub-clause 20.5.3.2 [swappable.requirements]) in the absence of specializations or overloads.

The overload of the non-member swap function defined in Table 96, however, defines semantics incompatible with the generic non-member swap function, since it is defined to call a member swap function whose semantics are undefined for some values of MoveConstructible and MoveAssignable types.

The obvious (perhaps naive) interpretation of sub-clause 20.5.3.2 [swappable.requirements] is as a guide to the "right" semantics to provide for a non-member swap function (called in the context defined by 20.5.3.2 [swappable.requirements] p3) in order to provide interoperable user-defined types for generic programming. The standard container types don't follow these guidelines.

More generally, the design in the standard represents a classic example of "contract narrowing". It is entirely reasonable for the contract of a particular swap overload to provide more guarantees, such as constant-time execution and noexcept, than are provided by the swap that is provided for any MoveConstructible and MoveAssignable types, but it is not reasonable for such an overload to fail to live up to the guarantees it provides for general types when it is applied to more specific types. Such an overload or specialization in generic programming is akin to an override of an inherited virtual function in OO programming: violating a superclass contract in a subclass may be legal from the point of view of the language, but it is poor design and can easily lead to errors. While we cannot prevent user code from providing overloads that violate the more general swap contract, we can avoid doing so within the library itself.

My proposed resolution is to draw a sharp distinction between member swap functions, which provide optimal performance but idiosyncratic contracts, and non-member swap functions, which should always fulfill at least the contract of 23.2.2 [utility.swap] and thus render objects swappable. The member swap for containers with non-propagating allocators, for example, would offer constant-time guarantees and noexcept but would only offer defined behavior for values with allocators that compare equal; non-member swap would test allocator equality and then dispatch to either member swap or std::swap depending on the result, providing defined behavior for all values (and rendering the type "swappable"), but offering neither the constant-time nor the noexcept guarantees.

[2013-03-15 Issues Teleconference]

Moved to Open.

This topic deserves more attention than can be given in the telocon, and there is no proposed resolution.

[2013-03-15 Issues Teleconference]

Moved to Open.

This topic deserves more attention than can be given in the telocon, and there is no proposed resolution.

[2016-03 Jacksonville]

Alisdair says that his paper P0178 addresses this.

[2016-08 Chicago]

Send to LEWG

[2016-06 Oulu]

P0178 reviewed, and sent back to LEWG for confirmation.

Proposed resolution:

Apply P0178.