在复制并交换惯用语中实现swap操作

4
根据什么是拷贝并交换惯用法如何为我的类提供交换函数,我尝试实现了交换函数,并使用了后一篇回答的第二个选项(使用调用成员函数的自由函数),而非前者的直接友元自由函数。
但是以下代码无法通过编译。
#include <iostream>

// Uncommenting the following two lines won't change the state of affairs
// class Bar;
// void swap(Bar &, Bar &);
class Bar {
public:
  Bar(unsigned int bottles=0) : bottles(bottles) { enforce(); } // (1)
  Bar(Bar const & b) : bottles(b.bottles) { enforce(); } // (1)

  Bar & operator=(Bar const & b) {
    // bottles = b.bottles;
    // enforce();
    // Copy and swap idiom (maybe overkill in this example)
    Bar tmp(b); // but apart from resource management it allows (1)
                // to enforce a constraint on the internal state
    swap(*this, tmp); // Can't see the swap non-member function (2)
    return *this;
  }

  void swap(Bar & that) {
    using std::swap;
    swap(bottles, that.bottles);
  }

  friend std::ostream & operator<<(std::ostream & out, Bar const & b) {
    out << b.bottles << " bottles";
    return out;
  }

private:
  unsigned int bottles;
  void enforce() { bottles /=2; bottles *= 2; } // (1) -- Ensure the number of bottles is even
};

void swap(Bar & man, Bar & woman) { // (2)
  man.swap(woman);
}

int main () {
  Bar man (5);
  Bar woman;

  std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
  swap(man, woman);
  std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

  return 0;
}

我知道复制和交换习惯用法在这里有些过度,但它也允许通过复制构造函数(1)对内部状态强制执行一些约束(一个更具体的例子是保持分数为最简形式)。不幸的是,这并不能编译,因为编译器只看到Bar::swap成员函数作为(2)的唯一候选项。我该使用友元非成员函数方法吗?
编辑:前往我的答案查看我最终采用的方案,感谢本问题中所有答案和评论。

