Семантика перемещения и ссылка на константу [дубликат]
в моем классе есть строковые переменные, и я хочу инициализировать их значениями, переданными конструктору.
Мой учитель думал, что мы передаем строки как константную ссылку:
MyClass::MyClass(const std::string &title){
this->title = title
}
Однако Clang-Tidy предлагает использовать команду перемещения:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
Поэтому мне интересно, как правильно это сделать в современном C ++.
Я уже осмотрелся, но ничего толком не ответил на мой вопрос. Заранее спасибо!
Ответы
Ни один из них не является оптимальным, поскольку они оба title
сначала создают конструкцию по умолчанию, а затем копируют, назначают или перемещают, присваивают . Используйте список инициализаторов членов.
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
секунды одним из следующих способов:
- Член построен по копии.
- A создается
std::string
конструкторомstd::string
преобразования, а затем создается копия элемента.
# 2 - Единственный конструктор преобразования, принимающий std::string
по значению.
MyClass(std::string title) : title(std::move(title)) {}
Это создаст 1 или 2 std::string
секунды одним из следующих способов:
- Аргумент создается путем оптимизации возвращаемого значения из временного (
str1
+str2
), а затем создается элемент move. - Аргумент создается копией, а затем создается элемент move.
- Аргумент строится, а затем строится член.
- Аргумент создается
std::string
конструктором преобразования, а затем создается элемент move.
№3 - Объединение двух конвертирующих конструкторов.
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
Это создаст 1 или 2 std::string
секунды одним из следующих способов:
- Член построен по копии.
- Член построен ходом.
- A создается
std::string
конструкторомstd::string
преобразования, а затем создается элемент move.
Пока что вариант #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
. - Член создается конструктором
std::string
(возможно, конвертирующим) черезB
.
# 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
конвертирующим конструктором.
Option #6
можно легко использовать для идеальной пересылки, если вы хотите использовать несколько аргументов в MyClass
конструкторе. Допустим, у вас есть 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))
{}
Копирование ссылки создает копию исходной переменной (исходная и новая находятся в разных областях), перемещение локальной переменной приводит к 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
лучше (при данных обстоятельствах).
Можно использовать константную ссылку, а затем использовать списки инициализаторов членов:
MyClass(const std::string &title) : m_title{title}
Где m_title - это ваша строка-член в классе.
Вы можете найти полезную помощь здесь: Списки инициализаторов членов конструктора
есть 2 случая: lvalue или rvalue of std::string
.
в std::string const&
версии, lvalue case достаточно эффективен, передается по ссылке, а затем копируется . но rvalue будет скопировано, а не перемещено , что имеет гораздо меньшую эффективность.
в std::string
версии, именующий будет скопирован при передаче , а затем перешли к члену. В этом случае rvalue будет перемещен дважды . но вообще дешевый конструктор ходов.
кроме того, в std::string&&
версии, он не может получить именующее выражение , но Rvalue это передается по ссылке , а затем переехал , лучше , чем переехал в два раза.
так что очевидно, что это лучшая практика с обоими const&
и &&
, как всегда и с STL. но если конструктор перемещения достаточно дешевый, также допустима передача по значению и перемещение.