Qual é o idioma de copiar e trocar?


2002

Qual é esse idioma e quando deve ser usado? Quais problemas ele resolve? O idioma muda quando o C ++ 11 é usado?

Embora tenha sido mencionado em muitos lugares, não tivemos nenhuma pergunta e resposta singular "o que é isso", então aqui está. Aqui está uma lista parcial dos lugares onde foi mencionado anteriormente:



2
Impressionante, vinculei essa pergunta da minha resposta para mover a semântica .
Fredoverflow

4
É uma boa idéia ter uma explicação completa para esse idioma, é tão comum que todos saibam disso.
Matthieu M.

16
Aviso: O idioma de cópia / troca é usado com muito mais frequência do que é útil. Muitas vezes, é prejudicial ao desempenho quando não é necessária uma garantia de segurança de exceção forte na atribuição de cópias. E quando é necessária uma forte segurança de exceção para a atribuição de cópias, ela é facilmente fornecida por uma curta função genérica, além de um operador de atribuição de cópias muito mais rápido. Consulte slideshare.net/ripplelabs/howard-hinnant-accu2014 slides 43 - 53. Resumo: copiar / trocar é uma ferramenta útil na caixa de ferramentas. Mas foi exagerado no mercado e subseqüentemente foi abusado.
Howard Hinnant

2
@ HowardHinnant: Sim, +1 a isso. Eu escrevi isso em um momento em que quase todas as perguntas em C ++ eram "ajudar minha classe a falhar quando uma cópia era" e essa era a minha resposta. É apropriado quando você quer apenas copiar / mover-semântica de trabalho ou o que for, para poder passar para outras coisas, mas não é realmente ideal. Sinta-se à vontade para colocar um aviso no topo da minha resposta, se achar que isso vai ajudar.
GManNickG

Respostas:


2184

Visão geral

Por que precisamos do idioma de copiar e trocar?

Qualquer classe que gerencia um recurso (um invólucro , como um ponteiro inteligente) precisa implementar os Três Grandes . Embora os objetivos e a implementação do construtor e destruidor de cópias sejam diretos, o operador de atribuição de cópias é sem dúvida o mais detalhado e difícil. Como isso deve ser feito? Que armadilhas precisam ser evitadas?

O idioma de copiar e trocar é a solução e ajuda o operador de atribuição de maneira elegante a conseguir duas coisas: evitar a duplicação de código e fornecer uma forte garantia de exceção .

Como funciona?

Conceitualmente , ele funciona usando a funcionalidade do construtor de cópia para criar uma cópia local dos dados e, em seguida, leva os dados copiados com uma swapfunção, trocando os dados antigos pelos novos. A cópia temporária é destruída, levando os dados antigos. Ficamos com uma cópia dos novos dados.

Para usar o idioma copy-and-swap, precisamos de três coisas: um construtor de cópias de trabalho, um destruidor de trabalho (ambos são a base de qualquer wrapper, portanto, deve estar completo de qualquer maneira) e uma swapfunção.

Uma função de troca é uma função não lançadora que troca dois objetos de uma classe, membro por membro. Podemos ser tentados a usar, em std::swapvez de fornecer os nossos, mas isso seria impossível; std::swapusa o operador construtor de cópia e atribuição de cópia em sua implementação e, finalmente, tentaríamos definir o operador de atribuição em termos de si mesmo!

(Não apenas isso, mas chamadas não qualificadas para swapusar nosso operador de troca personalizado, ignorando a construção e destruição desnecessárias de nossa classe que std::swapisso implicaria.)


Uma explicação detalhada

O objetivo

Vamos considerar um caso concreto. Queremos gerenciar, em uma classe inútil, uma matriz dinâmica. Começamos com um construtor, copiador-construtor e destruidor:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Essa classe quase gerencia a matriz com êxito, mas precisa operator=funcionar corretamente.

Uma solução com falha

Aqui está como uma implementação ingênua pode parecer:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

