Existe uma diferença entre inicialização de cópia e inicialização direta?


244

Suponha que eu tenho esta função:

void my_test()
{
    A a1 = A_factory_func();
    A a2(A_factory_func());

    double b1 = 0.5;
    double b2(0.5);

    A c1;
    A c2 = A();
    A c3(A());
}

Em cada agrupamento, essas instruções são idênticas? Ou existe uma cópia extra (possivelmente otimizável) em algumas das inicializações?

Eu já vi pessoas dizerem as duas coisas. Por favor, citar texto como prova. Adicione também outros casos, por favor.


1
E há o quarto caso discutido por @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum 5/07

1
Apenas uma nota de 2018: as regras foram alteradas no C ++ 17 , veja, por exemplo, aqui . Se meu entendimento estiver correto, no C ++ 17, ambas as instruções são efetivamente as mesmas (mesmo que o copiador seja explícito). Além disso, se a expressão init fosse de outro tipo A, a inicialização da cópia não exigiria a existência do construtor copy / move. É por isso que std::atomic<int> a = 1;está tudo bem no C ++ 17, mas não antes.
Daniel Langr

Respostas:


246

Atualização C ++ 17

No C ++ 17, o significado de A_factory_func()mudou de criar um objeto temporário (C ++ <= 14) para apenas especificar a inicialização de qualquer objeto para o qual essa expressão seja inicializada (falando livremente) no C ++ 17. Esses objetos (chamados "objetos de resultado") são as variáveis ​​criadas por uma declaração (como a1), objetos artificiais criados quando a inicialização acaba sendo descartada ou se um objeto é necessário para a ligação de referência (como, por exemplo A_factory_func();. No último caso, um objeto é criado artificialmente, chamado "materialização temporária", porqueA_factory_func() não possui uma variável ou referência que exigiria a existência de um objeto).

Como exemplos em nosso caso, no caso de a1e a2regras especiais dizem que em tais declarações, o objeto de resultado de um inicializador de prvalor do mesmo tipo que a1é variável a1e, portanto, A_factory_func()inicializa diretamente o objeto a1. Qualquer A_factory_func(another-prvalue)conversão intermediária no estilo funcional não teria nenhum efeito, porque apenas "passa" pelo objeto de resultado do valor externo para ser também o objeto de resultado do valor interno.


A a1 = A_factory_func();
A a2(A_factory_func());

Depende do tipo que A_factory_func()retorna. Suponho que ele retorne um A- e faça o mesmo - exceto que, quando o construtor de cópias for explícito, o primeiro falhará. Leia 8.6 / 14

double b1 = 0.5;
double b2(0.5);

Isso está fazendo o mesmo porque é um tipo interno (isso significa que não é um tipo de classe aqui). Leia 8.6 / 14 .

A c1;
A c2 = A();
A c3(A());

Isso não está fazendo o mesmo. O primeiro padrão inicializa se Afor um não-POD e não inicializa para um POD (Leia 8.6 / 9 ). A segunda cópia inicializa: O valor inicializa um temporário e depois copia esse valor para c2(Leia 5.2.3 / 2 e 8.6 / 14 ). Obviamente, isso exigirá um construtor de cópias não explícito (Leia 8.6 / 14 e 12.3.1 / 3 e 13.3.1.3/1 ). O terceiro cria uma declaração de função para uma função c3que retorna Ae que leva um ponteiro de função para uma função que retorna a A(Leia 8.2 ).


Explorando as Inicializações Direta e Inicialização de Cópia

Embora pareçam idênticos e devam fazer o mesmo, essas duas formas são notavelmente diferentes em certos casos. As duas formas de inicialização são diretas e copiam a inicialização:

T t(x);
T t = x;

Existe um comportamento que podemos atribuir a cada um deles:

  • A inicialização direta se comporta como uma chamada de função para uma função sobrecarregada: As funções, neste caso, são os construtores de T(incluindo explicituns) e o argumento éx . A resolução de sobrecarga encontrará o melhor construtor correspondente e, quando necessário, fará qualquer conversão implícita necessária.
  • A inicialização de cópia constrói uma sequência implícita de conversão: tenta converter xem um objeto do tipo T. (Ele pode copiar esse objeto para o objeto inicializado, portanto, também é necessário um construtor de cópias - mas isso não é importante abaixo)

Como você vê, a inicialização de cópia é de alguma forma parte da inicialização direta em relação a possíveis conversões implícitas: Embora a inicialização direta tenha todos os construtores disponíveis para chamada e , além disso, possa fazer qualquer conversão implícita necessária para corresponder aos tipos de argumento, inicie a cópia pode apenas configurar uma sequência de conversão implícita.

