【问题标题】:Why is passing by value (if a copy is needed) recommended in C++11 if a const reference only costs a single copy as well?如果 const 引用也只需要一个副本,为什么在 C++11 中建议按值传递(如果需要副本)?
【发布时间】:2018-02-12 04:47:04
【问题描述】:

我正在尝试了解移动语义、右值引用、std::move 等。我一直在尝试通过搜索本网站上的各种问题来弄清楚为什么不推荐传递 const std::string &name + _name(name)如果需要副本,则不是 std::string name + _name(std::move(name))

如果我理解正确,以下需要一个副本(通过构造函数)加上一个移动(从临时到成员):

Dog::Dog(std::string name) : _name(std::move(name)) {}

另一种(也是老式的)方法是通过引用传递它并复制它(从引用到成员):

Dog::Dog(const std::string &name) : _name(name) {}

如果第一种方法需要复制和移动两者,而第二种方法只需要一个副本,那么如何首选第一种方法,并且在某些情况下更快?

【问题讨论】:

  • 考虑右值参数表达式与左值。但也可以考虑将参数传递到调用层次结构中,这是通常的情况。
  • @Barry Gotcha。因此,它本质上是在传入临时变量并且可能只是从中获取的情况下的优化。如果我理解正确,对于非临时左值,它不会有所作为。
  • @SilvioMayolo name 在您的示例中不是转发引用。
  • 当你按值传递时,你可能只需要两步而没有副本。使用 const 引用,它总是至少需要一个副本。

标签: c++ c++11 move-semantics


【解决方案1】:

消费数据时,你需要一个可以消费的对象。当您收到std::string const& 时,您必须复制对象,而与是否需要参数无关。

当对象按值传递时,如果必须复制对象,则将复制该对象,即,当传递的对象不是临时对象时。但是,如果它碰巧是临时对象,则可能会在适当的位置构造对象,即,任何副本都可能已被删除,您只需为移动构造付费。也就是说,有可能实际上没有复制发生。

【讨论】:

  • 还有第三种情况;如果传入的对象是右值引用(即 std move 的返回值)。然后按值执行 2 次移动和 0 次复制,而 const ref 执行 1 次复制。
  • 关于你的第一句话,我很困惑:你为什么要在任何情况下复制对象?传递引用的意义在于不必复制您所引用的对象。
  • @Alex:注意附带条件:“当消费 ...”,即当您使用对象初始化另一个对象时。在这种情况下,您需要一份副本,但您 [通常] 提前知道这是否会发生。什么都不做(由于复制省略)或创建一个可以移动的复制(当复制省略不能使用时)是无论如何需要复制时最有效的方法。
  • 啊,有道理,对我的误解深表歉意!
【解决方案2】:

考虑使用左值和右值调用各种选项:

  1. Dog::Dog(const std::string &name) : _name(name) {}
    

    无论是用左值还是右值调用,这都需要一个副本,从name 初始化_name。移动不是一种选择,因为nameconst

  2. Dog::Dog(std::string &&name) : _name(std::move(name)) {}
    

    这只能用右值调用,它会移动。

  3.  Dog::Dog(std::string name) : _name(std::move(name)) {}
    

    当使用左值调用时,这将复制以传递参数,然后移动以填充数据成员。当使用右值调用时,这将移动以传递参数,然后移动以填充数据成员。在右值的情况下,可能会省略传递参数的移动。因此,用左值调用它会导致一个副本和一次移动,而用右值调用它会导致一到两次移动。

最佳解决方案是同时定义(1)(2)。解决方案(3) 可以相对于最优值有额外的移动。但是编写一个函数比编写两个几乎相同的函数更短且更易于维护,并且假定移动很便宜。

当使用可隐式转换为字符串的值(如const char*)进行调用时,会发生隐式转换,其中涉及长度计算和字符串数据的副本。然后我们陷入右值情况。在这种情况下,使用 string_view 提供了另一种选择:

  1. Dog::Dog(std::string_view name) : _name(name) {}
    

    当使用字符串左值或右值调用时,这会产生一个副本。当使用const char* 调用时,会进行一次长度计算和一份副本。

【讨论】:

  • +1 您可以指出,推理不一定适用于 setter 函数或赋值运算符(实际上,它主要适用于构造函数)。见stackoverflow.com/questions/18303287/…
  • 您已经考虑过Dog 是用右值std::string 或左值std::string 构造的可能性,但另外有一种情况是它是用隐式可转换为的东西构造的std::string,即const char*。 (所以有其他答案,但你的答案最接近完整。)
  • 为什么在情况(4)When called with a string lvalue or rvalue, this results in one copy. ?将参数绑定到name时会发生一个副本,初始化_name时应该会发生另一个副本?谢谢
  • string_view 类似于引用。它不会复制它初始化的字符串。
  • 如果我们知道我们正在使用传入的值,那么只定义 r-value 接受版本是否合理,如果调用者具有 l-value,则强制调用者指定 std::move,所以未来的人们会看到“哦,这是一个动作,它正在被消耗”?