E dizemos que terminamos; isso agora gerencia uma matriz, sem vazamentos. No entanto, ele sofre de três problemas, marcados sequencialmente no código como (n).

  1. O primeiro é o teste de auto-atribuição. Essa verificação serve para dois propósitos: é uma maneira fácil de impedir a execução de códigos desnecessários na atribuição automática e nos protege de erros sutis (como excluir a matriz apenas para tentar copiá-la). Mas em todos os outros casos, serve apenas para retardar o programa e agir como ruído no código; a atribuição automática raramente ocorre; portanto, na maioria das vezes essa verificação é um desperdício. Seria melhor se o operador pudesse funcionar corretamente sem ele.

  2. A segunda é que ela fornece apenas uma garantia básica de exceção. Se new int[mSize]falhar, *thisterá sido modificado. (Ou seja, o tamanho está incorreto e os dados se foram!) Para uma garantia de exceção forte, seria necessário algo semelhante a:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. O código foi expandido! O que nos leva ao terceiro problema: duplicação de código. Nosso operador de atribuição duplica efetivamente todo o código que já escrevemos em outro lugar, e isso é uma coisa terrível.

No nosso caso, o núcleo é de apenas duas linhas (a alocação e a cópia), mas com recursos mais complexos, esse inchaço do código pode ser um aborrecimento. Devemos nos esforçar para nunca nos repetir.

(Alguém pode se perguntar: se esse código é necessário para gerenciar um recurso corretamente, e se minha classe gerencia mais de um? Embora isso possa parecer uma preocupação válida e, de fato, exija cláusulas try/ não triviais catch, isso não é Isso porque uma classe deve gerenciar apenas um recurso !)

Uma solução de sucesso

Como mencionado, o idioma de copiar e trocar irá corrigir todos esses problemas. Mas agora, temos todos os requisitos, exceto um: uma swapfunção. Embora a Regra dos Três implique com sucesso a existência de nosso construtor de cópias, operador de atribuição e destruidor, ela deve realmente ser chamada de "Os Três Grandes e Meio": sempre que sua classe gerencia um recurso, também faz sentido fornecer uma swapfunção .

Precisamos adicionar a funcionalidade de troca à nossa classe e fazemos o seguinte:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Aqui está a explicação do motivo public friend swap.) Agora, não apenas podemos trocar os nossos dumb_array, mas os swaps em geral podem ser mais eficientes; apenas troca ponteiros e tamanhos, em vez de alocar e copiar matrizes inteiras. Além desse bônus em funcionalidade e eficiência, agora estamos prontos para implementar o idioma de copiar e trocar.

Sem mais delongas, nosso operador de atribuição é:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

E é isso! De uma só vez, todos os três problemas são resolvidos de uma maneira elegante.

Por que isso funciona?

Primeiro notamos uma escolha importante: o argumento do parâmetro é tomado por valor . Embora alguém possa facilmente fazer o seguinte (e, de fato, muitas implementações ingênuas do idioma):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Perdemos uma importante oportunidade de otimização . Não apenas isso, mas essa opção é crítica no C ++ 11, que será discutido mais adiante. (Em uma observação geral, uma orientação extremamente útil é a seguinte: se você deseja fazer uma cópia de algo em uma função, deixe o compilador fazer isso na lista de parâmetros.)

De qualquer forma, esse método de obter nosso recurso é a chave para eliminar a duplicação de código: usamos o código do construtor de cópia para fazer a cópia e nunca precisamos repetir nada disso. Agora que a cópia foi feita, estamos prontos para trocar.

Observe que, ao entrar na função, todos os novos dados já estão alocados, copiados e prontos para serem usados. É isso que nos dá uma forte garantia de exceção de graça: nem entraremos na função se a construção da cópia falhar e, portanto, não é possível alterar o estado de *this. (O que fizemos manualmente antes para garantir uma exceção forte, o compilador está fazendo por nós agora; que tipo.)

Neste ponto, estamos livres de casa, porque swapnão jogam. Trocamos nossos dados atuais pelos dados copiados, alterando com segurança nosso estado, e os dados antigos são colocados no temporário. Os dados antigos são liberados quando a função retorna. (Onde o escopo do parâmetro termina e seu destruidor é chamado.)

