移动语义 vs 常量引用

7

我的类有字符串变量,我想用传递给构造函数的值来初始化它们。

我的老师教我们通过 const 引用传递字符串:

MyClass::MyClass(const std::string &title){
  this->title = title
}

然而 Clang-Tidy 建议使用移动指令:

MyClass::MyClass(std::string title){
  this->title = std::move(title)
}

所以我想知道现代C++中正确的做法是什么。

我已经查找了一些资料,但没有真正回答我的问题。先谢谢!


1
一般来说,是的,第二个版本是当前标准。它有时可以避免复制,而第二个版本总是会执行复制。 - Yksisarvinen
2
这个回答解决了你的问题吗?常量引用 VS 移动语义 - Jan Schultke
如果C++为std::string引入了COW技术,那么第一个选项就会有很好的理由。 - Mansoor
1
对于这种参数是 数据接收器 的情况,我会使用 MyClass::MyClass(std::string title_) : title{std::move(title_)} {} - Eljay
1
@Yksisarvinen 有点小问题:我认为应该称之为“最佳实践”,而不是“标准”,尤其是考虑到已经接受答案中的警告。 - Spencer
4个回答

7

使用成员初始化列表更好,因为它们默认构造title,然后再进行复制赋值移动赋值,没有任何优势。请避免使用None

MyClass::MyClass(const std::string& title) : title(title) {}         // #1
// or
MyClass::MyClass(std::string title) : title(std::move(title)) {}     // #2
//or
MyClass::MyClass(const std::string& title) : title(title) {}         // #3
MyClass::MyClass(std::string&& title) : title(std::move(title)) {}   // #3

让我们看看C++17中发生了什么:


#1 - 一个单一的转换构造函数,传入一个const&参数。

MyClass::MyClass(const std::string& title) : title(title) {}

这将以以下方式之一创建 1 或 2 个 std::string:

  • 成员被复制构造。
  • 通过std::string转换构造函数构造一个std::string,然后复制构造该成员。

#2 - 一个取std::string值的单一转换构造函数。

MyClass(std::string title) : title(std::move(title)) {}

这将以以下一种或两种方式创建1或2个 std::string

  • 实参通过从临时对象(str1 + str2)上的返回值优化构造而成,然后成员被移动构造。
  • 实参通过复制构造而成,然后成员被移动构造。
  • 实参通过移动构造而成,然后成员被移动构造。
  • 实参通过 std::string 转换构造函数构造而成,然后成员被移动构造。

#3 - 结合两个转换构造函数。

MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}

这将以以下方式之一创建1或2个std::string

  • 成员被复制构造。
  • 成员是移动构造。
  • 使用std::string转换构造函数构造一个std::string,然后移动构造成员。

到目前为止,选项#3似乎是最有效的选择。让我们再检查几个选项。


#4 - 类似于#3,但用转发构造函数替换移动转换构造函数。

MyClass(const std::string& title) : title(title) {}                       // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}  // B

这将始终以以下方式之一创建 1 个 std::string

  • 成员通过 A 进行复制构造。
  • 成员通过 B 进行移动构造。
  • 成员通过 Bstd::string 构造函数(可能转换)构造。

#5 - 仅限转发构造函数 - 从 #4 中删除复制转换构造函数。

template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}

这将始终创建1个std::string,就像在#4中一样,但全部通过转发构造函数完成。

  • 成员被复制构造。
  • 成员被移动构造。
  • 成员通过一个std::string(可能是转换)构造函数构造。

#6 - 单参数转发构造函数。

template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}

这将始终创建1个std::string,就像#4和#5一样,但只需要一个参数,并将其转发到std::string构造函数。

  • 该成员是复制构造的。
  • 该成员是移动构造的。
  • 该成员是通过std::string转换构造函数构造的。

如果您想在MyClass构造函数中使用多个参数,那么选项#6可以轻松用于完美转发。比如说,您有一个int成员和另一个std::string成员:

template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
    x(X),
    title(std::forward<T>(title)),
    title2(std::forward<U>(title2))
{}

太棒了。关于类似情况的后续问题:是否可以修改#4以便我可以传递多个字符串?如果我想传递多个字符串和一个整数怎么办? - Finn
@Finn 我已经为此添加了选项#6 - 而且我注意到我在选项#5中忘记了转发引用,所以我现在重写了它。 :) - Ted Lyngmo
嗯,我现在尝试了#6,CLion显示一切都正确,但是当我编译它时,我得到了一个错误。它找不到构造函数(未定义的引用)。我需要像#4中那样多个构造函数吗? - Finn
@Finn 示例 - 你没有尝试将成员函数模板的实现放在你的.cpp文件中,是吗?成员函数模板应该在.hpp文件中。 - Ted Lyngmo
1
是的,那正是我尝试过的...把所有东西都放在头文件中确实起作用了。谢谢你。 - Finn

