Назначение перемещения несовместимо со стандартным копированием и обменом

Тестирование новой семантики Move.

Я только что спросил о проблемах, которые у меня были с конструктором Move. Но, как выясняется в комментариях, проблема в том, что оператор «Назначение перемещения» и оператор «Стандартное назначение» конфликтуют при использовании стандартной идиомы «Копировать и поменять».

Это класс, который я использую:

#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;
}

Но это нехорошо, так как это портит способность компилятора оптимизировать копирование и обмен. См. Что такое идиома копирования и обмена? Вот а также Вот

Я что-то упускаю не так очевидно?

21

Решение

Если вы определяете оператор присваивания для получения значения, вы не должны (не должны и не можете) определять оператор присваивания, используя ссылку-значение. Там нет никакого смысла в этом.

В общем случае вам нужно предоставить перегрузку, принимающую rvalue-reference, когда вам нужно отличить lvalue от rvalue, но в этом случае ваш выбор реализации означает, что вам не нужно проводить это различие. Независимо от того, есть ли у вас lvalue или rvalue, вы собираетесь создать аргумент и поменять местами содержимое.

String f();
String a;
a = f();   // with String& operator=(String)

В этом случае компилятор разрешит вызов a.operator=(f()); он поймет, что единственной причиной возвращаемого значения является аргумент operator= и исключит любую копию — в этом и состоит смысл заставить функцию принимать значение в первую очередь!

24

Другие решения

Другие ответы предлагают иметь только одну перегрузку operator =(String rhs) принимая аргумент по значению, но это не самая эффективная реализация.

Это правда, что в этом примере Дэвид Родригес — dribeas

String f();
String a;
a = f();   // with String& operator=(String)

копия не сделана. Однако предположим, что просто operator =(String rhs) предоставляется и рассмотрим этот пример:

String a("Hello"), b("World");
a = b;

Что происходит

  1. b копируется в rhs (выделение памяти + memcpy);
  2. a а также rhs поменялись местами;
  3. 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 также. (Это не тот случай, если выделение памяти выполняется «потенциально».)

Смотрите больше деталей в этом отличном объяснение Говард Хиннант.

8

Все, что вам нужно для копирования и назначения, это:

    // 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
}
3