Como o idioma não repete nenhum código, não podemos introduzir bugs no operador. Observe que isso significa que estamos livres da necessidade de uma verificação de auto-atribuição, permitindo uma implementação única e uniforme de operator=. (Além disso, não temos mais uma penalidade de desempenho em não atribuições próprias.)

E esse é o idioma de copiar e trocar.

E o C ++ 11?

A próxima versão do C ++, C ++ 11, faz uma mudança muito importante na maneira como gerenciamos os recursos: a Regra dos Três é agora a Regra dos Quatro (e meia). Por quê? Como não precisamos apenas copiar e construir nosso recurso, precisamos movê-lo também .

Felizmente para nós, isso é fácil:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

O que está acontecendo aqui? Lembre-se do objetivo de mover-construção: pegar os recursos de outra instância da classe, deixando-a em um estado garantido para ser atribuível e destrutível.

Então, o que fizemos é simples: inicialize através do construtor padrão (um recurso do C ++ 11), depois troque com other; sabemos que uma instância construída padrão de nossa classe pode ser atribuída e destruída com segurança; portanto, sabemos otherque será capaz de fazer o mesmo após a troca.

(Observe que alguns compiladores não oferecem suporte à delegação de construtores; nesse caso, temos que construir manualmente manualmente a classe. Essa é uma tarefa infeliz, mas felizmente trivial.)

Por que isso funciona?

Essa é a única mudança que precisamos fazer em nossa classe, então por que funciona? Lembre-se da sempre importante decisão que tomamos para tornar o parâmetro um valor e não uma referência:

dumb_array& operator=(dumb_array other); // (1)

Agora, se otherestiver sendo inicializado com um rvalue, ele será construído com movimentos . Perfeito. Da mesma maneira que o C ++ 03 vamos reutilizar nossa funcionalidade de construtor de cópia, assumindo o argumento por valor, o C ++ 11 selecionará automaticamente o construtor de movimentação quando apropriado também. (E, é claro, como mencionado no artigo vinculado anteriormente, a cópia / movimentação do valor pode simplesmente ser totalmente eliminada.)

E assim conclui o idioma de copiar e trocar.


Notas de rodapé

* Por que definimos mArraycomo nulo? Porque se qualquer código adicional no operador lançar, o destruidor de dumb_arraypoderá ser chamado; e se isso acontecer sem defini-lo como nulo, tentamos excluir a memória que já foi excluída! Evitamos isso definindo-o como nulo, pois a exclusão de nulo é uma não operação.

† Há outras alegações de que devemos nos especializar std::swappara o nosso tipo, fornecer uma swapfunção gratuita ao lado da classe swap, etc. Mas tudo isso é desnecessário: qualquer uso adequado swapserá por meio de uma chamada não qualificada e nossa função será encontrado através de ADL . Uma função serve.

‡ O motivo é simples: depois de ter o recurso disponível, você pode trocá-lo e / ou movê-lo (C ++ 11) para qualquer lugar que ele precisar. E, ao fazer a cópia na lista de parâmetros, você maximiza a otimização.

†† O construtor de movimentação geralmente deve ser noexcept, caso contrário, algum código (por exemplo, std::vectorlógica de redimensionamento) usará o construtor de cópia mesmo quando uma movimentação faria sentido. Obviamente, apenas marque-o como exceto se o código interno não gerar exceções.


17
@ GMan: Eu diria que uma classe que gerencia vários recursos ao mesmo tempo está fadada ao fracasso (a segurança de exceção se torna um pesadelo) e eu recomendo fortemente que uma classe gerencie UM recurso OU possua funcionalidade de negócios e use gerentes.
Matthieu M.

22
Não entendo por que o método swap é declarado como amigo aqui?
szx

9
@ asd: Para permitir que ele seja encontrado através do ADL.
GManNickG