1
复制引用会创建原始变量的副本(原始变量和新变量位于不同的区域),移动局部变量则将局部变量转换为rvalue(同样,原始变量和新变量位于不同的区域)。
从编译器的角度来看,move可能(并且确实)更快:
#include <string>

void MyClass(std::string title){
  std::string title2 = std::move(title);
}

翻译为:

MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
        sub     rsp, 40
        mov     rax, rdi
        lea     rcx, [rsp + 24]
        mov     qword ptr [rsp + 8], rcx
        mov     rdi, qword ptr [rdi]
        lea     rdx, [rax + 16]
        cmp     rdi, rdx
        je      .LBB0_1
        mov     qword ptr [rsp + 8], rdi
        mov     rsi, qword ptr [rax + 16]
        mov     qword ptr [rsp + 24], rsi
        jmp     .LBB0_3
.LBB0_1:
        movups  xmm0, xmmword ptr [rdi]
        movups  xmmword ptr [rcx], xmm0
        mov     rdi, rcx
.LBB0_3:
        mov     rsi, qword ptr [rax + 8]
        mov     qword ptr [rsp + 16], rsi
        mov     qword ptr [rax], rdx
        mov     qword ptr [rax + 8], 0
        mov     byte ptr [rax + 16], 0
        cmp     rdi, rcx
        je      .LBB0_5
        call    operator delete(void*)
.LBB0_5:
        add     rsp, 40
        ret

然而,
void MyClass(std::string& title){
  std::string title = title;
}

生成更大的代码(GCC类似):
MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
        push    r15
        push    r14
        push    rbx
        sub     rsp, 48
        lea     r15, [rsp + 32]
        mov     qword ptr [rsp + 16], r15
        mov     r14, qword ptr [rdi]
        mov     rbx, qword ptr [rdi + 8]
        test    r14, r14
        jne     .LBB0_2
        test    rbx, rbx
        jne     .LBB0_11
.LBB0_2:
        mov     qword ptr [rsp + 8], rbx
        mov     rax, r15
        cmp     rbx, 16
        jb      .LBB0_4
        lea     rdi, [rsp + 16]
        lea     rsi, [rsp + 8]
        xor     edx, edx
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long)
        mov     qword ptr [rsp + 16], rax
        mov     rcx, qword ptr [rsp + 8]
        mov     qword ptr [rsp + 32], rcx
.LBB0_4:
        test    rbx, rbx
        je      .LBB0_8
        cmp     rbx, 1
        jne     .LBB0_7
        mov     cl, byte ptr [r14]
        mov     byte ptr [rax], cl
        jmp     .LBB0_8
.LBB0_7:
        mov     rdi, rax
        mov     rsi, r14
        mov     rdx, rbx
        call    memcpy
.LBB0_8:
        mov     rax, qword ptr [rsp + 8]
        mov     qword ptr [rsp + 24], rax
        mov     rcx, qword ptr [rsp + 16]
        mov     byte ptr [rcx + rax], 0
        mov     rdi, qword ptr [rsp + 16]
        cmp     rdi, r15
        je      .LBB0_10
        call    operator delete(void*)
.LBB0_10:
        add     rsp, 48
        pop     rbx
        pop     r14
        pop     r15
        ret
.LBB0_11:
        mov     edi, offset .L.str
        call    std::__throw_logic_error(char const*)
.L.str:
        .asciz  "basic_string::_M_construct null not valid"

所以,在这种情况下,std::move更好。


0

有两种情况:std::stringlvaluervalue

std::string const& 版本中,lvalue 情况足够高效,通过引用传递然后复制。但是 rvalue 将被复制而不是移动,这样效率要低得多。

std::string 版本中,lvalue 在传递时会被复制,然后移动到成员中。在这种情况下,rvalue 将被移动两次。但通常它很便宜,因为是移动构造函数。

此外,在 std::string&& 版本中,它不能接收 lvalue,但是 rvalue 通过引用传递然后移动,比移动两次更好。

显然,使用const&&&是最佳实践,就像STL一直做的那样。但是如果移动构造函数足够便宜,只需通过值传递并移动也是可以接受的。


0

可以使用const引用,然后使用成员初始化列表:

MyClass(const std::string &title) : m_title{title}

其中m_title是类中的成员字符串。

您可以在这里找到有用的帮助:构造函数成员初始化列表


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