【解决方案3】:

先简短回答:通过 const& 调用总是要花费一份副本。 根据条件按价值调用可能只需要一步操作。但这取决于(请查看下面的代码示例以了解此表所指的场景):

            lvalue        rvalue      unused lvalue  unused rvalue
            ------------------------------------------------------
const&      copy          copy        -              -
rvalue&&    -             move        -              -
value       copy, move    move        copy           - 
T&&         copy          move        -              -
overload    copy          move        -              - 

所以我的执行摘要是,如果

  • 移动很便宜,因为可能会有额外的移动
  • 无条件使用该参数。如果不使用参数,按值调用也会花费一份副本,例如因为 if 子句或某事。

按值调用

考虑一个用于复制其参数的函数

class Dog {
public:
    void name_it(const std::string& newName) { names.push_back(newName); }
private:
    std::vector<std::string> names;
};

如果将左值传递给name_it,则在右值的情况下您也将进行两次复制操作。这很糟糕,因为右值可以让我移动。

一种可能的解决方案是为右值编写重载:

class Dog {
public:
    void name_it(const std::string& newName) { names.push_back(newName); }
    void name_it(std::string&& newName) { names.push_back(std::move(newName)); }
private:
    std::vector<std::string> names;
};

这解决了问题,一切都很好,尽管你有两个代码,两个函数的代码完全相同。

另一种可行的解决方案是使用完美转发,但这也有几个缺点,(例如,完美转发函数非常贪婪,并且使现有的重载 const& 函数无用,通常它们需要在头文件中,他们在目标代码中创建了几个函数等等。)

class Dog {
public:
    template<typename T>
    void name_it(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
    std::vector<std::string> names;
};

另一种解决方案是使用按值调用

class Dog {
public:
    void name_it(std::string newName) { names.push_back(std::move(newName)); }
private:
    std::vector<std::string> names;
};

重要的是,正如您提到的std::move。这样,您将拥有一个用于右值和左值的函数。您将移动右值,但接受左值的额外移动,这可能很好如果移动便宜并且无论条件如何都复制或移动参数。

所以最后我真的认为推荐一种方法而不是其他方法是完全错误的。这在很大程度上取决于。

#include <vector>
#include <iostream>
#include <utility>

using std::cout;

class foo{
public:
    //constructor
    foo()  {}
    foo(const foo&)  { cout << "\tcopy\n" ; }
    foo(foo&&)  { cout << "\tmove\n" ; }
};

class VDog {
public:
    VDog(foo name) : _name(std::move(name)) {}
private:
    foo _name;
};

class RRDog {
public:
    RRDog(foo&& name) : _name(std::move(name)) {}
private:
    foo _name;
};

class CRDog {
public:
    CRDog(const foo& name) : _name(name) {}
private:
    foo _name;
};

class PFDog {
public:
    template <typename T>
    PFDog(T&& name) : _name(std::forward<T>(name)) {}
private:
    foo _name;
};

//
volatile int s=0;

class Dog {
public:
    void name_it_cr(const foo& in_name) { names.push_back(in_name); }
    void name_it_rr(foo&& in_name)   { names.push_back(std::move(in_name));}
    
    void name_it_v(foo in_name) { names.push_back(std::move(in_name)); }
    template<typename T>
    void name_it_ur(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
    std::vector<foo> names;
};


int main()
{
    std::cout << "--- const& ---\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue:";
        a.name_it_cr(my_foo);
        std::cout << "rvalue:";
        b.name_it_cr(foo());
    }
    std::cout << "--- rvalue&& ---\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue: -\n";
        std::cout << "rvalue:";
        a.name_it_rr(foo());
    }
    std::cout << "--- value ---\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue:";
        a.name_it_v(my_foo);
        std::cout << "rvalue:";
        b.name_it_v(foo());
    }
    std::cout << "--- T&&--\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue:";
        a.name_it_ur(my_foo);
        std::cout << "rvalue:";
        b.name_it_ur(foo());
    }
    
    
    return 0;
}

输出:

--- const& ---
lvalue: copy
rvalue: copy
--- rvalue&& ---
lvalue: -
rvalue: move
--- value ---
lvalue: copy
    move
rvalue: move
--- T&&--
lvalue: copy
rvalue: move