8
@neuviemeporte: Com parênteses, os elementos das matrizes são inicializados por padrão. Sem, eles não são inicializados. Como no construtor copy, sobrescrevemos os valores de qualquer maneira, podemos pular a inicialização.
GManNickG

10
@neuviemeporte: você precisa swapser encontrado durante o ADL, se quiser que ele funcione no código mais genérico que você encontrará, como em boost::swapoutras instâncias de troca. A troca é uma questão complicada em C ++, e geralmente todos concordamos que um único ponto de acesso é o melhor (por consistência), e a única maneira de fazer isso em geral é uma função livre ( intnão pode ter um membro de troca, por exemplo). Veja minha pergunta para alguns antecedentes.
GManNickG 19/07/12

274

A atribuição, em sua essência, é duas etapas: derrubar o antigo estado do objeto e construir seu novo estado como uma cópia do estado de outro objeto.

Basicamente, é isso que o destruidor e o construtor de cópias fazem, então a primeira idéia seria delegar o trabalho a eles. No entanto, como a destruição não deve falhar, enquanto a construção pode, na verdade, queremos fazer o contrário : primeiro execute a parte construtiva e, se tiver sido bem-sucedida, faça a parte destrutiva . O idioma de copiar e trocar é uma maneira de fazer exatamente isso: primeiro chama o construtor de cópias de uma classe para criar um objeto temporário, depois troca seus dados pelos do temporário e, em seguida, permite que o destruidor do temporário destrua o estado antigo.
Desde aswap()deve nunca falhar, a única parte que pode falhar é a construção da cópia. Isso é realizado primeiro e, se falhar, nada será alterado no objeto de destino.

Em sua forma refinada, a cópia e troca é implementada pela execução da cópia, inicializando o parâmetro (sem referência) do operador de atribuição:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

1
Eu acho que mencionar o pimpl é tão importante quanto mencionar a cópia, a troca e a destruição. A troca não é magicamente segura contra exceções. É seguro contra exceções porque a troca de ponteiros é segura. Você não precisa usar um pimpl, mas se não o fizer, certifique-se de que cada troca de um membro seja segura contra exceções. Pode ser um pesadelo quando esses membros podem mudar e é trivial quando estão escondidos atrás de uma cafeteira. E então, vem o custo do cafetão. O que nos leva à conclusão de que muitas vezes a segurança de exceção tem um custo no desempenho.
wilhelmtell

7
std::swap(this_string, that)não oferece garantia de não-lance. Ele oferece uma forte segurança de exceção, mas não uma garantia de não-lance.
wilhelmtell

11
@ wilhelmtell: No C ++ 03, não há menção de exceções potencialmente lançadas por std::string::swap(que são chamadas por std::swap). No C ++ 0x, std::string::swapé noexcepte não deve lançar exceções.
James McNellis

2
@sbi @JamesMcNellis ok, mas o ponto ainda permanece: se você tem membros do tipo classe, deve certificar-se de que não é possível trocá-los. Se você tem um único membro que é um ponteiro, isso é trivial. Caso contrário, não é.
wilhelmtell

2
@wilhelmtell: Eu pensei que era o ponto de troca: ele nunca joga e é sempre O (1) (sim, eu sei, std::array...)
SBI

44

Já existem algumas boas respostas. Vou me concentrar principalmente no que acho que falta - uma explicação dos "contras" com o idioma de copiar e trocar ....

Qual é o idioma de copiar e trocar?

Uma maneira de implementar o operador de atribuição em termos de uma função de swap:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

A ideia fundamental é que:

  • a parte mais propensa a erros de atribuir a um objeto é garantir que todos os recursos necessários para o novo estado sejam adquiridos (por exemplo, memória, descritores)

  • essa aquisição pode ser tentada antes de modificar o estado atual do objeto (ou seja *this) se uma cópia do novo valor for feita, e é por isso que rhsé aceito por valor (ou seja, copiado) e não por referência

  • trocar o estado da cópia local rhse geralmente*this é relativamente fácil de fazer sem possíveis falhas / exceções, dado que a cópia local não precisa de nenhum estado específico posteriormente (apenas precisa do estado adequado para o destruidor executar, assim como para um objeto sendo movido de em> = C ++ 11)

