【问题标题】:Does C++11 change the behavior of explicitly calling std::swap to ensure ADL-located swap's are found, like boost::swap?C++11 是否会更改显式调用 std::swap 以确保找到位于 ADL 的交换的行为,例如 boost::swap?
【发布时间】:2012-02-28 12:37:48
【问题描述】:

背景

请考虑以下代码:

#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);
}

在 C++03 中,do_swap 的这种实现将被视为“损坏”:

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

通过显式指定std::,它禁止通过依赖于参数的查找找到ns::swap。 (然后编译失败,因为std::swap 试图复制foo,这是不允许的。)相反,我们这样做:

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
}

现在ns::swap 已找到,而std::swap 不那么专业,因此未使用。它更丑陋,但它有效并且事后看来是可以理解的。 boost::swap 为我们很好地包装了这个(并提供了数组重载):

#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
}

问题

因此我的问题是std::swap 在 C++11 中是否具有 boost::swap 的行为?如果不是,为什么?

在我看来,这似乎很明显。任何被更改破坏的代码一开始都可能非常脆弱(算法和容器,如std::sortstd::vector,未指定;允许实现调用 ADL 交换或不确定调用),因此更改将针对更好的。此外,std::swap 现在是为数组定义的,所以改变当然不是不可能的。

然而,虽然 §17.6.3.2 规定标准库中对 swap 的所有调用都必须在没有 std:: 限定的情况下完成(解决上述算法和容器的问题),但它未能触及 std::swap本身。它甚至给出了交换值的例子,包括using std::swap;。同样,第 20.2.2 节(其中指定了 std::swap)在 ADL 上只字未提。

最后,GCC 没有在他们的std::swap 实现中启用 ADL(MSVC 也没有,但这没什么好说的)。所以我一定是错的std::swap 采取boost::swap 的行为,但我不明白为什么没有做出改变。 :(And I'm not alone!

【问题讨论】:

  • 我还记得一直听说C++11的std::swap应该使用ADL,奇怪。
  • 如果要交换的项目在命名空间中,这里有什么依赖于 koenig 查找规则吗?当然,您始终可以为任何特定类型重载 swap,因为 swap 是一个模板,因此您的重载将始终优先于通用模板,例如 void swap(MyT&a, MyT&b) { a.swap(b); }
  • @CashCow:您的意思是专注于std 命名空间?这在某些时候有效(不能部分专门化函数,所以没有模板类),但它很笨拙并且不利于良好的设计。
  • Howard Hinnant 下面的回答给出了一个很好的反例。但是,请注意 Eric Niebler(至少)已经提议 std::swap(或类似的东西)应该按照你想要的方式行事。请参阅 "Customization Point Design in C++11 and Beyond"(2014 年 10 月的博文)和 N4381 Suggested Design for Customization Points,它们于 2015 年 5 月在 Lenexa 上进行了讨论。

标签: c++ c++11 swap language-lawyer rationale


【解决方案1】:

如果您提出了概念验证实施方案,我将不得不投票反对。我担心它会破坏以下代码,我很确定在过去的十几年中我至少在野外看到过一次或两次。

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);
    }

}

无论您认为上面的代码是好是坏,它都按照作者在 C++98/03 中的意图工作,因此静默破坏它的门槛相当高。告诉用户在 C++11 中他们不再需要编写 using std::swap; 并不足以抵消将上述代码静默转换为无限递归的缺点。

另一种摆脱写作using std::swap;的方法是改用std::iter_swap

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

【讨论】:

    【解决方案2】:

    在 C++20 中,这终于被标准化了:

    std::swap(a, b);
    

    这使用 ADL 调用正确的重载并施加正确的要求以在 SFINAE 中使用。魔法在[namespace.std]/7中指定:

    除了在命名空间std 中或在命名空间内的命名空间中 std,程序可以为任何库函数提供重载 指定为自定义点的模板,前提是 (a) 重载的声明取决于至少一种用户定义的类型,并且 (b) 重载满足标准库的要求 174 [ 注意: 这允许 (合格或不合格)调用自定义点调用 给定参数的最合适的重载。 — end 注意 ]

    174) 任何库自定义点都必须准备好 与任何满足用户定义的过载充分工作 本文件的最低要求。因此一个实现可能 根据 as-if 规则 ([intro.execution]) 选择提供任何 实例化函数对象形式的定制点 ([function.objects]) 即使自定义点的 规范采用功能模板的形式。模板 每个这样的函数对象的参数和函数参数 并且对象的operator() 的返回类型必须与 对应定制点的规格。

    (强调我的)

    swap[utility.swap]中被指定为自定义点:

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

    备注: 此函数为指定自定义点 ([namespace.std]),不参与重载解析 除非 is_­move_­constructible_­v&lt;T&gt;true 并且 is_­move_­assignable_­v&lt;T&gt;true。里面的表情 noexcept 相当于:

    is_nothrow_move_constructible_v<T> && is_nothrow_move_assignable_v<T>
    

    要求: 类型 T 应为 Cpp17MoveConstructible(表 26)和 Cpp17MoveAssignable(表 28)。

    效果:交换存储在两个位置的值。

    (强调我的)

    【讨论】:

    • 那么这如何与霍华德答案中的示例代码交互?
    • @aschepler 默默地将上面的代码变成无限递归 :(
    【解决方案3】:

    这是一个概念验证实现:

    #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);
    }
    

    【讨论】:

    • (请注意,即使仅在 C++ 中无法实现,标准库实现也可以使用编译器魔法,包括在 std::swap 的情况下手动更改 ADL 的规则。)
    • 阴影的好主意,+1。现在把它转发给委员会。 :)
    • 您可能想提一下,这仅在std(或本例中的std_)中的每个类型都具有自定义swap 时才有效,因为阴影不适用于这些类型( std::swap(或 std_::swap 在这种情况下是通过 ADL 找到的)。但当然,这(或编译器魔法)将是一个可能的解决方案。
    • @Grizzly:我赞成你的评论,所以我会考虑提到它。 :)
    【解决方案4】:

    好吧,boost::swap() 发送到std::swap()。要让std::swap() 做类似于boost::swap() 的事情,它需要委托其他地方。这是别的什么地方?该标准不要求提供实际实现的swap() 的另一个版本。这可以做到,但标准没有强制要求。

    为什么它不这样做?我没有看到任何提议此实施的提案。如果有人希望这样做,我相信它会被提议。

    【讨论】:

    • 我不同意你所说的任何事情,但我也不太确定你在说什么。可以做到,但不是因为没有人提议吗?我没有看过这些提案,但如果没有提出来,我会感到非常惊讶。
    • 诚然,我只搜索了标题中带有“swap”的提案,我找到的唯一文档是 N2590(简化 swap 重载)、N3103(当前交换无法进行小字符串优化( ) 规范)和 N3179(I/O 流的移动和交换)。他们都没有谈到在std::swap() 的实现中使用ADL。惊喜...!我不认为它是被提出来的。你提出来了吗?
    • 不,我没有,我没有及时意识到标准化过程。好吧,如果它真的没有被提出来,那就太令人失望了。距离被提出的东西不到一箭之遥。
    猜你喜欢
    • 1970-01-01
    • 2015-03-23
    • 2020-12-12
    • 1970-01-01
    • 2012-12-22
    • 2018-05-03
    • 1970-01-01
    相关资源
    最近更新 更多