【讨论】:

    【解决方案4】:

    除了性能原因之外,当副本在按值构造函数上引发异常时,它首先会在调用者身上引发,而不是在构造函数本身内引发。这使得编写 noexcept 构造函数更容易,并且不必担心资源泄漏或构造函数上的 try/catch 块。

    struct A {
        std::string a;
    
        A( ) = default;
        ~A( ) = default;
        A( A && ) noexcept = default;
        A &operator=( A && ) noexcept = default;
    
        A( A const &other ) : a{other.a} {
            throw 1;
        }
        A &operator=( A const &rhs ) {
            if( this != &rhs ) {
                a = rhs.a;
                throw 1;
            }
            return *this;
        }
    };
    
    struct B {
        A a;
    
        B( A value ) try : a { std::move( value ) }
        { std::cout << "B constructor\n"; }
        catch( ... ) {
            std::cerr << "Exception in B initializer\n";
        }
    };
    
    struct C {
        A a;
    
        C( A const &value ) try : a { value }
        { std::cout << "C constructor\n"; }
        catch( ... ) {
            std::cerr << "Exception in C initializer\n";
        }
    };
    
        int main( int, char ** ) {
    
        try {
            A a;
            B b{a};
        } catch(...) { std::cerr << "Exception outside B2\n"; }
    
    
    
        try {
            A a;
            C c{a};
        } catch(...) { std::cerr << "Exception outside C\n"; }
    
        return EXIT_SUCCESS;
    }
    

    会输出

    Exception outside B2
    Exception in C initializer
    Exception outside C
    

    【讨论】:

      【解决方案5】:

      我做了一个实验:

      #include <cstdio>
      #include <utility>
      
      struct Base {
        Base() { id++; }
        static int id;
      };
      
      int Base::id = 0;
      
      struct Copyable : public Base {
        Copyable() = default;
        Copyable(const Copyable &c) { printf("Copyable [%d] is copied\n", id); }
      };
      
      struct Movable : public Base {
        Movable() = default;
      
        Movable(Movable &&m) { printf("Movable [%d] is moved\n", id); }
      };
      
      struct CopyableAndMovable : public Base {
        CopyableAndMovable() = default;
      
        CopyableAndMovable(const CopyableAndMovable &c) {
          printf("CopyableAndMovable [%d] is copied\n", id);
        }
      
        CopyableAndMovable(CopyableAndMovable &&m) {
          printf("CopyableAndMovable [%d] is moved\n", id);
        }
      };
      
      struct TEST1 {
        TEST1() = default;
        TEST1(Copyable c) : q(std::move(c)) {}
        TEST1(Movable c) : w(std::move(c)) {}
        TEST1(CopyableAndMovable c) : e(std::move(c)) {}
      
        Copyable q;
        Movable w;
        CopyableAndMovable e;
      };
      
      struct TEST2 {
        TEST2() = default;
        TEST2(Copyable const &c) : q(c) {}
        //  TEST2(Movable const &c) : w(c)) {}
        TEST2(CopyableAndMovable const &c) : e(std::move(c)) {}
      
        Copyable q;
        Movable w;
        CopyableAndMovable e;
      };
      
      int main() {
        Copyable c1;
        Movable c2;
        CopyableAndMovable c3;
        printf("1\n");
        TEST1 z(c1);
        printf("2\n");
        TEST1 x(std::move(c2));
        printf("3\n");
        TEST1 y(c3);
      
        printf("4\n");
        TEST2 a(c1);
        printf("5\n");
        TEST2 s(c3);
      
        printf("DONE\n");
        return 0;
      }
      

      结果如下:

      1
      Copyable [4] is copied
      Copyable [5] is copied
      2
      Movable [8] is moved
      Movable [10] is moved
      3
      CopyableAndMovable [12] is copied
      CopyableAndMovable [15] is moved
      4
      Copyable [16] is copied
      5
      CopyableAndMovable [21] is copied
      DONE
      

      结论:

      template <typename T>
      Dog::Dog(const T &name) : _name(name) {} 
      // if T is only copyable, then it will be copied once
      // if T is only movable, it results in compilation error (conclusion: define separate move constructor)
      // if T is both copyable and movable, it results in one copy
      
      template <typename T>
      Dog::Dog(T name) : _name(std::move(name)) {}
      // if T is only copyable, then it results in 2 copies
      // if T is only movable, and you called Dog(std::move(name)), it results in 2 moves
      // if T is both copyable and movable, it results in one copy, then one move.
      

      【讨论】:

        猜你喜欢
        • 2019-12-08
        • 1970-01-01
        • 1970-01-01
        • 2023-03-26
        • 2016-10-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-27
        相关资源
        最近更新 更多