Quando deve ser usado? (Quais problemas ele resolve [/ create] ?)

  • Quando você deseja que o objeto atribuído seja afetado por uma atribuição que gera uma exceção, supondo que você tenha ou possa escrever uma swapgarantia com exceção forte e, idealmente, uma que não possa falhar / throw.. †

  • Quando você deseja uma maneira limpa, fácil de entender e robusta de definir o operador de atribuição em termos de swapfunções (mais simples) de construtor de cópias e funções de destruidor.

    • A atribuição automática feita como uma cópia e troca evita casos extremos negligenciados.

  • Quando qualquer penalidade de desempenho ou momentaneamente maior uso de recursos criado por um objeto temporário extra durante a atribuição não é importante para o seu aplicativo. ⁂

swaplançamento: geralmente é possível trocar confiavelmente membros de dados que os objetos rastreiam por ponteiro, mas membros que não são de ponteiros que não possuem troca sem lançamento ou para os quais a troca deve ser implementada como X tmp = lhs; lhs = rhs; rhs = tmp;construção de cópia ou atribuição pode jogar, ainda tem o potencial de falhar, deixando alguns membros de dados trocados e outros não. Esse potencial se aplica até ao C ++ 03 std::string, quando James comenta outra resposta:

@wilhelmtell: No C ++ 03, não há menção de exceções potencialmente lançadas por std :: string :: swap (que é chamada por std :: swap). No C ++ 0x, std :: string :: swap é noexcept e não deve gerar exceções. - James McNellis Dec 22 '10 às 15:24


