Quando tornar um tipo não móvel no C ++ 11?

127

Fiquei surpreso que isso não tenha aparecido nos meus resultados de pesquisa, pensei que alguém teria perguntado isso antes, dada a utilidade da semântica de movimento no C ++ 11:

Quando tenho que (ou é uma boa idéia para mim) tornar uma classe não móvel no C ++ 11?

( Outros motivos que não problemas de compatibilidade com o código existente).

user541686
fonte
2
impulso é sempre um passo à frente - "caro para tipos de movimentos" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin
1
Eu acho que essa é uma pergunta muito boa e útil ( +1de mim), com uma resposta muito completa de Herb (ou de seu irmão gêmeo, ao que parece ), então fiz uma entrada na FAQ. Se alguém me fizer ping apenas no lounge , isso poderá ser discutido lá.
S13
1
As classes móveis AFAIK ainda podem estar sujeitas a fatias, portanto, faz sentido proibir a movimentação (e cópia) de todas as classes base polimórficas (ou seja, todas as classes base com funções virtuais).
Philipp
1
@ Mehrdad: Estou apenas dizendo que "T tem um construtor de movimentos" e " T x = std::move(anotherT);ser legal" não são equivalentes. O último é uma solicitação de movimentação que pode recorrer ao copiador no caso de T não ter um movedor. Então, o que significa "móvel" exatamente?
sellibitze
1
@ Mehrdad: Confira a seção da biblioteca padrão C ++ sobre o que significa "MoveConstructible". Alguns iteradores podem não ter um construtor de movimentação, mas ainda é MoveConstructible. Cuidado com as várias definições de pessoas "móveis" que têm em mente.
sellibitze

Respostas:

110

A resposta de Herb (antes de ser editado) realmente deu um bom exemplo de um tipo que não deve ser móvel: std::mutex.

O tipo mutex nativo do sistema operacional (por exemplo, pthread_mutex_tnas plataformas POSIX) pode não ser "invariável no local", o que significa que o endereço do objeto faz parte do seu valor. Por exemplo, o sistema operacional pode manter uma lista de ponteiros para todos os objetos mutex inicializados. Se std::mutexcontivesse um tipo de mutex do SO nativo como membro de dados e o endereço do tipo nativo std::mutexdevesse permanecer fixo (porque o SO mantém uma lista de ponteiros para os mutexes), seria necessário armazenar o tipo de mutex nativo no heap para que ele permanecesse em o mesmo local quando movido entre std::mutexobjetos ou o std::mutexnão deve se mover. Não é possível armazená-lo no heap, porque a std::mutexpossui um constexprconstrutor e deve ser elegível para inicialização constante (ou seja, inicialização estática) para que um globalstd::mutexé garantido para ser construído antes do início da execução do programa, portanto, seu construtor não pode usá-lo new. Portanto, a única opção que resta é std::mutexser imóvel.

O mesmo raciocínio se aplica a outros tipos que contêm algo que requer um endereço fixo. Se o endereço do recurso precisar permanecer fixo, não o mova!

Há outro argumento para não se mexer std::mutex: seria muito difícil fazê-lo com segurança, porque você precisaria saber que ninguém está tentando bloquear o mutex no momento em que está sendo movido. Como os mutexes são um dos blocos de construção que você pode usar para evitar corridas de dados, seria lamentável se eles não estivessem seguros contra as próprias corridas! Com um imóvel, std::mutexvocê sabe que as únicas coisas que alguém pode fazer depois que ele for construído e antes de ser destruído é bloqueá-lo e desbloqueá-lo, e essas operações garantem explicitamente a segurança de threads e a não introduzir corridas de dados. Esse mesmo argumento se aplica aos std::atomic<T>objetos: a menos que eles possam ser movidos atomicamente, não seria possível movê-los com segurança, outro encadeamento pode estar tentando chamarcompare_exchange_strongno objeto no momento em que está sendo movido. Portanto, outro caso em que os tipos não devem ser móveis é onde eles são blocos de construção de baixo nível de código simultâneo seguro e devem garantir a atomicidade de todas as operações neles. Se o valor do objeto puder ser movido para um novo objeto a qualquer momento, você precisará usar uma variável atômica para proteger todas as variáveis ​​atômicas, para saber se é seguro usá-lo ou se foi movido ... e uma variável atômica para proteger essa variável atômica, e assim por diante ...

Eu acho que generalizaria para dizer que quando um objeto é apenas um pedaço de memória pura, não um tipo que atua como detentor de um valor ou abstração de um valor, não faz sentido movê-lo. Tipos fundamentais como intnão podem se mover: movê-los é apenas uma cópia. Você não pode arrancar as tripas de um int, pode copiar seu valor e depois defini-lo como zero, mas ainda é um intcom um valor, são apenas bytes de memória. Mas um intainda é móvelnos termos do idioma porque uma cópia é uma operação de movimentação válida. No entanto, para tipos não copiáveis, se você não deseja ou não pode mover o pedaço de memória e também não pode copiar seu valor, ele não é móvel. Um mutex ou uma variável atômica é um local específico da memória (tratado com propriedades especiais), portanto, não faz sentido mover-se e também não é copiável, portanto, não é móvel.

