Does C++11 change the behavior of explicitly calling std::swap to ensure ADL-located swap's are found, like boost::swap?

StackOverflow https://stackoverflow.com/questions/9170389

質問

Background

Consider for this question the following code:

#include <utility>

namespace ns
{
    struct foo
    {
        foo() : i(0) {}
        int i;
        
    private:
        foo(const foo&); // not defined,
        foo& operator=(const foo&); // non-copyable
    };
    
    void swap(foo& lhs, foo& rhs)
    {
        std::swap(lhs.i, rhs.i);
    }
}

template <typename T>
void do_swap(T& lhs, T& rhs); // implementation to be determined

int main()
{
    ns::foo a, b;
    do_swap(a, b);
}

In C++03, this implementation of do_swap would be considered "broken":

template <typename T>
void do_swap(T& lhs, T& rhs)
{
    std::swap(lhs, rhs);
}

By explicitly specifying std::, it prohibits ns::swap from being found via argument-dependent lookup. (It then fails to compile because std::swap tries to copy a foo, which is not allowed.) Instead, we do this:

template <typename T>
void do_swap(T& lhs, T& rhs)
{
    using std::swap; // allow std::swap as a backup if ADL fails to find a swap
    swap(lhs, rhs); // unqualified call to swap, allow ADL to operate
}

Now ns::swap is found and std::swap, being less specialized, is not used. It's uglier, but it works and is understandable in hind-sight. boost::swap wraps this up nicely for us (and provides array overloads):

#include <boost/swap.hpp>

template <typename T>
void do_swap(T& lhs, T& rhs)
{
    boost::swap(lhs, rhs); // internally does what do_swap did above
}

Question

My question is thus: does std::swap take on the behavior of boost::swap in C++11? If not, why?

To me it seems obvious that it ought to. Any code broken by the change was probably quite flimsy in the first place (algorithms and containers, like std::sort and std::vector, were underspecified; implementations were allowed to call ADL swap's or not indeterminately), so the change would be for the better. Additionally, std::swap is now defined for arrays, so change at all certainly isn't out of the question.

However, while §17.6.3.2 specifies that all calls to swap within the standard library must be done without std:: qualification (fixing the problem with algorithms and containers noted above), it fails to touch on std::swap itself. It even gives examples of swapping values that include using std::swap;. Likewise §20.2.2 (where std::swap is specified) doesn't say a word on ADL.

Lastly, GCC does not enable ADL in their std::swap implementation (nor does MSVC, but that's not saying much). So I must be wrong that std::swap takes on the behavior of boost::swap, but I don't understand why the change wasn't made. :( And I'm not alone!

役に立ちましたか?

解決

I would have had to vote against your proof-of-concept implementation had it been proposed. I fear it would break the following code, which I'm pretty sure I've seen in the wild at least once or twice over the past dozen years.

namespace oops
{

    struct foo
    {
        foo() : i(0) {}
        int i;

        void swap(foo& x) {std::swap(*this, x);}
    };

    void swap(foo& lhs, foo& rhs)
    {
        lhs.swap(rhs);
    }

}

Whether you think the above is good code or bad, it works as the author intends in C++98/03 and so the bar for silently breaking it is pretty high. Telling users that in C++11 they would no longer have to write using std::swap; isn't a sufficiently high benefit to outweigh the disadvantage of silently turning the above code into infinite recursion.

Another way to get out of writing using std::swap; is to use std::iter_swap instead:

template <typename T>
void do_swap(T& lhs, T& rhs)
{
    std::iter_swap(&lhs, &rhs); // internally does what do_swap did above
}

他のヒント

In C++20, this is finally standardized:

std::swap(a, b);

This uses ADL to call the correct overload and imposes the correct requirements to use in SFINAE. The magic is specified in [namespace.std]/7:

Other than in namespace std or in a namespace within namespace std, a program may provide an overload for any library function template designated as a customization point, provided that (a) the overload's declaration depends on at least one user-defined type and (b) the overload meets the standard library requirements for the customization point.174 [ Note: This permits a (qualified or unqualified) call to the customization point to invoke the most appropriate overload for the given arguments. — end note ]

174) Any library customization point must be prepared to work adequately with any user-defined overload that meets the minimum requirements of this document. Therefore an implementation may elect, under the as-if rule ([intro.execution]), to provide any customization point in the form of an instantiated function object ([function.objects]) even though the customization point's specification is in the form of a function template. The template parameters of each such function object and the function parameters and return type of the object's operator() must match those of the corresponding customization point's specification.

(emphasis mine)

And swap is designated as a customization point in [utility.swap]:

template<class T>
  constexpr void swap(T& a, T& b) noexcept(see below);

Remarks: This function is a designated customization point ([namespace.std]) and shall not participate in overload resolution unless is_­move_­constructible_­v<T> is true and is_­move_­assignable_­v<T> is true. The expression inside noexcept is equivalent to:

is_nothrow_move_constructible_v<T> && is_nothrow_move_assignable_v<T>

Requires: Type T shall be Cpp17MoveConstructible (Table 26) and Cpp17MoveAssignable (Table 28).

Effects: Exchanges values stored in two locations.

(emphasis mine)

Here's a proof-of-concept implementation:

#include <utility>

// exposition implementation
namespace std_
{
    namespace detail
    {
        // actual fallback implementation
        template <typename T>
        void swap(T& lhs, T& rhs)
        {
            T temp = std::move(lhs);
            lhs = std::move(rhs);
            rhs = std::move(temp);
        }
    }

    template <typename T>
    void swap(T& lhs, T& rhs)
    {
        using detail::swap; // shadows std_::swap, stops recursion
        swap(lhs, rhs); // unqualified call, allows ADL
    }
}

namespace ns
{
    struct foo
    {
        foo() : i(0) {}
        int i;

    private:
        foo(const foo&); // not defined,
        foo& operator=(const foo&); // non-copyable
    };

    void swap(foo& lhs, foo& rhs)
    {
        std::swap(lhs.i, rhs.i);
    }
}


int main()
{
    int i = 0, j = 0;
    std_::swap(i, j);

    ns::foo a, b;
    std_::swap(a, b);
}

Well, boost::swap() dispatches to std::swap(). To have std::swap() do something similar to boost::swap() it would need to delegate somewhere else. What is this somewhere else? The standard doesn't mandate another version of swap() which provides the actual implementation. This can be done but the standard doesn't mandate it.

Why it doesn't do it? I didn't see any proposal proposing this implementation. If someone had wanted this to do done I'm sure it would have been proposed.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top