Eu tentei muito e consegui o código a seguir para gerar texto diferente para cada um desses formulários , sem usar o "óbvio" por meio de explicitconstrutores.

#include <iostream>
struct B;
struct A { 
  operator B();
};

struct B { 
  B() { }
  B(A const&) { std::cout << "<direct> "; }
};

A::operator B() { std::cout << "<copy> "; return B(); }

int main() { 
  A a;
  B b1(a);  // 1)
  B b2 = a; // 2)
}
// output: <direct> <copy>

Como funciona e por que gera esse resultado?

  1. Inicialização direta

    Primeiro, ele não sabe nada sobre conversão. Apenas tentará chamar um construtor. Nesse caso, o seguinte construtor está disponível e é uma correspondência exata :

    B(A const&)

    Não há conversão, muito menos uma conversão definida pelo usuário, necessária para chamar esse construtor (observe que nenhuma conversão de qualificação const também acontece aqui). E assim a inicialização direta o chamará.

  2. Inicialização de cópia

    Como dito acima, a inicialização de cópia construirá uma sequência de conversão quando anão tiver um tipo Bou derivado (o que é claramente o caso aqui). Portanto, ele procurará maneiras de fazer a conversão e encontrará os seguintes candidatos

    B(A const&)
    operator B(A&);

    Observe como eu reescrevi a função de conversão: O tipo de parâmetro reflete o tipo do thisponteiro, que em uma função de membro não-const é não-const. Agora, chamamos esses candidatos de xcomo argumento. O vencedor é a função de conversão: porque, se tivermos duas funções candidatas, ambas aceitando uma referência para o mesmo tipo, menos const versão vence (a propósito, também é o mecanismo que prefere a função membro não const que exige objetos -const).

    Observe que, se mudarmos a função de conversão para uma função membro const, a conversão será ambígua (porque ambos têm um tipo de parâmetro A const&): O compilador do Comeau a rejeita corretamente, mas o GCC a aceita no modo não pedante. Ao mudar para, -pedantictambém gera o aviso de ambiguidade apropriado.

Espero que isso ajude um pouco a esclarecer como essas duas formas diferem!


Uau. Eu nem percebi a declaração da função. Eu praticamente tenho que aceitar sua resposta apenas por ser o único a saber sobre isso. Existe uma razão para que as declarações de função funcionem dessa maneira? Seria melhor se c3 fosse tratado de maneira diferente dentro de uma função.
rlbond

4
Bah, desculpe pessoal, mas eu tive que remover meu comentário e publicá-lo novamente, por causa do novo mecanismo de formatação: é porque nos parâmetros de função R() == R(*)()e T[] == T*. Ou seja, tipos de função são tipos de ponteiro de função e tipos de matriz são tipos de ponteiro para elemento. Isso é péssimo. Ele pode ser contornado por A c3((A()));(parens em torno da expressão).
Johannes Schaub - litb 9/07/09

4
Posso perguntar o que significa "'Ler 8.5 / 14'"? A que isso se refere? Um livro? Um capítulo? Um website?
AZP

9
O @AzP, muitas pessoas no SO, muitas vezes querem referências às especificações do C ++, e foi o que fiz aqui, em resposta à solicitação da rlbond "Por favor, cite o texto como prova". Eu não quero citar as especificações, pois isso incha a minha resposta e é muito mais trabalho para manter-se atualizado (redundância).
Johannes Schaub - litb 29/09

1
@luca eu recomendo para iniciar uma nova questão para que para que outros possam beneficiar a resposta pessoas dão aswell
Johannes Schaub - litb

49

A atribuição é diferente da inicialização .

As duas linhas a seguir inicializam . Uma única chamada de construtor é feita:

A a1 = A_factory_func();  // calls copy constructor
A a1(A_factory_func());   // calls copy constructor

mas não é equivalente a:

A a1;                     // calls default constructor
a1 = A_factory_func();    // (assignment) calls operator =

No momento, não tenho um texto para provar isso, mas é muito fácil experimentar:

#include <iostream>
using namespace std;

class A {
public:
    A() { 
        cout << "default constructor" << endl;
    }

    A(const A& x) { 
        cout << "copy constructor" << endl;
    }

    const A& operator = (const A& x) {
        cout << "operator =" << endl;
        return *this;
    }
};

int main() {
    A a;       // default constructor
    A b(a);    // copy constructor
    A c = a;   // copy constructor
    c = b;     // operator =
    return 0;
}

