Por que nenhuma atribuição / construtor de movimentação padrão?

89

Sou um programador simples. As variáveis ​​de meus membros de classe geralmente consistem em tipos POD e contêineres STL. Por causa disso, raramente preciso escrever operadores de atribuição ou construtores de cópia, pois eles são implementados por padrão.

Adicione a isso, se eu usar std::moveem objetos não móveis, ele utiliza o operador de atribuição, o que significa que std::moveé perfeitamente seguro.

Como sou um programador simples, gostaria de aproveitar as vantagens dos recursos de movimentação sem adicionar um construtor de movimentação / operador de atribuição a cada classe que escrevo, já que o compilador poderia simplesmente implementá-los como " this->member1_ = std::move(other.member1_);..."

Mas não (pelo menos não no Visual 2010), há algum motivo particular para isso?

Mais importante; existe alguma maneira de contornar isso?

Atualização: Se você olhar para a resposta do GManNickG, ele fornece uma ótima macro para isso. E se você não sabia, se você implementar a semântica de movimento, poderá remover a função de membro de troca.

Viktor Sehr
fonte
5
você sabe que pode fazer com que o compilador gere um ctor de movimento padrão
aaronman
3
std :: move não executa um movimento, ele simplesmente converte de um valor l para um valor r. O movimento ainda é executado pelo construtor de movimento.
Owen Delahoy
1
Você está falando MyClass::MyClass(Myclass &&) = default;?
Sandburg
Sim, hoje em dia :)
Viktor Sehr

Respostas:

76

A geração implícita de construtores de movimento e operadores de atribuição tem sido controversa e houve grandes revisões em rascunhos recentes do padrão C ++, portanto, os compiladores disponíveis atualmente provavelmente se comportarão de maneira diferente com relação à geração implícita.

Para mais informações sobre a história do problema, consulte a lista de artigos do WG21 de 2010 e pesquise por "mov"

A especificação atual (N3225, de novembro) afirma (N3225 12.8 / 8):

Se a definição de uma classe Xnão declara explicitamente um construtor de movimento, um será declarado implicitamente como padrão se e somente se

  • X não tem um construtor de cópia declarado pelo usuário e

  • X não tem um operador de atribuição de cópia declarado pelo usuário,

  • X não tem um operador de atribuição de movimentação declarado pelo usuário,

  • X não tem um destruidor declarado pelo usuário, e

  • o construtor de movimento não seria definido implicitamente como excluído.

Há linguagem semelhante em 12.8 / 22 especificando quando o operador de atribuição de movimentação é declarado implicitamente como padrão. Você pode encontrar a lista completa de alterações feitas para suportar a especificação atual de geração de movimento implícito em N3203: Estreitando as condições para gerar movimentos implícitos , que foi amplamente baseada em uma das resoluções propostas pelo artigo N3201 de Bjarne Stroustrup : Seguir em frente .

James McNellis
fonte
4
Escrevi um pequeno artigo com alguns diagramas que descrevem as relações para o construtor / atribuição implícito (mover) aqui: mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny
Ugh, então sempre que eu tenho que definir destruidores em branco em classes de base polimórficas apenas para especificá-los como virtuais, eu tenho que definir explicitamente o construtor de movimento e o operador de atribuição também :(.
someguy
@James McNellis: Isso é algo que eu tentei anteriormente, mas o compilador não pareceu gostar. Ia postar a mensagem de erro nesta mesma resposta, mas depois de tentar reproduzir o erro, percebi que menciona que é cannot be defaulted *in the class body*. Então, eu defini o destruidor externo e funcionou :). Acho um pouco estranho, no entanto. Alguém tem uma explicação? O compilador é gcc 4.6.1
someguy
3
Talvez pudéssemos obter uma atualização para esta resposta agora que o C ++ 11 foi ratificado? Curioso que comportamentos venceram.
Joseph Garvin
2
@Guy Avraham: Acho que o que eu estava dizendo (já faz 7 anos) é que se eu tiver um destruidor declarado pelo usuário (mesmo um virtual vazio), nenhum construtor de movimento será declarado implicitamente como padrão. Suponho que isso resultaria em semântica de cópia? (Eu não toco em C ++ há anos.) James McNellis então comentou que virtual ~D() = default;deveria funcionar e ainda permitir um construtor de movimento implícito.
someguy
13

Construtores de movimento gerados implicitamente foram considerados para o padrão, mas podem ser perigosos. Veja a análise de Dave Abrahams .

No final, no entanto, o padrão incluiu a geração implícita de construtores de movimento e operadores de atribuição de movimento, embora com uma lista bastante substancial de limitações:

Se a definição de uma classe X não declara explicitamente um construtor de movimento, um será declarado implicitamente como padrão se e somente se
- X não tiver um construtor de cópia declarado pelo usuário,
- X não tiver um operador de atribuição de cópia declarado pelo usuário ,
- X não tem um operador de atribuição de movimentação declarado pelo usuário,
- X não tem um destruidor declarado pelo usuário e
- o construtor de movimentação não seria implicitamente definido como excluído.

No entanto, isso não é tudo na história. Um ctor pode ser declarado, mas ainda definido como excluído:

