公共友元交换成员函数

214

在对拷贝并交换惯用法的精美回答中,有一段代码我需要一点帮助:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

他添加了一条注释。

还有其他声称我们应该为自己的类型专门化 std::swap,提供一个类内交换函数和一个自由函数交换等说法。但这都是不必要的:任何正确使用 swap 的方法都将通过未限定调用进行,并且我们的函数将通过 ADL 被找到。只需要一个函数。

我对 friend 有点“不友好”,我必须承认。所以,我的主要问题是:

  • 看起来像一个自由函数,但它在类体内?
  • 为什么这个 swap 不是静态的?显然它没有使用任何成员变量。
  • “任何正确使用 swap 的方法都将通过 ADL 找到 swap”?ADL 会搜索命名空间,对吧?但它是否还会查找类内部?或者这就是 friend 的作用所在?

次要问题:

  • 在 C++11 中,我应该使用 noexcept 标记我的 swap 吗?
  • 在 C++11 和其 范围 for 循环中,我应该像在类内部那样放置 friend iter begin()friend iter end() 吗?我认为这里不需要 friend,对吗?

考虑到关于基于范围的for循环的附加问题:最好编写成员函数并将范围访问留给std命名空间中的begin()和end()(§24.6.5),基于范围的for循环在内部使用来自全局或std命名空间的这些函数(参见§6.5.4)。然而,它的缺点是这些函数是<iterator>头文件的一部分,如果您没有包含它,您可能需要自己编写它们。 - Vitus
9
为什么不是静态的 - 因为 friend 函数根本不是成员函数。 - aschepler
2个回答

215

有几种编写swap函数的方法,其中一些比其他方法更好。然而,随着时间的推移,发现单个定义效果最佳。让我们考虑如何思考编写swap函数。


首先我们可以看到像 std::vector<> 这样的容器有一个单参数成员函数 swap,例如:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

那么,我们的类也应该有一个成员函数 swap 吧?实际上并不需要。标准库有各种不必要的东西,其中一个就是成员函数 swap。为什么呢?接下来我们来了解一下。


我们应该确定什么是规范的,以及我们的类需要做些什么才能与之配合。交换的规范方法是使用std::swap。这就是为什么成员函数没有用处的原因:它们通常不是交换事物的方式,并且对std::swap的行为没有影响。
那么,为了让std::swap起作用,我们应该提供一个std::swap的特化实现(std::vector<> 应该提供),对吗?
namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

那在这种情况下确实可以解决问题,但它存在一个明显的问题:函数特化不能是部分的。也就是说,我们不能使用此方法来专门化模板类,只能专门化特定的实例:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

这种方法有时候有效,但并非总是如此。一定有更好的方法。


有!我们可以使用一个friend函数,并通过ADL找到它:

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

当我们想要交换某些东西时,我们会关联std::swap,然后进行未限定调用:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

什么是友元函数?这个领域存在一些混淆。

在C++标准化之前,友元函数执行一种称为“友元名称注入”的操作,使得代码表现出好像该函数已在周围的命名空间中编写一样。例如,在标准化之前,以下代码是等效的:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

然而,当ADL被发明后,这个问题就被解决了。现在friend函数只能通过ADL找到;如果你想要它作为一个自由函数,那么它需要被声明为这样(例如这里)。但是,有一个问题。
如果你只使用std::swap(x, y),你的重载将永远不会被找到,因为你已经明确地说“只在std中查找,其他地方都不用找”!这就是为什么有些人建议编写两个函数的原因:一个作为通过ADL找到的函数,另一个用来处理显式的std::限定符。
但是,正如我们所看到的,这种方法并不能在所有情况下都奏效,最终导致代码变得混乱难懂。相反,惯用的交换方式采用了另一种方法:不是让类来提供std::swap,而是让使用者来确保他们不使用限定的swap,就像上面那样。只要人们知道这一点,这种方法通常都能很好地工作。但问题在于:需要使用未限定的调用方式很不直观!
为了让这更容易,一些库(如Boost)提供了函数boost::swap,它只是调用了未限定的swap函数,并将std::swap作为相关命名空间。这有助于再次使事情简洁,但仍然不太理想。
请注意,在C++11中,std::swap的行为没有改变,我和其他人错误地认为会发生变化。如果您受到此影响,请在此处阅读
简而言之:成员函数只是噪音,特化不美观且不完整,但友元函数完整且有效。当你交换时,要么使用boost::swap,要么使用未限定的swap与关联的std::swap