2
Boa referência: "A linguagem de programação C ++, edição especial" de Bjarne Stroustrup, seção 10.4.4.1 (página 245). Descreve a inicialização e a atribuição de cópias e por que elas são fundamentalmente diferentes (embora ambos usem o operador = como sintaxe).
Naaff

Nit menor, mas eu realmente não gosto quando as pessoas dizem que "A a (x)" e "A a = x" são iguais. Estritamente não são. Em muitos casos, eles farão exatamente a mesma coisa, mas é possível criar exemplos em que, dependendo do argumento, na verdade, diferentes construtores são chamados.
Richard Corden

Não estou falando de "equivalência sintática". Semanticamente, as duas formas de inicialização são as mesmas.
Mehrdad Afshari

@MehrdadAfshari No código de resposta de Johannes, você obtém resultados diferentes com base em qual dos dois você usa.
Brian Gordon

1
@BrianGordon Sim, você está certo. Eles não são equivalentes. Eu havia abordado o comentário de Richard em minha edição há muito tempo.
Mehrdad Afshari

22

double b1 = 0.5; é chamada implícita do construtor.

double b2(0.5); é uma chamada explícita.

Veja o código a seguir para ver a diferença:

#include <iostream>
class sss { 
public: 
  explicit sss( int ) 
  { 
    std::cout << "int" << std::endl;
  };
  sss( double ) 
  {
    std::cout << "double" << std::endl;
  };
};

int main() 
{ 
  sss ddd( 7 ); // calls int constructor 
  sss xxx = 7;  // calls double constructor 
  return 0;
}

Se a sua classe não tiver construtores explícitos, as chamadas explícitas e implícitas são idênticas.


5
+1. Boa resposta. É bom observar também a versão explícita. A propósito, é importante observar que você não pode ter as duas versões de um único construtor sobrecarregadas ao mesmo tempo. Portanto, apenas falharia em compilar no caso explícito. Se os dois compilarem, terão que se comportar da mesma forma.
Mehrdad Afshari

4

Primeiro agrupamento: depende do que A_factory_funcretorna. A primeira linha é um exemplo de inicialização de cópia , a segunda linha é de inicialização direta . Se A_factory_funcretornar um Aobjeto, eles serão equivalentes, ambos chamarão o construtor de cópias A; caso contrário, a primeira versão criará um rvalor do tipo a Apartir de um operador de conversão disponível para o tipo de retorno deA_factory_func ou Aconstrutores apropriados e, em seguida, chamará o construtor de cópia para construir a a1partir deste. temporário. A segunda versão tenta encontrar um construtor adequado que aceite qualquerA_factory_func retornos ou que precise de algo em que o valor retornado possa ser implicitamente convertido.

Segundo agrupamento: exatamente a mesma lógica é válida, exceto que os tipos incorporados não possuem construtores exóticos, portanto, na prática, são idênticos.

Terceiro agrupamento: c1é inicializado por padrão, c2é inicializado por cópia a partir de um valor inicializado temporário. Qualquer membro c1desse tipo de pod (ou membros de membros, etc., etc.) poderá não ser inicializado se o usuário fornecer construtores padrão (se houver) não os inicializar explicitamente. Para c2isso, depende se existe um construtor de cópias fornecido pelo usuário e se isso inicializa adequadamente esses membros, mas todos os membros do temporário serão inicializados (inicializado com zero se não inicializado explicitamente). Como litb visto, c3é uma armadilha. Na verdade, é uma declaração de função.


4

De importância:

[12,2 / 1] Temporaries of class type are created in various contexts: ... and in some initializations (8.5).

Ou seja, para inicialização de cópia.

[12,8 / 15] When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...

Em outras palavras, um bom compilador não criará uma cópia para a inicialização da cópia quando puder ser evitada; em vez disso, chamará o construtor diretamente - ou seja, assim como na inicialização direta.

Em outras palavras, a inicialização de cópia é como a inicialização direta na maioria dos casos <opinion> onde código compreensível foi gravado. Como a inicialização direta potencialmente causa conversões arbitrárias (e, portanto, provavelmente desconhecidas), eu prefiro sempre usar a inicialização de cópia sempre que possível. (Com o bônus de que realmente se parece com a inicialização.) </opinion>

Goriness técnico: [12.2 / 1 cont de cima] Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created.

Ainda bem que não estou escrevendo um compilador C ++.


4

Você pode ver sua diferença nos tipos de construtor explicite implicitquando inicializa um objeto:

Aulas :

class A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
};

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};

E na main função:

int main()
{
    A a1 = 1;      // OK: copy-initialization selects A::A(int)
    A a2(2);       // OK: direct-initialization selects A::A(int)
    A a3 {4, 5};   // OK: direct-list-initialization selects A::A(int, int)
    A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
    A a5 = (A)1;   // OK: explicit cast performs static_cast

//  B b1 = 1;      // error: copy-initialization does not consider B::B(int)
    B b2(2);       // OK: direct-initialization selects B::B(int)
    B b3 {4, 5};   // OK: direct-list-initialization selects B::B(int, int)
//  B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
    B b5 = (B)1;   // OK: explicit cast performs static_cast
}

Por padrão, um construtor é implicitassim, você tem duas maneiras de inicializá-lo:

A a1 = 1;        // this is copy initialization
A a2(2);         // this is direct initialization

E definindo uma estrutura como explicitapenas uma maneira direta:

B b2(2);        // this is direct initialization
B b5 = (B)1;    // not problem if you either use of assign to initialize and cast it as static_cast

3

Respondendo em relação a esta parte:

A c2 = A (); A c3 (A ());

Como a maioria das respostas é anterior ao c ++ 11, estou adicionando o que o c ++ 11 tem a dizer sobre isso:

Um especificador de tipo simples (7.1.6.2) ou especificador de nome de tipo (14.6) seguido por uma lista de expressões entre parênteses constrói um valor do tipo especificado, dada a lista de expressões. Se a lista de expressões for uma expressão única, a expressão de conversão de tipo será equivalente (em definição e se definida em significado) à expressão de conversão correspondente (5.4). Se o tipo especificado for um tipo de classe, o tipo de classe deve estar completo. Se a lista de expressões especificar mais de um valor único, o tipo deve ser uma classe com um construtor adequadamente declarado (8.5, 12.1) e a expressão T (x1, x2, ...) é equivalente em efeito à declaração T t (x1, x2, ...); para algumas variáveis ​​temporárias inventadas t, com o resultado sendo o valor de t como um pré-valor.

Portanto, a otimização ou não são equivalentes conforme o padrão. Observe que isso está de acordo com o que outras respostas mencionaram. Apenas citando o que o padrão tem a dizer por uma questão de correção.


Nenhuma das "listas de expressões de seus exemplos" especifica mais que um único valor ". Como isso é relevante?
Underscore_d

0

Muitos desses casos estão sujeitos à implementação de um objeto, por isso é difícil fornecer uma resposta concreta.

Considere o caso

A a = 5;
A a(5);

Nesse caso, assumindo um operador de atribuição adequado e um construtor de inicialização que aceite um único argumento inteiro, a maneira como implemento os referidos métodos afeta o comportamento de cada linha. No entanto, é uma prática comum que um deles chame o outro na implementação, a fim de eliminar o código duplicado (embora, em um caso tão simples como esse, não haja um objetivo real).

Editar: Como mencionado em outras respostas, a primeira linha de fato chamará o construtor de cópia. Considere os comentários relacionados ao operador de atribuição como comportamento pertencente a uma atribuição independente.

Dito isto, como o compilador otimiza o código terá seu próprio impacto. Se eu tiver o construtor de inicialização chamando o operador "=" - se o compilador não fizer otimizações, a linha superior executará 2 saltos em oposição a um na linha inferior.

Agora, para as situações mais comuns, seu compilador otimizará esses casos e eliminará esse tipo de ineficiência. Então, efetivamente, todas as diferentes situações que você descreve serão as mesmas. Se você quiser ver exatamente o que está sendo feito, consulte o código do objeto ou uma saída de montagem do seu compilador.


Não é uma otimização . O compilador deve chamar o construtor da mesma forma nos dois casos. Como resultado, nenhum deles será compilado se você tiver operator =(const int)e não A(const int). Veja a resposta de @jia3ep para mais detalhes.
Mehrdad Afshari

Eu acredito que você está correto, na verdade. No entanto, ele compilará muito bem usando um construtor de cópia padrão.
22410 dborba

Além disso, como mencionei, é prática comum que um construtor de cópias chame um operador de atribuição, quando as otimizações do compilador entram em cena.
22410 dborba

0

Isto é da linguagem de programação C ++ de Bjarne Stroustrup:

Uma inicialização com um = é considerada uma inicialização de cópia . Em princípio, uma cópia do inicializador (o objeto do qual estamos copiando) é colocada no objeto inicializado. No entanto, essa cópia pode ser otimizada (elided) e uma operação de movimentação (baseada na semântica de movimentação) pode ser usada se o inicializador for um rvalor. Deixar o = torna a inicialização explícita. Inicialização explícita é conhecida como inicialização direta .

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.