公共朋友交换成员功能


169

在对复制和交换惯用语的漂亮回答中,有一段代码我需要一些帮助:

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,在提供免费交换功能的同时提供类内交换,等等。但这都是不必要的:对交换的任何正确使用都将通过不合格的调用,我们的功能将通过ADL找到。一种功能会起作用。

随着friend我对“不友好”的条款一点,我必须承认。因此,我的主要问题是:

  • 看起来像一个自由函数,但是它在类体内?
  • 为什么这不是swap静态的?它显然不使用任何成员变量。
  • “任何正确使用交换的方法都会通过ADL找出交换”?ADL将搜索名称空间,对吗?但是它也可以在类内部查看吗?还是在这里friend进来?

附带问题:

  • 随着C ++ 11,我要记住我的swaps的noexcept
  • 随着C ++ 11和它的范围,对,我应该将friend iter begin()friend iter end()在类中以同样的方式?我认为friend这里不需要这个,对吗?

考虑到有关基于范围的问题:最好编写成员函数并将范围访问权保留在std名称空间(第24.6.5节)中的begin()和end()上,基于范围的内部使用这些范围来自global或std名称空间(请参见第6.5.4节)。但是,缺点是这些函数是<iterator>头文件的一部分,如果不包括它,则可能需要自己编写。
Vitus

2
为什么不是静态的-因为friend函数根本不是成员函数。
aschepler '16

Answers:


175

有几种写方法swap,有些比其他更好。但是,随着时间的流逝,发现单个定义最有效。让我们考虑一下如何考虑编写swap函数。


我们首先看到类似的容器std::vector<>具有单参数成员函数swap,例如:

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

那么,我们的班级自然也应该吧?好吧,不是真的。标准库包含各种不必要的东西,而成员swap就是其中之一。为什么?我们继续。


我们应该做的是确定什么是规范的,以及班级需要做什么才能使用它。交换的规范方法是std::swap。这就是为什么成员函数没用的原因:它们通常不是我们应该交换的东西,并且与的行为无关std::swap

那么,要进行std::swap工作,我们应该提供(并且std::vector<>应该已经提供)的专业化std::swap,对吗?

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

什么是friend功能?这个区域周围有混乱。

在C ++标准化之前,friend函数执行了一种称为“朋友名称注入”的操作,其中代码的行为就好像函数是在周围的名称空间中编写的一样。例如,这些是等效的预标准:

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

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

但是,在发明ADL时就将其删除。friend然后只能通过ADL找到该功能。如果您希望将其作为自由函数,则需要这样声明它(例如,参见this)。但是!出现问题了。

如果仅使用std::swap(x, y),将永远不会找到您的重载,因为您已经明确地说“向内看std,别无其他”!这就是为什么有人建议编写两个函数的原因:一个是通过ADL找到的函数,另一个是处理明确std::资格的函数。

但是就像我们看到的那样,这并非在所有情况下都有效,并且最终导致丑陋的混乱。取而代之的是,惯用的交换走了另一条路:不是使类成为要提供的类std::swap,而是由交换者来确保它们不使用qualified swap,如上。只要人们知道,这种方法就可以很好地工作。但是这里存在一个问题:需要使用不合格的调用是不直观的!

为了简化此过程,像Boost这样的某些库提供了function boost::swap,该函数只对进行了无限制的调用swap,并将其std::swap作为关联的命名空间。这有助于使事情再次变得简洁,但仍然令人沮丧。

请注意,C ++ 11中的行为没有变化std::swap,我和其他人错误地认为是这种情况。如果您对此有所了解,请在这里阅读


简而言之:成员函数仅仅是噪声,专业化是丑陋且不完整的,而friend函数却是完整且有效的。而当你交换,无论是使用boost::swap或不合格的swapstd::swap关联的。


†非正式地,如果在函数调用期间要考虑名称,则将其关联。有关详细信息,请参阅第3.4.2节。在这种情况下,std::swap通常不考虑;但是我们可以将其关联(将其添加到unqual认为的重载集合中swap),以便找到它。