Um construtor de cópia / movimentação declarado implicitamente é um membro público embutido de sua classe. Um construtor de cópia / movimentação padrão para uma classe X é definido como excluído (8.4.3) se X tiver:

- um membro variante com um construtor correspondente não trivial e X é uma classe tipo união,
- um membro de dados não estático do tipo de classe M (ou matriz) que não pode ser copiado / movido devido à resolução de sobrecarga (13.3), como aplicada ao construtor correspondente de M, resulta em uma ambigüidade ou uma função que é excluída ou inacessível do construtor padrão,
- uma classe base direta ou virtual B que não pode ser copiada / movida devido à resolução de sobrecarga (13.3), conforme aplicada ao construtor correspondente de B , resulta em uma ambigüidade ou uma função que é excluída ou inacessível do construtor padrão,
- qualquer classe base direta ou virtual ou membro de dados não estáticos de um tipo com um destruidor que é excluído ou inacessível do construtor padrão,
- para o construtor de cópia, um membro de dados não estático do tipo de referência rvalue, ou
- para o construtor de movimento, um membro de dados não estático ou classe de base direta ou virtual com um tipo que não tem um construtor de movimento e não é trivial copiável.

Jerry Coffin
fonte
O projeto de trabalho atual permite a geração de movimento implícito sob certas condições e acho que a resolução aborda amplamente as preocupações de Abrahams.
James McNellis
Não tenho certeza se entendi qual movimento pode ocorrer no exemplo entre o Tweak 2 e o Tweak 3. Você poderia explicar?
Matthieu M.
@Matthieu M .: Tanto o Tweak 2 quanto o Tweak 3 estão quebrados, e de maneiras muito semelhantes, na verdade. No Tweak 2, existem membros privados com invariantes que podem ser quebrados pelo ctor de movimento. No Tweak 3, a classe não tem membros privados em si , mas como ela usa herança privada, os membros públicos e protegidos da base tornam-se membros privados da derivada, levando ao mesmo problema.
Jerry Coffin
1
Eu realmente não entendi como o construtor move quebraria a invariante de classe em Tweak2. Suponho que tenha algo a ver com o fato de que o Numberseria movido e vectorcopiado ... mas não tenho certeza: / Eu entendo que o problema iria se espalhar para Tweak3.
Matthieu M.
O link que você forneceu parece estar morto?
Wolf
8

(por enquanto, estou trabalhando em uma macro estúpida ...)

Sim, eu também fui por esse caminho. Aqui está sua macro:

// detail/move_default.hpp
#ifndef UTILITY_DETAIL_MOVE_DEFAULT_HPP
#define UTILITY_DETAIL_MOVE_DEFAULT_HPP

#include <boost/preprocessor.hpp>

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE(pR, pData, pBase) pBase(std::move(pOther))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE(pR, pData, pBase) pBase::operator=(std::move(pOther));

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR(pR, pData, pMember) pMember(std::move(pOther.pMember))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT(pR, pData, pMember) pMember = std::move(pOther.pMember);

#define UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        ,                                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)                                                   \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#endif

// move_default.hpp
#ifndef UTILITY_MOVE_DEFAULT_HPP
#define UTILITY_MOVE_DEFAULT_HPP

#include "utility/detail/move_default.hpp"

// move bases and members
#define UTILITY_MOVE_DEFAULT(pT, pBases, pMembers) UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)

// base only version
#define UTILITY_MOVE_DEFAULT_BASES(pT, pBases) UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)

// member only version
#define UTILITY_MOVE_DEFAULT_MEMBERS(pT, pMembers) UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)

#endif

(Eu removi os comentários reais, que são longos e documentais.)

Você especifica as bases e / ou membros em sua classe como uma lista de pré-processador, por exemplo:

#include "move_default.hpp"

struct foo
{
    UTILITY_MOVE_DEFAULT_MEMBERS(foo, (x)(str));

    int x;
    std::string str;
};

struct bar : foo, baz
{
    UTILITY_MOVE_DEFAULT_BASES(bar, (foo)(baz));
};

struct baz : bar
{
    UTILITY_MOVE_DEFAULT(baz, (bar), (ptr));

    void* ptr;
};

E surge um construtor de movimento e um operador de atribuição de movimento.

(À parte, se alguém souber como posso combinar os detalhes em uma macro, isso seria ótimo.)

GManNickG
fonte
Muito obrigado, o meu é muito semelhante, exceto que tive que passar o número de variáveis ​​de membro como um argumento (o que é realmente uma merda).
Viktor Sehr
1
@Viktor: Sem problemas. Se não for tarde demais, acho que você deve marcar uma das outras respostas como aceita. O meu foi mais um "a propósito, aqui está um caminho" e não uma resposta à sua pergunta real.
GManNickG
1
Se eu estiver lendo sua macro corretamente, então, assim que seu compilador implementar membros move padrão, seus exemplos acima se tornarão não copiáveis. A geração implícita de membros de cópia é inibida quando há membros de movimentação declarados explicitamente presentes.
Howard Hinnant
@Howard: Tudo bem, é uma solução temporária até então. :)
GManNickG
GMan: Esta macro adiciona moveconstructor \ assign se você tiver uma função de troca:
Viktor Sehr
4

VS2010 não faz isso porque eles não eram padrão no momento da implementação.

Cachorro
fonte