【问题标题】:c++11 optimal parameter passingc++11最优参数传递
【发布时间】:2012-05-06 16:28:35
【问题描述】:

考虑这些类:

#include <iostream>
#include <string>

class A
{
    std::string test;
public:
    A (std::string t) : test(std::move(t)) {}
    A (const A & other) { *this = other; }
    A (A && other) { *this = std::move(other); }

    A & operator = (const A & other)
    {
        std::cerr<<"copying A"<<std::endl;
        test = other.test;
        return *this;
    }

    A & operator = (A && other)
    {
        std::cerr<<"move A"<<std::endl;
        test = other.test;
        return *this;
    }
};

class B
{
    A a;
public:   
    B (A && a) : a(std::move(a)) {}
    B (A const & a) : a(a) {}
};

在创建B 时,我总是为A 提供一个最佳的正向路径,一个用于右值的移动或一个用于左值的副本。

是否可以用一个构造函数实现相同的结果?这种情况下问题不大,但是多参数呢?我需要参数列表中每个可能出现的左值和右值的组合。

这不仅限于构造函数,也适用于函数参数(例如setter)。

注意:这个问题完全是关于class Bclass A 的存在只是为了可视化复制/移动调用的执行方式。

【问题讨论】:

标签: c++ c++11 parameter-passing rvalue-reference


【解决方案1】:

“按价值”方法是一种选择。它不像你所拥有的那样最优,但只需要一个重载:

class B
{
    A a;
public:   
    B (A _a) : a(move(_a)) {}
};

左值和 xvalues 的成本都是 1 次额外的移动构造,但这对于纯右值(1 次移动)仍然是最佳的。 “xvalue”是使用 std::move 转换为右值的左值。

您也可以尝试“完美转发”解决方案:

class B
{
    A a;
public:   
    template <class T,
              class = typename std::enable_if
              <
                 std::is_constructible<A, T>::value
              >::type>
    B (T&& _a) : a(std::forward<T>(_a)) {}
};

这将使您回到最佳数量的复制/移动构造。但是您应该限制模板构造函数,使其不会过于通用。您可能更喜欢使用 is_convertible 而不是 is_constructible ,就像我在上面所做的那样。这也是一个单一的构造函数解决方案,但是随着您添加参数,您的约束变得越来越复杂。

注意:上面的约束之所以是必要的,是因为如果没有,B 的客户在查询std::is_constructible&lt;B, their_type&gt;::value 时会得到错误的答案。如果对 B 没有适当的约束,它会错误地回答 true。

我想说,这些解决方案中没有一个总是比其他解决方案更好。这里需要进行工程权衡。

【讨论】:

  • 关于按值:在我的测试中,我对真正的右值也有额外的移动(vs2010)。
  • 有趣。谢谢你的信息。我无权访问 vs2010。
  • 好的,这很好,但是如果将代码应用于每个构造函数/设置器,会有点不可读。有没有一种情况我不希望构造函数/设置器的这种行为。如果没有,如果我按值声明,编译器不能默认执行此操作吗? (如果不再使用左值,也会自动转换为 xrvalue,就像在这个构造函数中一样)
  • 已经列出了三个基本的解决方案。它们各有优缺点。我目前不知道有任何其他解决方案可以与这三者竞争。
  • 如果B 是一个聚合,编译器可以默认做正确的事情,这意味着(除其他外)B::apublicB 没有用户声明的构造函数。然后你可以构造一个BB b{ A() };,这将把构造B::a 从临时移动,或者你可以做B b{ a2 };,它将从左值a2 复制构造B::a。但是一旦你有了用户声明的构造函数,你就需要说明它们的参数类型是什么,并明确说明你对这些参数做了什么。
【解决方案2】:

B的构造函数使用推导的参数类型:

template <typename T> explicit B(T && x) : a(std::forward<T>(x) { }

这适用于可构造 A 对象的任何参数。

如果A 有多个具有不同数量参数的构造函数,您可以通过在任何地方添加... 来使整个事物可变。

不过,正如@Howard 所说,您应该添加一个约束,以使该类看起来不能从实际上不是的参数构造。

【讨论】:

    【解决方案3】:

    如果您的示例中的stringstd::string,则不必在意:默认提供的复制和移动调用它们各自的in 成员。并且std::string 实现了复制和移动,因此临时变量被移动,变量被复制。

    不需要定义具体的拷贝和移动ctor和assign。 你可以离开构造函数

    A::A(string s) :test(std::move(s)) {}
    

    一般来说,复制和移动的简单实现如下

    class A
    {
    public:
        A() :p() {}
    
        A(const A& a) :p(new data(*a.p)) {} //copy
        A(A&& a) :p(a.p) { a.p=0; }         //move
    
        A& operator=(A a) //note: pass by value
        { clear(); swap(a); return *this; }
        ~A() { clear(); }
    
        void swap(A& a) { std::swap(p,a.p); }
        void clear() { delete p; p=0; }
    
    private:
    
        data* p;
    };
    

    operator= 采用内部移动的值。如果它来自一个临时的被移动,如果它来自一个变量被复制。 复制和移动之间的区别需要不同的构造函数,但是,如果我们将 A 派生为

    class B: public A
    {
    ...
    };
    

    没有必要覆盖任何东西,因为 B 的默认 copy-ctor 调用 A 的副本,B 的默认移动调用 A 的移动,并且 B 的所有默认分配运算符调用唯一定义的对于 A(根据转发的内容移动或复制)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-03-29
      • 2016-04-21
      • 1970-01-01
      • 2010-09-21
      • 2014-06-27
      • 1970-01-01
      • 1970-01-01
      • 2013-01-17
      相关资源
      最近更新 更多