【问题标题】:Is it okay to define a totally general swap() function?可以定义一个完全通用的 swap() 函数吗?
【发布时间】:2016-08-19 10:21:10
【问题描述】:

下面的sn-p:

#include <memory>
#include <utility>

namespace foo
{
    template <typename T>
    void swap(T& a, T& b)
    {
        T tmp = std::move(a);
        a = std::move(b);
        b = std::move(tmp);
    }

    struct bar { };
}

void baz()
{
    std::unique_ptr<foo::bar> ptr;
    ptr.reset();
}

不为我编译:

$ g++ -std=c++11 -c foo.cpp
In file included from /usr/include/c++/5.3.0/memory:81:0,
                 from foo.cpp:1:
/usr/include/c++/5.3.0/bits/unique_ptr.h: In instantiation of ‘void std::unique_ptr<_Tp, _Dp>::reset(std::unique_ptr<_Tp, _Dp>::pointer) [with _Tp = foo::bar; _Dp = std::default_delete<foo::bar>; std::unique_ptr<_Tp, _Dp>::pointer = foo::bar*]’:
foo.cpp:20:15:   required from here
/usr/include/c++/5.3.0/bits/unique_ptr.h:342:6: error: call of overloaded ‘swap(foo::bar*&, foo::bar*&)’ is ambiguous
  swap(std::get<0>(_M_t), __p);
      ^
In file included from /usr/include/c++/5.3.0/bits/stl_pair.h:59:0,
                 from /usr/include/c++/5.3.0/bits/stl_algobase.h:64,
                 from /usr/include/c++/5.3.0/memory:62,
                 from foo.cpp:1:
/usr/include/c++/5.3.0/bits/move.h:176:5: note: candidate: void std::swap(_Tp&, _Tp&) [with _Tp = foo::bar*]
     swap(_Tp& __a, _Tp& __b)
     ^
foo.cpp:7:10: note: candidate: void foo::swap(T&, T&) [with T = foo::bar*]
     void swap(T& a, T& b)

我声明一个swap() 函数如此笼统以至于与std::swap 冲突,这是我的错吗?

如果是这样,有没有办法定义foo::swap(),这样它就不会被 Koenig 查找拖入?

【问题讨论】:

  • 不能在 GCC 或 Clang 上编译,但可以在 MSVC 2015 上编译。也许是另一个未记录的特性。
  • 该死,在不到第一个小时的时间内,这些都是一些不错的比率。 +16 票,观看 77 次,收藏 4 次。
  • 你应该没有理由在一个只包含特定类型的非常特定的命名空间中定义这样一个通用的swap 模板。只需为foo::bar 定义一个非模板swap 重载。将常规交换留给std::swap,只提供特定的重载。
  • @TemplateRex foo 是我们在标准库上使用的间接层。我们支持的许多平台都有不完整/有缺陷的标准库,在这些平台上,我们将直接将foo::shared_ptr 发送到boost::shared_ptr 而不是std::shared_ptr。我们的一个平台有一个旧的std::swap(),它可以复制而不是移动,所以我们在foo 中提供了一个替代品。
  • @TavianBarnes 我已经更新了我的答案,以回应您的两位 cmets。

标签: c++ c++11 gcc libstdc++ argument-dependent-lookup


【解决方案1】:
  • unique_ptr&lt;T&gt; 要求 T*NullablePointer [unique.ptr]p3
  • NullablePointer 要求 T* 的左值是 Swappable [nullablepointer.requirements]p1
  • Swappable 本质上需要using std::swap; swap(x, y);x 选择重载,yT* 类型的左值[swappable.requirements]p3

在最后一步中,您的类型 foo::bar 会产生歧义,因此违反了 unique_ptr 的要求。 libstdc++ 的实现是符合标准的,尽管我会说这相当令人惊讶。


措辞当然有点复杂,因为它是通用的。

[unique.ptr]p3

如果remove_reference_t&lt;D&gt;::pointer类型存在, 那么unique_ptr&lt;T, D&gt;::pointer 应该是同义词 remove_reference_t&lt;D&gt;::pointer。否则unique_ptr<T, D>::pointer 应为T* 的同义词。 unique_ptr<T, D>::pointer类型应满足NullablePointer的要求。

(强调我的)

[nullablepointer.requirements]p1

NullablePointer 类型是类指针类型,支持 null 价值观。一个类型P满足NullablePointer的要求如果:

  • [...]
  • P 类型的左值是可交换的 (17.6.3.2),
  • [...]

[swappable.requirements]p2

当且仅当:

  • 表达式swap(t, u)swap(u, t) 在下述上下文中计算时有效,并且
  • [...]

[swappable.requirements]p3

评估swap(t, u)swap(u, t) 的上下文应 确保通过选择名为“swap”的二进制非成员函数 候选集上的重载决议,包括:

  • &lt;utility&gt;和中定义的两个swap函数模板
  • 由参数相关查找生成的查找集。