你的代码完全没有遵循GMan回答中的成功解决方案 - Cody Gray
1
如果你正确实现了一个nothrow的移动赋值运算符和移动构造函数,那么就不需要实现swap。 - Richard Hodges
1
在类内部,为什么不直接调用 this->swap(other) - Yakk - Adam Nevraumont
1
如果要举一个好的例子来说明这个幻灯片(http://www.slideshare.net/ripplelabs/howard-hinnant-accu2014)上的第50页,那么这个例子就是。默认复制构造函数和复制赋值运算符,你将得到最优和正确的代码。你可以通过不提及它们来简单地实现这一点。编译器将隐式声明和定义最优代码。 - Howard Hinnant
@HowardHinnant,我喜欢这些幻灯片,谢谢你分享它们,可惜没有视频。 - green diod
5个回答

5

我理解我们已经使用了c++11?

如果是这样的话,只要我们正确实现了移动赋值运算符和移动构造函数(最好不抛出异常),std::swap的默认实现将是最优的。

http://en.cppreference.com/w/cpp/algorithm/swap

#include <iostream>

class Bar {
public:
    Bar(unsigned int bottles=0) : bottles(bottles) { enforce(); } // (1)
    Bar(Bar const & b) : bottles(b.bottles) {
        // b has already been enforced. is enforce necessary here?
        enforce();
    } // (1)
    Bar(Bar&& b) noexcept
    : bottles(std::move(b.bottles))
    {
        // no need to enforce() because b will have already been enforced;
    }

    Bar& operator=(Bar&& b) noexcept
    {
        auto tmp = std::move(b);
        swap(tmp);
        return *this;
    }

    Bar & operator=(Bar const & b)
    {
        Bar tmp(b); // but apart from resource management it allows (1)
        swap(tmp);
        return *this;
    }

    void swap(Bar & that) noexcept {
        using std::swap;
        swap(bottles, that.bottles);
    }

    friend std::ostream & operator<<(std::ostream & out, Bar const & b) {
        out << b.bottles << " bottles";
        return out;
    }

private:
    unsigned int bottles;
    void enforce() {  } // (1)
};

/* not needed anymore
void swap(Bar & man, Bar & woman) { // (2)
    man.swap(woman);
}
*/
int main () {
    Bar man (5);
    Bar woman;

    std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
    using std::swap;
    swap(man, woman);
    std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

    return 0;
}

预期结果:

Before -> m: 5 bottles / w: 0 bottles
After  -> m: 0 bottles / w: 5 bottles

编辑:

为了让关心性能的人(例如@JosephThompson)放心,让我来消除你们的疑虑。将调用std::swap移动到虚函数中(以强制clang生成任何代码),然后使用apple clang和-O2编译,得到以下结果:

void doit(Bar& l, Bar& r) override {
    std::swap(l, r);
}

变成了这样:

__ZN8swapper24doitER3BarS1_:            ## @_ZN8swapper24doitER3BarS1_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp85:
    .cfi_def_cfa_offset 16
Ltmp86:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp87:
    .cfi_def_cfa_register %rbp
    movl    (%rsi), %eax
    movl    (%rdx), %ecx
    movl    %ecx, (%rsi)
    movl    %eax, (%rdx)
    popq    %rbp
    retq
    .cfi_endproc 

看到了吗?最优的。C++标准库很棒!


是的,我使用gcc的-std=c++14来支持c++11。实际上,我对默认的operator=非常满意,但是我希望在类的内部状态上增加额外的约束执行。当然,我同意您关于某些特殊函数中enforce()的必要性(或不必要性)的评论。特别是在这个例子的背景下,瓶子很快就会飞到天上去! - green diod
然而,如果Bar继承自Foo,并且我有类似于Bar(Foo const&)的东西(如果这是不好的设计,请告诉我),也许我想确保仍然调用enforce(),例如Bar(Foo const& f):Foo(f){enforce();} - green diod
1
@greendiod 我说的是 movl 汇编指令,而不是 C++ 的移动操作。将此代码复制到 http://gcc.godbolt.org 并使用 -std=c++14 -O2(Clang 或 GCC)进行编译,然后检查 std_swapcustom_swap 的汇编代码。经过反思,显然在交换过程中没有调用 operator delete,但我的观点仍然是编译器未能生成“最优”代码。当然,我只是从技术角度进行论证 ;)。在进行过早优化之前,请进行基准测试。 - Joseph Thomson
1
也许我应该写“接近最优”的。 - Richard Hodges
1
@RichardHodges 哈哈,是的。我同意在绝大多数情况下,C++11后的std::swap已经足够好了。除非你正在编写高度优化的库代码,或者有基准测试来支持你,否则编写自定义的swap以及移动操作无疑是过早的优化。我只是试图在技术上保持正确:最好的正确方式 :) - Joseph Thomson
显示剩余13条评论

4

注意:这是使用复制和交换的C++11之前的方法。有关C++11解决方案,请参见此答案

为了使其正常工作,您需要修复一些问题。首先,您需要前向声明交换自由函数,以便operator=知道它。为了做到这一点,您还需要前向声明Bar,以便swap有一个名为bar的类型。

class Bar;

void swap(Bar & man, Bar & woman);

// rest of code

接下来我们需要告诉编译器在哪里查找 swap 函数。我们使用作用域解析运算符来实现这一点。这将告诉编译器在类的外部范围中查找 swap 函数。

Bar & operator=(Bar const & b) {
  // bottles = b.bottles;
  // enforce();
  // Copy and swap idiom (maybe overkill in this example)
  Bar tmp(b); // but apart from resource management it allows (1)
            // to enforce a constraint on the internal state
  ::swap(*this, tmp); // Can't see the swap non-member function (2)
//^^ scope operator 
  return *this;
}

我们将所有内容综合起来,得到了这个实时示例
然而,operator =的赋值符应该是这样的。
Bar & operator=(Bar b) // makes copy
{
    ::swap(*this, b) // swap the copy
    return *this; // return the new value
}

1
虽然这个答案是事后诸葛亮了,为什么不在成员函数中使用成员函数swap呢? - Richard Hodges
1
@RichardHodges,原帖似乎不想使用成员函数或友元函数。看起来他正在使用这个答案中的第二种选项。我不知道哪种方法实际上更好,或者是否可以说一种方法比另一种更好,所以我提出了这个解决方案来使其工作。 - NathanOliver
@NathanOliver 抱歉,我的意思是瓶子 *= 2,在这里有点人为,但 enforce() 在一个保持简化形式的分数类中将会是 reduce()。 - green diod
@NathanOliver 理解,但是实现ADL交换有点老派了。自从C++11以来,如果类具有可移动赋值和可移动构造,则不必要。见下方回答。 - Richard Hodges
1
@RichardHodges 我觉得我的编辑可以帮助澄清这一点。 - NathanOliver
显示剩余2条评论