10
我不同意成员函数只是噪音。成员函数允许例如std::vector<std::string>().swap(someVecWithData);,这对于swap自由函数是不可能的,因为这两个参数都是通过非常量引用传递的。
ildjarn 2011年

3
@ildjarn:您可以在两行上执行此操作。具有成员函数违反了DRY原理。
GManNickG 2011年

4
@GMan:如果一个以另一个术语实现,则DRY原理不适用。否则,没有人会主张与实施中的一个类operator=operator+operator+=预期的对称性存在,但显然对相关类的运营商正在接受/。我认为,成员swap+名称空间范围swap也是如此。
ildjarn 2011年

3
@GMan我认为它正在考虑过多的功能。鲜为人知,但是即使a function<void(A*)> f; if(!f) { }也会失败,只是因为A声明了一个与自己一样operator!接受的(不太可能,但是可能发生)。如果作者认为“哦,我有一个'operator bool',为什么我要实施'operator!'?那会违反DRY!”,那将是致命的。您只需要为实现,并为构造一个,就会出错,因为这两个候选都需要用户定义的转换。ffoperator!function<>operator!AAfunction<...>
Johannes Schaub-litb 2011年

1
让我们考虑一下如何考虑编写一个[member] swap函数。那么,我们的班级自然也应该吧?好吧,不是真的。标准库具有各种不必要的东西,成员交换就是其中之一。链接的GotW提倡成员交换功能。
Xeverous

7

该代码等效于(几乎在所有方面):

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
  • 无需进一步资格即可提及班级的静态成员

确切的规则在部分中[class.friend](我引用了C ++ 0x草案的第6和第7段):

当且仅当该类是非本地类(9.8),函数名称不合格且该函数具有名称空间范围时,才可以在该类的朋友声明中定义一个函数。

此类函数是隐式内联的。在类中定义的朋友功能在其定义的类的(词法)范围内。在类外部定义的朋友函数不是。


2
实际上,在标准C ++中,朋友功能未放置在封闭的名称空间中。以前的行为被称为“朋友名称注入”,但是被ADL取代,被第一个标准取代。见顶部。(尽管行为非常相似。)
GManNickG 2011年

1
并不完全等效。问题中的代码使其swap仅对ADL可见。它是封闭名称空间的成员,但是其名称对其他名称查找表单不可见。编辑:我看到@GMan再一次变得更快:) @Ben在ISO C ++中一直都是这样:)
Johannes Schaub-litb 2011年

2
@Ben:不,朋友注入从来没有在一个标准中存在过,但是在此之前它被广泛使用,这就是为什么这个想法(以及对编译器的支持)趋于继续的原因,但是从技术上讲并不存在。friend函数仅由ADL查找,并且如果它们仅需要是具有friend访问权限的自由函数,则既需要friend在类内声明它们,又需要在类外部声明它们是正常的自由函数。例如,您可以在此答案中看到这种必要性。
GManNickG 2011年

2
@towi:因为friend函数在名称空间范围内,所以对您所有三个问题的答案都应该清楚:(1)它是一个自由函数,并且可以通过朋友访问该类的私有成员和受保护成员。(2)它根本不是成员,既不是实例也不是静态的。(3)ADL不在类内搜索,但是可以这样做,因为friend函数具有名称空间范围。
Ben Voigt

1
@本 在规范中,函数是名称空间成员,短语“该函数具有名称空间范围”可以解释为该函数是名称空间成员(它在很大程度上取决于此类语句的上下文)。并且它向该名称空间添加了一个仅对ADL可见的名称(实际上,IIRC的某些部分与规范中的其他部分在是否添加任何名称方面存在矛盾。但是需要添加名称来检测添加到该名称中的不兼容声明)命名空间,因此实际上添加了一个不可见的名称请参阅3.3.1p4处的注释)。
Johannes Schaub-litb 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.