Переместить присвоение несовместимо со стандартным копированием и свопированием
Тестирование новой семантики Move.
Я просто спросил о проблемах, которые у меня были с Move Constructor. Но, как выясняется в комментариях, проблема в том, что оператор "Назначение перемещения" и оператор "Стандартное присваивание" сталкиваются при использовании стандартной идиомы "Копировать и своп".
Это класс, который я использую:
#include <string.h>
#include <utility>
class String
{
int len;
char* data;
public:
// Default constructor
// In Terms of C-String constructor
String()
: String("")
{}
// Normal constructor that takes a C-String
String(char const* cString)
: len(strlen(cString))
, data(new char[len+1]()) // Allocate and zero memory
{
memcpy(data, cString, len);
}
// Standard Rule of three
String(String const& cpy)
: len(cpy.len)
, data(new char[len+1]())
{
memcpy(data, cpy.data, len);
}
String& operator=(String rhs)
{
rhs.swap(*this);
return *this;
}
~String()
{
delete [] data;
}
// Standard Swap to facilitate rule of three
void swap(String& other) throw ()
{
std::swap(len, other.len);
std::swap(data, other.data);
}
// New Stuff
// Move Operators
String(String&& rhs) throw()
: len(0)
, data(null)
{
rhs.swap(*this);
}
String& operator=(String&& rhs) throw()
{
rhs.swap(*this);
return *this;
}
};
Довольно стандартный болотный я думаю.
Затем я проверил свой код следующим образом:
int main()
{
String a("Hi");
a = String("Test Move Assignment");
}
Здесь присвоение a
должно использовать оператор Move Assignment. Но есть столкновение с оператором "Стандартное присваивание" (которое записывается как стандартная копия и своп).
> g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.0.0
Thread model: posix
> g++ -std=c++11 String.cpp
String.cpp:64:9: error: use of overloaded operator '=' is ambiguous (with operand types 'String' and 'String')
a = String("Test Move Assignment");
~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
String.cpp:32:17: note: candidate function
String& operator=(String rhs)
^
String.cpp:54:17: note: candidate function
String& operator=(String&& rhs)
^
Теперь я могу исправить это, изменив оператор "Стандартное присвоение" на:
String& operator=(String const& rhs)
{
String copy(rhs);
copy.swap(*this);
return *this;
}
Но это нехорошо, так как это противоречит способности компилятора оптимизировать копирование и свопинг. См. Что такое идиома копирования и свопинга? здесь и здесь
Я пропустил что-то не столь очевидное?
Ответы
Ответ 1
Если вы определяете оператор присваивания для принятия значения, вы не должны (не нужно и не можете) определить оператор присваивания, принимающий rvalue-reference. Нет никакого смысла.
В общем, вам нужно только обеспечить перегрузку с использованием rvalue-reference, когда вам нужно различать lvalue от rvalue, но в этом случае ваш выбор реализации означает, что вам не нужно делать это различие. Если у вас есть lvalue или rvalue, вы собираетесь создать аргумент и поменять содержимое.
String f();
String a;
a = f(); // with String& operator=(String)
В этом случае компилятор решит, что вызов будет a.operator=(f());
, он поймет, что единственной причиной возвращаемого значения является аргумент operator=
и будет лишен какой-либо копии - это точка создания функция принимает значение в первую очередь!
Ответ 2
Другие ответы предлагают иметь только одну перегрузку operator =(String rhs)
с аргументом по значению, но это не самая эффективная реализация.
Верно, что в этом примере Дэвид Родригес - дрибеас
String f();
String a;
a = f(); // with String& operator=(String)
копия не выполнена. Однако предположим, что предоставляется только operator =(String rhs)
и рассмотрим этот пример:
String a("Hello"), b("World");
a = b;
Что происходит, это
-
b
копируется в rhs
(выделение памяти + memcpy
);
-
a
и rhs
меняются местами;
-
rhs
уничтожен.
Если мы реализуем operator =(const String& rhs)
и operator =(String&& rhs)
, тогда мы можем избежать выделения памяти на шаге 1, когда цель имеет длину, большую, чем исходная. Например, это простая реализация (не идеальная: может быть лучше, если String
имеет член capacity
):
String& operator=(const String& rhs) {
if (len < rhs.len) {
String tmp(rhs);
swap(tmp);
else {
len = rhs.len;
memcpy(data, rhs.data, len);
data[len] = 0;
}
return *this;
}
String& operator =(String&& rhs) {
swap(rhs);
}
В дополнение к точке производительности, если swap
- noexcept
, тогда operator =(String&&)
может быть noexcept
. (Это не так, если распределение памяти "потенциально" выполнено.)
Подробнее об этом превосходном объясните Говардом Хиннантом.
Ответ 3
Все, что вам нужно для копирования и назначения:
// As before
String(const String& rhs);
String(String&& rhs)
: len(0), data(0)
{
rhs.swap(*this);
}
String& operator = (String rhs)
{
rhs.swap(*this);
return *this;
}
void swap(String& other) noexcept {
// As before
}