1

您知道Bar有一个swap成员函数,因此直接调用它即可。

Bar& operator=(Bar const& b) {
    Bar tmp(b);
    tmp.swap(*this);
    return *this;
}

非成员函数 swap 只是为了让 Bar 的客户端能够利用其优化的 swap 实现,而不必知道它是否存在,使用 using std::swap 习惯用法来启用 参数依赖查找
using std::swap;
swap(a, b);

你的意思是在 Bar 类内部,我应该只使用 swap 成员函数,而对于 Bar 的客户端,我应该提供非成员的 swap 函数吗? - green diod
2
是的。在类内部调用非成员函数swap似乎有点毫无意义,因为它最终还是会调用成员函数swap。非成员函数swap只是为了支持使用using std::swap惯用法来启用ADL。这在您交换未知类型对象的通用代码中特别有用。 - Joseph Thomson

0

你还需要在那个函数中启用 std::swap

using std::swap;
swap(*this, tmp); // Can't see the swap non-member function (2)

引用您所提到的答案

如果 swap 如示例 1)中所示使用,则可以找到您的函数。

它的使用方式如下:

{
  using std::swap; // enable 'std::swap' to be found
                   // if no other 'swap' is found through ADL
  // some code ...
  swap(lhs, rhs); // unqualified call, uses ADL and finds a fitting 'swap'
                  // or falls back on 'std::swap'
  // more code ...
}

在 Coliru 上实时运行


0

对于上述情况,如果只需要强制执行一些内部约束条件,则最好使用默认值,并仅在直接初始化构造函数中强制执行一次约束条件。但是,如果您需要实现这些功能,请查看@RichardHodges的答案!还请参阅@HowardHinnant的评论(特别是有关编译器何时隐式声明特殊成员的幻灯片部分...)。

这是我最终得到的结果(不再有显式的复制和交换):

#include <iostream>

class Bar {
public:
  Bar(unsigned int bottles=0) : bottles(bottles) { enforce(); } // The only point of enforcement

  friend std::ostream & operator<<(std::ostream & out, Bar const & b) {
    out << b.bottles << " bottles";
    return out;
  }

private:
  unsigned int bottles;
  void enforce() { bottles /= 2; bottles *=2; }
};

int main () {
  Bar man (5);
  Bar woman;

  std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
  using std::swap; // Argument dependent lookup
  swap(man, woman);
  std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

  return 0;
}

现在,如果Bar继承自Foo(它不需要enforce),会发生什么。这是最初让我认为需要展开自己的特殊函数并从复制和交换惯用语的复制部分中获益以enforce约束的用例。事实证明,即使在这种情况下,我也不需要:
#include <iostream>

class Foo {
public:
  Foo(unsigned int bottles=11) : bottles(bottles) {} // This is odd on purpose

  virtual void display(std::ostream & out) const {
    out << bottles << " bottles";
  }

protected:
  unsigned int bottles;
};

std::ostream & operator<<(std::ostream & out, Foo const & f) {
  f.display(out);
  return out;
}

class Bar : public Foo {
public:
  Bar(unsigned int bottles=0) : Foo(bottles) { enforce(); }
  Bar(Foo const & f) : Foo(f) { enforce(); }

  void display(std::ostream & out) const override {
    out << bottles << " manageable bottles";
  }

private:
  void enforce() { bottles /= 2; bottles *=2; }
};

int main () {
  Bar man (5); // Again odd on purpose
  Bar woman;

  std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
  using std::swap; // Argument dependent lookup
  swap(man, woman);
  std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

  Foo fool(7); // Again odd
  Bar like(fool);
  std::cout << fool << " -> (copy) " << like << std::endl;
  Bar crazy;
  crazy = fool;
  std::cout << fool << " ->   (=)  " << crazy << std::endl;

  return 0;
}

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