C++11最佳参数传递方式

8

考虑以下类:

#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,对于rvalues只需一次移动,对于lvalues只需一次复制。
有没有可能用一个构造函数实现同样的结果?在这种情况下不是大问题,但如果有多个参数呢?我将需要在参数列表中针对每个lvalue和rvalue出现的所有可能性进行组合。
这不仅限于构造函数,也适用于函数参数(例如设置器)。
注意:本问题严格与class B有关;class A仅存在于可视化复制/移动调用的方式。

你应该阅读这些内容:https://dev59.com/xF7Va4cB1Zd3GeqPHTLt 和 https://dev59.com/92445IYBdhLWcg3wmLZd - James Custer
@JamesCuster:我只是想测试每个构造函数/运算符被调用的次数。 - fscan
3个回答

9
“按值传递”方法是一种选择。虽然不如您所拥有的最优,但只需要一个重载即可实现:
class B
{
    A a;
public:   
    B (A _a) : a(move(_a)) {}
};

成本是对于 lvalues 和 xvalues 都多出 1 次构造,但对于 prvalues(1 次 move)仍然是最优的。"xvalue" 是一个已使用 std::move 转换为 rvalue 的 lvalue。
您还可以尝试 "完美转发" 解决方案:
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::value时会得到错误的答案。它将在没有对B进行适当约束的情况下错误地回答true。
我认为这些解决方案中没有一个总是比其他解决方案更好。在这里需要进行工程权衡。

好的,在这种情况下(传递、修改、按值返回),如果编译器在所有可能的地方执行复制/移动省略,那么2重载解决方案(以及完美转发解决方案)将花费lvalue(1个副本)、xvalue(1个移动)、prvalue(1个移动),而按值传递的解决方案将花费lvalue(1个副本+1个移动)、xvalue(2个移动)、prvalue(1个移动)。 - Howard Hinnant
@Mr.C64:使用static_assert不能防止std::is_constructible<B, their_type>::value错误地返回true - Howard Hinnant
@HowardHinnant:您能给一些例子吗? - Mr.C64
也许 Concepts 将会把这种风格带入时尚?template<class T> B (T&& _a) requires std::is_constructible_v<A, T> : a(std::forward<T>(_a)) {} 可以说是可读的。 - Toby Speight
那就是我通常对概念的看法:这种技术具有更好的语法。 - Howard Hinnant
显示剩余10条评论

2

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

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

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

如果A有多个构造函数,每个构造函数都有不同数量的参数,您可以通过在所有位置添加...来使整个过程变为可变长。

正如@Howard所说,您应该添加一个约束条件,以便该类看起来不能从实际上无法构建的参数中构建。


1
如果你的样本中的stringstd::string,那么就不需要关心:默认提供的复制和移动调用它们各自的成员。而且std::string已经实现了复制和移动,所以临时变量会被移动,变量会被复制。
没有必要定义特定的复制和移动构造函数和赋值函数。 你可以只使用构造函数。
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 的默认复制构造函数调用 A 的复制构造函数,B 的默认移动函数调用 A 的移动函数,所有 B 的默认赋值运算符都调用 A 定义的唯一一个(根据转发的内容是移动还是复制)。


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