‡ A implementação do operador de atribuição que parece sã ao atribuir a partir de um objeto distinto pode facilmente falhar na atribuição automática. Embora possa parecer inimaginável que o código do cliente tente tentar se auto-atribuir, isso pode acontecer com relativa facilidade durante operações de algo em contêineres, com x = f(x);código em que fé (talvez apenas para algumas #ifdeframificações) uma ala de macro #define f(x) xou uma função que retorna uma referência xou até mesmo (provavelmente ineficiente, mas conciso) como x = c1 ? x * 2 : c2 ? x / 2 : x;). Por exemplo:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

Na auto-atribuição, a exclusão do código acima x.p_;aponta p_para uma região de heap recém-alocada e tenta ler os dados não inicializados nela (comportamento indefinido), se isso não fizer algo muito estranho, copytenta uma auto-atribuição para todos os destruído 'T'!


Id O idioma de copiar e trocar pode introduzir ineficiências ou limitações devido ao uso de um temporário extra (quando o parâmetro do operador é construído com cópia):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Aqui, um manuscrito Client::operator=pode verificar se *thisjá está conectado ao mesmo servidor que rhs(talvez enviando um código de "redefinição", se útil), enquanto a abordagem de copiar e trocar invocaria o construtor de cópia que provavelmente seria gravado para abrir uma conexão de soquete distinta e feche a original. Isso poderia significar não apenas uma interação remota de rede, em vez de uma cópia simples de variável em processo, mas também os limites de clientes ou servidores em recursos ou conexões de soquete. (É claro que essa classe tem uma interface bastante horrível, mas isso é outra questão ;-P).


4
Dito isso, uma conexão de soquete era apenas um exemplo - o mesmo princípio se aplica a qualquer inicialização potencialmente cara, como análise / inicialização / calibração de hardware, gerando um conjunto de threads ou números aleatórios, determinadas tarefas de criptografia, caches, verificações de sistema de arquivos, banco de dados conexões etc ..
Tony Delroy

Há mais um golpe (maciço). Tecnicamente, nas especificações atuais, o objeto não terá um operador de atribuição de movimento! Se mais tarde usada como membro de uma classe, a nova classe não terá o movedor gerado automaticamente! Fonte: youtu.be/mYrbivnruYw?t=43m14s
user362515

3
O principal problema com o operador de atribuição de cópias Clienté que a atribuição não é proibida.
S22

No exemplo do cliente, a classe deve ser tornada não copiável.
John Z. Li

25

Esta resposta é mais como uma adição e uma ligeira modificação às respostas acima.

Em algumas versões do Visual Studio (e possivelmente em outros compiladores), há um bug que é realmente irritante e não faz sentido. Portanto, se você declarar / definir sua swapfunção assim:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... o compilador gritará com você quando você chamar a swapfunção:

insira a descrição da imagem aqui

Isso tem algo a ver com uma friendfunção sendo chamada e um thisobjeto sendo passado como parâmetro.


Uma maneira de contornar isso é não usar a friendpalavra-chave e redefinir a swapfunção:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Desta vez, você pode simplesmente ligar swape passar other, deixando o compilador feliz:

insira a descrição da imagem aqui


Afinal, você não precisa usar uma friendfunção para trocar 2 objetos. Faz tanto sentido fazer swapuma função membro que tenha um otherobjeto como parâmetro.

Você já tem acesso ao thisobjeto, portanto, transmiti-lo como um parâmetro é tecnicamente redundante.


1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg . Esta é uma versão simplificada. Um erro parece ocorrer cada vez que uma friendfunção é chamada com *thiso parâmetro
Oleksiy

1
@GManNickG como eu disse, é um bug e pode funcionar bem para outras pessoas. Eu só queria ajudar algumas pessoas que podem ter o mesmo problema que eu. Eu tentei isso com o Visual Studio 2012 Express e 2013 Preview ea única coisa que fez isso ir embora, foi a minha modificação
Oleksiy

8
@GManNickG não caberia em um comentário com todas as imagens e exemplos de código. E está tudo bem se as pessoas votarem menos, tenho certeza de que há alguém por aí que está recebendo o mesmo bug; as informações nesta postagem podem ser exatamente o que elas precisam.
precisa saber é

14
observe que isso é apenas um bug no realce do código IDE (IntelliSense) ... Ele será compilado perfeitamente sem avisos / erros.
Amro #

3
Por favor, reporte o bug VS aqui se você não o tiver feito (e se não foi corrigido) connect.microsoft.com/VisualStudio
Matt

15

Gostaria de adicionar uma palavra de aviso ao lidar com contêineres compatíveis com o alocador do estilo C ++ 11. A troca e a atribuição têm semânticas sutilmente diferentes.

Para concretização, vamos considerar um contêiner std::vector<T, A>, onde Aé algum tipo de alocador com estado, e compararemos as seguintes funções:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

O objetivo de ambas as funções fse fmé dar ao estado que btinha inicialmente. No entanto, há uma pergunta oculta: o que acontece se a.get_allocator() != b.get_allocator()? A resposta é: depende. Vamos gravação AT = std::allocator_traits<A>.

  • Se AT::propagate_on_container_move_assignmentestiver std::true_type, fmreatribui o alocador de acom o valor de b.get_allocator(), caso contrário, não e acontinua a usar seu alocador original. Nesse caso, os elementos de dados precisam ser trocados individualmente, pois o armazenamento ae bnão é compatível.

  • Se AT::propagate_on_container_swapestiver std::true_type, fstroque dados e alocadores da maneira esperada.

  • Se AT::propagate_on_container_swapfor std::false_type, precisamos de uma verificação dinâmica.

    • Se a.get_allocator() == b.get_allocator(), então os dois contêineres usam armazenamento compatível, e a troca prossegue da maneira usual.
    • No entanto, se a.get_allocator() != b.get_allocator()o programa tiver um comportamento indefinido (consulte [container.requirements.general / 8]).

O resultado é que a troca se tornou uma operação não trivial no C ++ 11 assim que seu contêiner começa a oferecer suporte a alocadores com estado. Esse é um "caso de uso avançado", mas não é totalmente improvável, pois as otimizações de movimento geralmente só se tornam interessantes quando a classe gerencia um recurso, e a memória é um dos recursos mais populares.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.