非正式地,如果在函数调用期间考虑到它,则名称被视为关联。有关详细信息,请阅读§3.4.2。在这种情况下,std::swap 通常不会被考虑; 但是我们可以关联它(将其添加到未限定的 swap 考虑的重载集中),从而使其被找到。


13
我不同意成员函数只是无用的说法。成员函数使得例如std::vector<std::string>().swap(someVecWithData);成为可能,而使用swap自由函数则不行,因为两个参数都是通过非const引用传递的。请注意,我的翻译不改变原始内容的含义,同时尽可能地使其更加通俗易懂。 - ildjarn
3
你可以用两行代码来实现。拥有成员函数违反了DRY原则。 - GManNickG
5
DRY原则不适用于一种实现是基于另一种实现的情况。否则,没有人会倡导在类中实现operator=operator+operator+=,但显然这些运算符对相关类的存在被接受/期望是为了保持对称性。我认为成员swap和命名空间范围内的swap也是同样如此。 - ildjarn
3
我认为它考虑了太多功能。鲜为人知的是,即使 function<void(A*)> f; if(!f) { } 也可能失败,只因为 A 声明了一个接受 f 同样有效的 operator!(虽然不太可能,但仍有可能发生)。如果 function<> 的作者想到“哦,我有一个'operator bool',为什么要实现'operator!'呢?那会违反DRY原则!”这将是致命的。你只需要为 A 实现一个 operator!,并且 A 有一个可以构造 function<...> 的构造函数,事情就会出问题,因为两个候选对象都需要用户定义的转换。 - Johannes Schaub - litb
1
让我们考虑一下如何编写一个成员交换函数。自然而然地,我们的类也应该有这个功能,对吧?其实并不是这样。标准库有各种不必要的东西,其中成员交换就是其中之一。链接的GotW主张使用成员交换函数。 - Xeverous
显示剩余22条评论

8

那段代码在几乎每个方面上都等同于:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

在类中定义的友元函数:
  • 位于封闭命名空间中
  • 自动inline
  • 能够引用类的静态成员而无需进一步限定
具体规则在第节中(我引用了C++0x草案的第6和7段):

如果且仅当类是非本地类(9.8),函数名称未限定,函数具有命名空间作用域时,函数可以在类的友元声明中定义。

这样的函数是隐式内联的。在类中定义的友元函数处于其定义所在的类的(词法)范围内。在类外定义的友元函数不在其中。


4
在标准C++中,友元函数实际上并没有被放置在封闭的命名空间中。旧的行为称为“友元名称注入”,但已被自第一个标准以来的ADL取代。请参见this的开头。(尽管行为非常相似。) - GManNickG
2
并不完全等价。问题中的代码使得swap只对ADL可见。它是封闭命名空间的成员,但其名称对于其他名称查找形式不可见。编辑:我看到@GMan又快了:) @Ben,在ISO C++中一直都是这样的 :) - Johannes Schaub - litb
2
@Ben:不,友元注入从未在标准中存在过,但在之前被广泛使用,所以这个想法(和编译器的支持)继续传承下来,但从技术上讲,并不存在。friend函数只能通过ADL进行查找,如果它们需要具有friend访问权限的自由函数,它们需要在类内部声明为friend,并在类外部作为普通自由函数进行声明。例如,你可以在这个回答中看到这种必要性。 - GManNickG
2
@towi:因为友元函数位于命名空间作用域中,所以你的三个问题的答案都应该变得清晰明了:(1)它是一个自由函数,可以访问类的私有和保护成员。(2)它既不是实例成员也不是静态成员。(3)ADL不会在类内部搜索,但这没关系,因为友元函数具有命名空间作用域。 - Ben Voigt
1
@Ben。在规范中,该函数是命名空间成员,并且短语“函数具有命名空间作用域”可以解释为该函数是命名空间成员(这基本上取决于此类语句的上下文)。它向该命名空间添加一个名称,该名称仅对ADL可见(实际上,我IRC某些部分与规范中的其他部分相矛盾,关于是否添加任何名称。但是需要添加名称以检测添加到该命名空间的不兼容声明,因此实际上添加了一个不可见的名称。请参见3.3.1p4处的注释)。 - Johannes Schaub - litb
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接