Pergunta

Testando a nova semântica de movimentos.

Acabei de perguntar sobre um problema que estava tendo com o construtor de movimentos. Mas, como se vê nos comentários, o problema é realmente que o operador de "atribuição de mover" e o operador "atribuição padrão" se chocam quando você usa o idioma padrão de "copiar e trocar".

Esta é a classe que estou usando:

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

Padrão muito pântano, eu acho.

Então eu testei meu código assim:

int main()
{
    String  a("Hi");
    a   = String("Test Move Assignment");
}

Aqui a tarefa para a deve usar o operador "mover atribuição". Mas há um confronto com o operador de "atribuição padrão" (que é escrito como sua cópia e troca padrão).

> 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)
                ^

Agora eu posso corrigir isso modificando o operador de "atribuição padrão" para:

    String& operator=(String const& rhs)
    {
        String copy(rhs);
        copy.swap(*this);
        return *this;
    }

Mas isso não é bom, pois mexe com a capacidade do compilador de otimizar a cópia e a troca. Veja o que é o idioma de cópia e troca? aqui e aqui

Estou perdendo algo não tão óbvio?

Foi útil?

Solução

Se você definir o operador de atribuição para aceitar um valor, não deve (não precisar e não pode) definir o operador de atribuição que toma uma referência RValue. Não faz sentido isso.

Em geral, você só precisa fornecer uma sobrecarga de uma referência de RValue quando precisa diferenciar um LValue de um RValue, mas, neste caso, sua escolha de implementação significa que você não precisa fazer essa distinção. Se você tem um LValue ou um Rvalue, criará o argumento e trocará o conteúdo.

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

Nesse caso, o compilador resolverá a chamada para ser a.operator=(f()); ele perceberá que a única razão para o valor de retorno é ser o argumento para operator= E eliminará qualquer cópia -esse é o sentido de fazer a função assumir um valor em primeiro lugar!

Outras dicas

Outras respostas sugerem ter apenas uma sobrecarga operator =(String rhs) Tomando o argumento por valor, mas isso é não a implementação mais eficiente.

É verdade que neste exemplo de David Rodríguez - Dribeas

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

Nenhuma cópia é feita. No entanto, suponha apenas operator =(String rhs) é fornecido e considere este exemplo:

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

O que acontece é

  1. b é copiado para rhs (Alocação de memória + memcpy);
  2. a e rhs são trocados;
  3. rhs está destruído.

Se implementarmos operator =(const String& rhs) e operator =(String&& rhs) Em seguida, podemos evitar a alocação de memória na etapa 1 quando o alvo tem um comprimento maior que o da fonte. Por exemplo, essa é uma implementação simples (não perfeita: pode ser melhor se String tinha um capacity membro):

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

Além do ponto de desempenho se swap é noexcept, então operator =(String&&) pode ser noexcept também. (O que não é o caso se a alocação de memória for "potencialmente" realizada.)

Veja mais detalhes neste excelente explicação Por Howard Hinnant.

Tudo o que você precisa de cópia e atribuição é o seguinte:

    // 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
   }
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top