请注意,对于指针类型 T*,出于 ADL 的目的,关联的命名空间和类派生自类型 T。因此,foo::bar*foo 作为关联的命名空间。 swap(x, y) 的 ADL,其中 xyfoo::bar*,因此将找到 foo::swap

【讨论】:

  • 你可以找到一个更简单的细分here 那些不会说律师的人。无论如何,虽然 libstdc++ 肯定遵循这里的规则,但似乎没有任何要求它在重置中使用 ADL 交换,考虑到 libcxx 的删除器交换在功能上是等效的。我会将此归结为实施质量。
  • @user6253369 这里是 OP 的 QoI。只是不要提供复制 STL 模板自定义点的通用模板,尤其是不要为此类 STL 模板提供自己的类。
  • @user6253369 对于实际定义自定义交换的花哨指针,使用它们可能会更有效。
【解决方案2】:

问题在于 libstdc++ 对unique_ptr 的实现。这是来自他们的 4.9.2 分支:

https://gcc.gnu.org/onlinedocs/gcc-4.9.2/libstdc++/api/a01298_source.html#l00339

  338       void
  339       reset(pointer __p = pointer()) noexcept
  340       {
  341     using std::swap;
  342     swap(std::get<0>(_M_t), __p);
  343     if (__p != pointer())
  344       get_deleter()(__p);
  345       }

如您所见,有一个不合格的交换调用。现在让我们看看 libcxx (libc++) 的实现:

https://git.io/vKzhF

_LIBCPP_INLINE_VISIBILITY void reset(pointer __p = pointer()) _NOEXCEPT
{
    pointer __tmp = __ptr_.first();
    __ptr_.first() = __p;
    if (__tmp)
        __ptr_.second()(__tmp);
}

_LIBCPP_INLINE_VISIBILITY void swap(unique_ptr& __u) _NOEXCEPT
    {__ptr_.swap(__u.__ptr_);}

他们不会在reset 内部调用swap,也不会使用不合格的交换调用。


Dyp's answer 提供了关于为什么libstdc++ 符合标准以及为什么当标准库需要调用swap 时您的代码会中断的非常可靠的细分。引用TemplateRex

你应该没有理由在 一个非常具体的命名空间,只包含特定的类型。只需定义 foo::bar 的非模板 swap 重载。离开一般交换 到std::swap,并且只提供特定的重载。 source

例如,这不会编译:

std::vector<foo::bar> v;
std::vector<foo::bar>().swap(v);

如果您的目标是使用旧标准库/GCC(如 CentOS)的平台,我建议您使用 Boost 而不是重新发明轮子以避免这样的陷阱。

【讨论】:

  • “我建议使用 Boost 而不是重新发明轮子” 同意!我们在foo 中为很多事情做这件事。不幸的是boost::swap() 不做移动分配,而只是委托给std::swap
  • @TavianBarnes 这可能是真的,但我相信 Boost 等价的 unique_ptr 例如使用移动语义进行交换。
  • 对,但我经常想做swap(a, b);,其中ab 是不可复制的类型。为我定义的每个只移动类型声明一个重载是很痛苦的,所以我需要一个可以移动的swap() 实现。
  • @TavianBarnes 我通常不会给出可疑的建议,但是您是否考虑过在共享库级别替换交换?我相信这在嵌入式系统中很常见。
  • @user6253369 我将您到 GitHub 的链接直接更新为提交,并通过 GitHub 的链接缩短器运行它。如果是错误的部分,请随时恢复编辑。
【解决方案3】:

此技术可用于避免 foo::swap() 被 ADL 发现:

namespace foo
{
    namespace adl_barrier
    {
        template <typename T>
        void swap(T& a, T& b)
        {
            T tmp = std::move(a);
            a = std::move(b);
            b = std::move(tmp);
        }
    }

    using namespace adl_barrier;
}

这就是 Boost.Range 的独立 begin()/end() 函数的定义方式。我在问问题之前尝试了类似的方法,但改为using adl_barrier::swap;,这不起作用。

至于问题中的 sn-p 是否应该按原样工作,我不确定。我可以看到的一个复杂情况是unique_ptr 可以具有来自Deleter 的自定义pointer 类型,应该与通常的using std::swap; swap(a, b); 习惯用法交换。对于问题中的foo::bar*,该成语显然被打破了。

【讨论】:

  • 如果它是“完全通用的”,那么为什么不使用 using 声明将 std::swap 导入到 foo 中?
  • @dyp foo::swap() 是因为我们的一个平台有一个标准库,而 std::swap() 有缺陷(复制而不是移动)。除了在那个平台上,我可能会做using std::swap;
  • 顺便说一句,我认为 ADL 保护你的类型(即foo::bar)而不是你的函数(即foo::swap)更好,因为那些是导致悲伤的原因,以及namespace foo 中的函数是开放式的(有人可能会添加 foo::beginfoo::end ,然后你就会遇到新的麻烦,受 ADL 保护的 foo::bar 可以防止这种情况发生)。
猜你喜欢
  • 2019-11-17
  • 2014-08-23
  • 1970-01-01
  • 1970-01-01
  • 2015-01-08
  • 2017-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多