Czy istnieje różnica między inicjowaniem kopii a inicjalizacją bezpośrednią?

244

Załóżmy, że mam tę funkcję:

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

Czy w każdej grupie te instrukcje są identyczne? Czy może istnieje dodatkowa (możliwa do zoptymalizowania) kopia w niektórych inicjalizacjach?

Widziałem, jak ludzie mówią obie rzeczy. Proszę zacytować tekst jako dowodu. Proszę również dodać inne skrzynki.

rlbond
źródło
1
I jest czwarty przypadek omawiany przez @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum
1
Tylko uwaga z 2018 r .: Zasady uległy zmianie w C ++ 17 , patrz np . Tutaj . Jeśli dobrze rozumiem, w C ++ 17 obie instrukcje są w rzeczywistości takie same (nawet jeśli ctor kopiowania jest jawny). Ponadto, jeśli wyrażenie init byłoby innego typu A, inicjalizacja kopii nie wymagałaby istnienia konstruktora kopiowania / przenoszenia. Dlatego std::atomic<int> a = 1;w C ++ 17 jest w porządku, ale nie wcześniej.
Daniel Langr,

Odpowiedzi:

246

Aktualizacja C ++ 17

W C ++ 17 znaczenie A_factory_func()zmieniło się z tworzenia obiektu tymczasowego (C ++ <= 14) na samo określenie inicjalizacji dowolnego obiektu, na który to wyrażenie jest inicjowane (luźno mówiąc) w C ++ 17. Te obiekty (zwane „obiektami wynikowymi”) są zmiennymi utworzonymi przez deklarację (jak a1), sztuczne obiekty utworzone, gdy inicjalizacja kończy się odrzuceniem, lub jeśli obiekt jest potrzebny do powiązania odwołania (jak, w A_factory_func();. W ostatnim przypadku obiekt jest sztucznie tworzony, zwany „materializacją tymczasową”, ponieważA_factory_func() nie ma zmiennej ani odwołania, które w innym przypadku wymagałoby istnienia obiektu).

Jako przykłady w naszym przypadku, w przypadku a1i a2zasad specjalnych, mówi się, że w takich deklaracjach obiekt wynikowy inicjalizatora wartości tego samego typu co a1zmienny a1, a zatem A_factory_func()bezpośrednio inicjuje obiekt a1. Żadna pośrednia obsada w stylu funkcjonalnym nie miałaby żadnego efektu, ponieważ A_factory_func(another-prvalue)po prostu „przechodzi” przez obiekt wynikowy wartości zewnętrznej, która jest również obiektem wynikowym wartości wewnętrznej.


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

Zależy, jaki typ A_factory_func()zwraca. Zakładam, że zwraca A- i robi to samo - z wyjątkiem tego, że gdy konstruktor kopii jest jawny, to pierwszy nie powiedzie się. Przeczytaj 8.6 / 14

double b1 = 0.5;
double b2(0.5);

Robi to samo, ponieważ jest to typ wbudowany (w tym przypadku nie jest to typ klasy). Przeczytaj 8.6 / 14 .

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

To nie robi tego samego. Pierwszy domyślnie inicjuje, jeśli nie Ajest POD, i nie wykonuje żadnej inicjalizacji dla POD (odczyt 8.6 / 9 ). Druga kopia inicjuje: Wartość inicjuje wartość tymczasową, a następnie kopiuje tę wartość do c2(Przeczytaj 5.2.3 / 2 i 8.6 / 14 ). Będzie to oczywiście wymagało jawnego konstruktora kopii ( odczyty 8.6 / 14 i 12.3.1 / 3 i 13.3.1.3/1 ). Trzeci tworzy deklarację funkcji dla funkcji, c3która zwraca an Ai która przenosi wskaźnik funkcji do funkcji zwracającej a A(Odczyt 8.2 ).


Wchodzenie w inicjalizacje Bezpośrednia i kopiowanie inicjalizacja

Mimo że wyglądają identycznie i powinny robić to samo, te dwie formy są bardzo różne w niektórych przypadkach. Dwie formy inicjalizacji to inicjalizacja bezpośrednia i kopiowanie:

T t(x);
T t = x;

Istnieje zachowanie, które możemy przypisać każdemu z nich:

  • Bezpośrednia inicjalizacja zachowuje się jak wywołanie funkcji dla przeciążonej funkcji: Funkcje, w tym przypadku, są konstruktorami T(włączając je explicit), a argumentem jest x. Rozdzielczość przeciążenia znajdzie najlepszego pasującego konstruktora, a gdy zajdzie taka potrzeba, dokona jakiejkolwiek niejawnej konwersji wymaganej.
  • Inicjalizacja kopiowania tworzy niejawną sekwencję konwersji: Próbuje przekonwertować xna obiekt typu T. (Następnie może skopiować ten obiekt do obiektu inicjowanego, więc potrzebny jest również konstruktor kopiowania - ale nie jest to ważne poniżej)