Jonathan Wakely
fonte
17
Marcar com +1 um exemplo menos exótico de algo que não pode ser movido porque possui um endereço especial é um nó em uma estrutura de gráfico direcionada.
Potatoswatter
3
Se o mutex não for copiável e móvel, como posso copiar ou mover um objeto que contenha um mutex? (Como uma classe thread-safe com a sua própria exclusão mútua para sincronização ...)
tr3w
4
@ tr3w, você não pode, a menos que você criar o mutex na pilha e segurá-la por meio de um unique_ptr ou similar
Jonathan Wakely
2
@ tr3w: Você não mudaria a classe inteira, exceto a parte mutex?
precisa saber é o seguinte
3
@BenVoigt, mas o novo objeto terá seu próprio mutex. Eu acho que ele quer dizer ter operações de movimentação definidas pelo usuário que movem todos os membros, exceto o membro mutex. E daí se o objeto antigo estiver expirando? Seu mutex expira com ele.
Jonathan Wakely
57

Resposta curta: Se um tipo é copiável, também deve ser móvel. No entanto, o contrário não é verdadeiro: alguns tipos std::unique_ptrsão móveis, mas não faz sentido copiá-los; esses são naturalmente tipos somente de movimentação.

Segue uma resposta um pouco mais longa ...

Existem dois tipos principais de tipos (entre outros de propósito mais específico, como características):

  1. Tipos de valor, como intou vector<widget>. Eles representam valores e devem ser naturalmente copiáveis. No C ++ 11, geralmente você deve pensar em mover-se como uma otimização de cópia e, portanto, todos os tipos copiáveis ​​devem ser naturalmente móveis. não precisa mais do objeto original e apenas o destruirá de qualquer maneira.

  2. Tipos de referência que existem em hierarquias de herança, como classes base e classes com funções-membro virtuais ou protegidas. Normalmente, eles são mantidos por ponteiro ou referência, geralmente um base*ou base&, e portanto não fornecem construção de cópia para evitar o fatiamento; se você deseja obter outro objeto como um existente, geralmente chama uma função virtual como clone. Eles não precisam de construção ou atribuição de movimento por dois motivos: eles não são copiáveis ​​e já possuem uma operação natural de "movimento" ainda mais eficiente - basta copiar / mover o ponteiro para o objeto e o próprio objeto não. precisa mudar para um novo local de memória.

A maioria dos tipos se enquadra em uma dessas duas categorias, mas também existem outros tipos que também são úteis, apenas mais raros. Em particular aqui, tipos que expressam propriedade exclusiva de um recurso, como std::unique_ptr, naturalmente, são apenas para movimentação, porque não são semelhantes a valores (não faz sentido copiá-los), mas você os usa diretamente (nem sempre por ponteiro ou referência) e, portanto, deseja mover objetos desse tipo de um lugar para outro.

Herb Sutter
fonte
61
O verdadeiro Herb Sutter, por favor, se levantaria? :)
fredoverflow
6
Sim, passei de usar uma conta do Google OAuth para outra e não posso me incomodar em procurar uma maneira de mesclar os dois logins que me fornecem aqui. (Ainda outro argumento contra o OAuth entre os muito mais convincentes.) Eu provavelmente não usarei o outro novamente, então é isso que vou usar agora para uma publicação ocasional no SO.
Herb Sutter
7
Eu pensei que std::mutexera imóvel, como mutexes POSIX são usados ​​por endereço.
Filhote de cachorro
9
@ SChepurin: Na verdade, isso é chamado HerbOverflow, então.
S13
26
Isso está recebendo muitas críticas positivas, ninguém percebeu que diz quando um tipo deve ser apenas para movimentação, o que não é a questão? :)
Jonathan Wakely
18

Na verdade, quando procuro, descobri que alguns tipos no C ++ 11 não são móveis:

  • todos os mutextipos ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • todos os atomictipos
  • once_flag

Aparentemente, há uma discussão sobre Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4

billz
fonte
1
... iteradores não devem ser móveis ?! O que? Por que?
precisa saber é o seguinte
Sim, acho que iterators / iterator adaptorsdeve ser editado como C ++ 11 tem move_iterator?
billz
Ok, agora estou apenas confuso. Você está falando de iteradores que movem seus destinos ou sobre a movimentação dos próprios iteradores ?
user541686
1
Assim é std::reference_wrapper. Ok, os outros realmente parecem ser móveis.
Christian Rau
1
Estes parecem se enquadram em três categorias: 1. de baixo nível tipos relacionados com a concorrência (Atomics, exclusões mútuas), 2. classes base polimórficos ( ios_base, type_info, facet), 3. coisas estranhas sortidas ( sentry). Provavelmente, as únicas classes imutáveis ​​que um programador médio escreverá estão na segunda categoria.
Philipp
0

Outra razão que eu encontrei - desempenho. Digamos que você tenha uma classe 'a' que possui um valor. Você deseja gerar uma interface que permita ao usuário alterar o valor por um tempo limitado (para um escopo).

Uma maneira de conseguir isso é retornando um objeto 'protetor de escopo' de 'a' que retorna o valor em seu destruidor, da seguinte maneira:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Se eu fizesse o change_value_guard móvel, teria que adicionar um 'if' ao seu destruidor para verificar se a proteção foi removida - isso é um extra se e um impacto no desempenho.

Sim, claro, provavelmente pode ser otimizado por qualquer otimizador sensato, mas ainda assim é bom que a linguagem (isso requer C ++ 17, porém, para poder retornar um tipo não móvel exija garantia de cópia) não nos exija pagar isso se, se não quisermos mudar a proteção, a não ser devolvê-la da função de criação (o princípio de não pagar pelo que você não usa).

saarraz1
fonte