Jak widać, inicjalizacja kopiowania jest w pewnym sensie częścią bezpośredniej inicjalizacji w odniesieniu do możliwych niejawnych konwersji: Chociaż inicjalizacja bezpośrednia ma wszystkie konstruktory do wywołania, a ponadto może wykonać dowolną niejawną konwersję, która musi pasować do typów argumentów, inicjalizacja kopii może po prostu skonfigurować jedną domyślną sekwencję konwersji.

Starałem się i otrzymałem następujący kod, aby wypisać inny tekst dla każdej z tych form , bez użycia „oczywistego” za pomocą explicitkonstruktorów.

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

Jak to działa i dlaczego generuje taki wynik?

  1. Bezpośrednia inicjalizacja

    Najpierw nic nie wie o konwersji. Po prostu spróbuje wywołać konstruktora. W takim przypadku dostępny jest następujący konstruktor, który jest dokładnie zgodny :

    B(A const&)

    Do wywołania tego konstruktora nie jest wymagana konwersja, a tym bardziej konwersja zdefiniowana przez użytkownika (zauważ, że tutaj również nie występuje konwersja kwalifikacji stałej). I tak to nazwie bezpośrednia inicjalizacja.

  2. Kopiuj inicjalizację

    Jak powiedziano powyżej, inicjalizacja kopii skonstruuje sekwencję konwersji, jeśli anie zostanie ona wpisana Bani z niej wyprowadzona (co wyraźnie ma miejsce tutaj). Będzie więc szukał sposobów na konwersję i znajdzie następujących kandydatów

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

    Zwróć uwagę, jak przepisałem funkcję konwersji: Typ parametru odzwierciedla typ thiswskaźnika, który w funkcji składowej innej niż const to non-const. Teraz nazywamy tych kandydatów xargumentem. Zwycięzcą jest funkcja konwersji: Ponieważ jeśli mamy dwie funkcje kandydujące, obie akceptują odniesienie do tego samego typu, to mniej const wygrywa wersja (nawiasem mówiąc, jest to również mechanizm, który preferuje funkcje niezaangażowane -konst. obiektów).

    Zauważ, że jeśli zmienimy funkcję konwersji na stałą element członkowski, wówczas konwersja będzie dwuznaczna (ponieważ oba mają typ parametru A const&wtedy): Kompilator Comeau odpowiednio ją odrzuca, ale GCC akceptuje ją w trybie nie-pedantycznym. -pedanticJednak przełączenie na sprawia, że ​​wyświetla również odpowiednie ostrzeżenie o dwuznaczności.

Mam nadzieję, że to pomoże nieco wyjaśnić różnice między tymi dwiema formami!

Johannes Schaub - litb
źródło
Łał. Nawet nie zdawałem sobie sprawy z deklaracji funkcji. Prawie muszę zaakceptować twoją odpowiedź tylko dlatego, że jestem jedynym, który o tym wie. Czy istnieje powód, dla którego deklaracje funkcji działają w ten sposób? Byłoby lepiej, gdyby c3 było traktowane inaczej w funkcji.
rlbond
4
Ach, przepraszam, ale musiałem usunąć mój komentarz i opublikować go ponownie, z powodu nowego silnika formatowania: To dlatego, że w parametrach funkcji R() == R(*)()i T[] == T*. Oznacza to, że typy funkcji są typami wskaźników funkcji, a typy tablic są typami wskaźników do elementów. To jest do bani. Można to obejść A c3((A()));(parens wokół wyrażenia).
Johannes Schaub - litb
4
Czy mogę zapytać, co oznacza „Przeczytaj 8.5 / 14”? Co to dotyczy? Książka? Rozdział? Strona internetowa?
AzP
9
@AzP wielu ludzi na SO często chce odwoływać się do specyfikacji C ++, i to właśnie zrobiłem tutaj, w odpowiedzi na prośbę rlbond „Proszę cytować tekst jako dowód”. Nie chcę cytować specyfikacji, ponieważ przesadza z moją odpowiedzią i jest o wiele więcej pracy, aby być na bieżąco (redundancja).
Johannes Schaub - litb
1
@luca Polecam, aby rozpocząć nowe pytanie, aby inni mogli skorzystać z odpowiedzi udzielanej przez innych
Johannes Schaub - lit
49

Przypisanie różni się od inicjalizacji .

Obie poniższe linie wykonują inicjalizację . Wykonywane jest pojedyncze wywołanie konstruktora:

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

ale nie jest to równoważne z:

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

W tej chwili nie mam tekstu, aby to udowodnić, ale bardzo łatwo jest eksperymentować:

#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;
}
Mehrdad Afshari
źródło
2
Dobra referencja: „The C ++ Programming Language, Special Edition” Bjarne Stroustrup, sekcja 10.4.4.1 (strona 245). Opis inicjalizacji kopiowania i przypisywania kopii oraz dlaczego są one zasadniczo różne (chociaż oba używają operatora = jako składni).
Naaff
Drobna nit, ale naprawdę nie lubię, gdy ludzie mówią, że „A a (x)” i „A a = x” są równe. Ściśle nie są. W wielu przypadkach zrobią dokładnie to samo, ale możliwe jest tworzenie przykładów, w których w zależności od argumentu wywoływane są różne konstruktory.
Richard Corden
Nie mówię o „równoważności składniowej”. Semantycznie oba sposoby inicjalizacji są takie same.
Mehrdad Afshari
@MehrdadAfshari W kodzie odpowiedzi Johannesa otrzymujesz różne dane wyjściowe w zależności od tego, którego z nich używasz.
Brian Gordon,
1
@BrianGordon Tak, masz rację. Nie są równoważne. Już dawno wypowiedziałem się w komentarzu Richarda.
Mehrdad Afshari
22

double b1 = 0.5; jest niejawnym wywołaniem konstruktora.

double b2(0.5); jest wyraźnym połączeniem.

Spójrz na następujący kod, aby zobaczyć różnicę:

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

Jeśli twoja klasa nie ma jawnych struktur wewnętrznych, jawne i niejawne wywołania są identyczne.

Kirill V. Lyadvinsky
źródło
5
+1. Niezła odpowiedź. Warto również zauważyć wyraźną wersję. Nawiasem mówiąc, należy zauważyć, że nie można jednocześnie przeciążać obu wersji jednego konstruktora. Tak więc po prostu nie skompilowałby się w jawnym przypadku. Jeśli oba kompilują się, muszą zachowywać się podobnie.
Mehrdad Afshari
4

Pierwsze grupowanie: zależy od tego, co A_factory_funczwraca. Pierwszy wiersz to przykład inicjalizacji kopii , drugi wiersz to bezpośrednia inicjalizacja . Jeśli A_factory_funczwraca Aobiekt, wówczas są one równoważne, oba wywołują konstruktor kopiujący A, w przeciwnym razie pierwsza wersja tworzy wartość typu Az dostępnych operatorów konwersji dla typu zwracanego A_factory_funclub odpowiednich Akonstruktorów, a następnie wywołuje konstruktor kopiujący, aby skonstruować a1z tego chwilowy. Druga wersja próbuje znaleźć odpowiedniego konstruktora, który odbierze dowolne A_factory_funczwroty lub coś, na co wartość domyślna może zostać niejawnie przekonwertowana.

Druga grupa: dokładnie taka sama logika, z tym wyjątkiem, że typy wbudowane nie mają żadnych egzotycznych konstruktorów, więc są w praktyce identyczne.

Trzecie grupowanie: c1jest inicjowane domyślnie, c2jest inicjowane kopiowaniem z wartości zainicjowanej tymczasowo. Dowolni członkowie c1tego typu pod ((członkowie członków itp. Itp.)) Nie mogą zostać zainicjowani, jeśli użytkownik podał domyślny konstruktor (jeśli taki istnieje) nie inicjuje go jawnie. Ponieważ c2zależy to od tego, czy istnieje dostarczony przez użytkownika konstruktor kopii i czy odpowiednio inicjuje on te elementy, ale wszystkie elementy tymczasowe zostaną zainicjowane (inicjowane zerowo, jeśli inaczej nie zostaną wyraźnie zainicjowane). Jak zauważył litb, c3jest pułapką. To właściwie deklaracja funkcji.

CB Bailey
źródło
4

Nutowy:

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

Tj. Do inicjalizacji kopii.

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

Innymi słowy, dobry kompilator nie utworzy kopii w celu zainicjowania kopii, gdy można tego uniknąć; zamiast tego po prostu wywoła konstruktor bezpośrednio - tj. podobnie jak w przypadku bezpośredniej inicjalizacji.

Innymi słowy, inicjalizacja kopiowania jest jak inicjalizacja bezpośrednia w większości przypadków <opinion>, gdzie napisano zrozumiały kod. Ponieważ bezpośrednia inicjalizacja może potencjalnie powodować dowolne (i dlatego prawdopodobnie nieznane) konwersje, wolę zawsze używać inicjalizacji kopiowania, gdy jest to możliwe. (Z premią, że faktycznie wygląda jak inicjalizacja.) </opinion>

Techniczna gorliwość: [12,2 / 1 cd z góry] 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.

Cieszę się, że nie piszę kompilatora C ++.

John H.
źródło
4

Można zobaczyć jego różnicę expliciti implicitrodzajów konstruktorów podczas inicjalizacji obiektu:

Klasy:

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

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

I w main funkcji:

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
}

Domyślnie konstruktor jest implicittaki, że można go zainicjować na dwa sposoby:

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

A poprzez zdefiniowanie struktury jako explicittylko jeden sposób jest bezpośredni:

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
BattleTested
źródło
3

Odpowiedzi dotyczące tej części:

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

Ponieważ większość odpowiedzi jest wcześniejszych niż c ++ 11, dodaję co c ++ 11 ma do powiedzenia na ten temat:

Specyfikator typu prostego (7.1.6.2) lub specyfikator nazwy typu (14.6), a następnie lista wyrażeń w nawiasach konstruuje wartość określonego typu na podstawie listy wyrażeń. Jeśli lista wyrażeń jest pojedynczym wyrażeniem, wyrażenie konwersji typu jest równoważne (w zdefiniowaniu i jeśli zdefiniowano w znaczeniu) z odpowiednim wyrażeniem rzutowanym (5.4). Jeżeli określony typ jest typem klasy, typ klasy musi być kompletny. Jeżeli lista wyrażeń określa więcej niż jedną wartość, typ powinien być klasą z odpowiednio zadeklarowanym konstruktorem (8.5, 12.1), a wyrażenie T (x1, x2, ...) jest równoważne z deklaracją T t (x1, x2, ...); dla niektórych wynalezionych zmiennych tymczasowych t, ​​przy czym wynikiem jest wartość t jako wartość.

Optymalizacja, czy nie, są równoważne zgodnie ze standardem. Pamiętaj, że jest to zgodne z tym, o czym wspominały inne odpowiedzi. Wystarczy zacytować to, co norma ma do powiedzenia dla zachowania poprawności.

bashrc
źródło
Żaden z przykładów „lista wyrażeń nie zawiera więcej niż jednej wartości”. Jak to ma znaczenie?
underscore_d
0

Wiele z tych przypadków podlega implementacji obiektu, więc trudno jest podać konkretną odpowiedź.

Rozważ przypadek

A a = 5;
A a(5);

W tym przypadku, zakładając właściwy operator przypisania i konstruktor inicjujący, który akceptuje pojedynczy argument liczby całkowitej, sposób, w jaki implementuję wspomniane metody, wpływa na zachowanie każdej linii. Jednak powszechną praktyką jest wywoływanie jednego z nich w implementacji w celu wyeliminowania duplikatu kodu (chociaż w tak prostym przypadku nie byłoby to prawdziwym celem).

Edycja: Jak wspomniano w innych odpowiedziach, pierwszy wiersz w rzeczywistości wywoła konstruktora kopiowania. Rozważ komentarze dotyczące operatora przypisania jako zachowania dotyczące samodzielnego przypisania.

To powiedziawszy, jak kompilator zoptymalizuje kod, będzie miał wtedy swój własny wpływ. Jeśli mam konstruktor inicjujący wywołujący operator „=” - jeśli kompilator nie dokonuje optymalizacji, górna linia wykona następnie 2 skoki w przeciwieństwie do jednego w dolnym wierszu.

Teraz, w najczęstszych sytuacjach, twój kompilator zoptymalizuje te przypadki i wyeliminuje tego rodzaju nieefektywności. W efekcie wszystkie opisane sytuacje okażą się takie same. Jeśli chcesz dokładnie zobaczyć, co się dzieje, możesz spojrzeć na kod obiektu lub dane wyjściowe zestawu kompilatora.

dborba
źródło
To nie jest optymalizacja . W obu przypadkach kompilator musi wywołać konstruktor. W rezultacie żaden z nich nie zostanie skompilowany, jeśli po prostu masz operator =(const int)i nie A(const int). Aby uzyskać więcej informacji, zobacz odpowiedź @ jia3ep.
Mehrdad Afshari
Uważam, że masz rację. Jednak skompiluje się dobrze przy użyciu domyślnego konstruktora kopiowania.
dborba
Jak już wspomniałem, powszechną praktyką jest wywoływanie przez konstruktora kopii operatora przypisania, w którym to momencie zaczynają działać optymalizacje kompilatora.
dborba
0

Pochodzi z języka programowania C ++ autorstwa Bjarne Stroustrup:

Inicjalizacja za pomocą = jest uważana za inicjalizację kopii . Zasadniczo kopia inicjalizatora (obiekt, z którego kopiujemy) jest umieszczana w inicjowanym obiekcie. Jednak taka kopia może być zoptymalizowana (pomijana), a operacja przenoszenia (oparta na semantyce ruchu) może być użyta, jeśli inicjalizatorem jest wartość. Pozostawienie = powoduje, że inicjalizacja jest jawna. Jawna inicjalizacja jest znana jako bezpośrednia inicjalizacja .

Bharat
źródło