Kopya başlatma ve doğrudan başlatma arasında bir fark var mı?


244

Bu işleve sahip olduğumu varsayalım:

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

Her grupta bu ifadeler aynı mıdır? Veya bazı başlatmalarda fazladan (muhtemelen optimize edilebilir) bir kopya var mı?

İnsanların her ikisini de söylediğini gördüm. Lütfen alıntı kanıtı olarak metni. Ayrıca başka durumlar da ekleyin.


1
Ve @JohannesSchaub - tarafından tartışılan dördüncü durum var A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum

1
Sadece 2018 notu: C ++ 17'de kurallar değişti , bakınız, örneğin, burada . Anlayışım doğruysa, C ++ 17'de her iki ifade de aynıdır (copy ctor açık olsa bile). Ayrıca, init ifadesi başka tipte Aolursa, kopya başlatma, kopyalama / taşıma yapıcısının bulunmasını gerektirmez. Bu yüzden std::atomic<int> a = 1;C ++ 17'de tamam ama daha önce değil.
Daniel Langr

Yanıtlar:


246

C ++ 17 Güncellemesi

C ++ 17'de, A_factory_func()geçici bir nesne (C ++ <= 14) oluşturmanın anlamı, bu ifadenin C ++ 17'de başlatıldığı (gevşekçe) herhangi bir nesnenin başlatılmasını belirlemeye kadar değişti. Bu nesneler ("sonuç nesneleri" olarak adlandırılır), bir bildirim tarafından oluşturulan değişkenlerdir ( a1başlatma gibi ), başlatma işlemi sona erdiğinde oluşturulan yapay nesneler veya referans ciltleme için bir nesne gerekiyorsa (örneğin, içinde A_factory_func();. Son durumda, "geçici materyalizasyon" adı verilen bir nesne yapay olarak oluşturulur, çünkü A_factory_func()aksi takdirde bir nesnenin var olmasını gerektiren bir değişken veya referansa sahip değildir).

Bizim durumumuzda örnek olarak, özel kurallar a1ve a2özel kurallar söz konusu olduğunda, bu tür bildirimlerde, a1değişkenle aynı türdeki bir başlangıç ​​başlatıcısının sonuç nesnesinin değişken olduğunu a1ve dolayısıyla A_factory_func()nesneyi doğrudan başlattığını söyler a1. Herhangi bir işlevsel işlevsel tarzdaki dökümün herhangi bir etkisi olmayacaktır, çünkü A_factory_func(another-prvalue)sadece dış ön değerin sonuç nesnesi, aynı zamanda iç ön değerin sonuç nesnesi olarak "geçer".


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

Hangi türün A_factory_func()döndüğüne bağlıdır. AKopya oluşturucu açık olduğunda, o zaman birincisi başarısız olur dışında - o zaman aynı yapıyor - varsayalım . Oku 8.6 / 14

double b1 = 0.5;
double b2(0.5);

Bu aynı şeyi yapıyor çünkü yerleşik bir tip (burada bir sınıf tipi değil). 8.6 / 14'ü okuyun .

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

Bu aynı şeyi yapmıyor. İlk varsayılan A, POD olmayan bir değer olup olmadığını başlatır ve POD için herhangi bir başlatma yapmaz (Okuma 8.6 / 9 ). İkinci kopya başlatılır: Değer bir geçici değeri başlatır ve sonra bu değeri c2(Read 5.2.3 / 2 ve 8.6 / 14 ) içine kopyalar . Bu tabii ki açık olmayan bir kopya oluşturucu gerektirecektir (Read 8.6 / 14 and 12.3.1 / 3 and 13.3.1.3/1 ). Üçüncüsü, bir işlevi c3döndüren Ave A( geri 8.2 ) döndüren bir işleve bir işlev işaretçisi alan bir işlev için işlev bildirimi oluşturur .


Başlatmalara Doğrudan Ayrılma ve Kopyalama başlatma

Aynı görünseler ve aynı şeyi yapmaları gerekiyorsa da, bu iki form bazı durumlarda oldukça farklıdır. İki başlatma şekli doğrudan ve kopya başlatmadır:

T t(x);
T t = x;

Her birine atfedebileceğimiz davranışlar var:

  • Doğrudan başlatma, aşırı yüklenmiş bir işleve bir işlev çağrısı gibi davranır: Bu durumda işlevler, T( explicitolanlar dahil ) yapıcılarıdır ve argüman x. Aşırı yük çözünürlüğü, en iyi eşleşen yapıcıyı bulur ve gerektiğinde örtük dönüştürme gerekir.
  • Kopya başlatma işlemi, örtük bir dönüştürme sırası oluşturur: xTür nesnesine dönüştürmeye çalışır T. (Daha sonra bu nesnenin üzerinden başlatılan nesneye kopyalanabilir, bu nedenle bir kopya oluşturucu da gereklidir - ancak bu aşağıda önemli değildir)

Gördüğünüz gibi, kopya başlatma , bir şekilde olası örtük dönüşümlerle ilgili doğrudan başlatmanın bir parçasıdır: Doğrudan başlatma, tüm kurucuları arayabilir ve buna ek olarak , argüman türlerini eşleştirmek için gereken örtük dönüşümü yapabilir, kopya başlatma yalnızca bir örtülü dönüşüm dizisi oluşturabilir.

Ben sert çalıştı ve yapıcılar aracılığıyla "bariz" kullanmadan, bu formların her biri için farklı metin çıkışı için aşağıdaki kodu aldımexplicit .

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

Nasıl çalışır ve bu sonucu neden çıkarır?

  1. Doğrudan başlatma

    İlk önce dönüşüm hakkında hiçbir şey bilmiyor. Sadece bir kurucu çağırmaya çalışacaktır. Bu durumda, aşağıdaki kurucu kullanılabilir ve tam eşleşir :

    B(A const&)

    Bu kurucuyu çağırmak için gerekli olan, daha az kullanıcı tanımlı bir dönüşüm yoktur (burada herhangi bir nitelik yeterlilik dönüşümü de gerçekleşmez). Ve böylece doğrudan başlatma bunu çağırır.

  2. Kopya başlatma

    Gibi, başlatma bir dönüştürme sekansı inşa edecek kopyalama yukarıda adı geçen atip değil B(burada açık bir şekilde olduğu) ya da bundan türetilen. Bu yüzden, dönüşüm yapmanın yollarını arayacak ve aşağıdaki adayları bulacak

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

    Dönüştürme işlevini nasıl yeniden yazdığıma dikkat edin: Parametre türü this, sabit olmayan üye işlevinde sabit olmayan işaretçi türünü yansıtır . Şimdi bu adayları xargüman olarak adlandırıyoruz. Kazanan dönüşüm fonksiyonudur: Çünkü aynı tipe referansı kabul eden iki aday fonksiyonumuz varsa, o zaman daha az const versiyonu kazanır (bu arada, const olmayan üye fonksiyonunu tercih eden mekanizma da -sık nesneler).

    Dönüştürme işlevini sabit üye işlevi olarak değiştirirsek, dönüştürme belirsizdir (çünkü her ikisinde de bir parametre türü vardır A const&): Comeau derleyicisi bunu düzgün bir şekilde reddeder, ancak GCC bunu bilgiçlik dışı modda kabul eder. Buna geçmek -pedantic, bunun da uygun belirsizlik uyarısı vermesini sağlar.

Umarım bu, bu iki formun nasıl farklılaştığını daha net hale getirmeye yardımcı olur!


Vay. İşlev beyanı hakkında bile bir fikrim yoktu. Sadece bunu bilen tek kişi olduğun için cevabını kabul etmek zorundayım. İşlev bildirimlerinin bu şekilde çalışması için bir neden var mı? C3 bir fonksiyon içinde farklı muamele görürse daha iyi olur.
rlbond

4
Bah, üzgünüm millet, ama yeni biçimlendirme motoru nedeniyle yorumumu kaldırmak ve tekrar göndermek zorunda kaldı: Çünkü fonksiyon parametreleri, R() == R(*)()ve T[] == T*. Yani, işlev türleri işlev işaretçisi türleridir ve dizi türleri işaretçi-öğe türleridir. Bu berbat. Etrafında A c3((A()));çözümlenebilir (ifadenin etrafındaki parensler).
Johannes Schaub - litb

4
"" 8.5 / 14'ü Oku "nun ne anlama geldiğini sorabilir miyim? Bu ne anlama geliyor? Kitap? Bir bölüm? Bir internet sitesi?
AzP

9
@AzP SO birçok millet genellikle C ++ spec referanslar istiyorum ve rlbond isteği "Lütfen metni kanıt olarak alıntı." Spesifikasyonu belirtmek istemiyorum, çünkü bu cevabımı şişiriyor ve güncel tutmak için çok daha fazla iş (yedeklilik).
Johannes Schaub - litb

1
@luca yeni bir soru başlatmak için tavsiye böylece insanlar aswell yanıtı verebilirsiniz
Johannes Schaub - litb

49

Atama , başlatma işleminden farklı .

Aşağıdaki satırların her ikisi de başlatma yapar . Tek bir yapıcı çağrısı yapılır:

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

ancak şuna eşdeğer değildir:

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

Şu anda bunu kanıtlayacak bir metnim yok ama denemek çok kolay:

#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
İyi referans: Bjarne Stroustrup'un "C ++ Programlama Dili, Özel Sürüm", bölüm 10.4.4.1 (sayfa 245). Kopya başlatma ve kopyalama atamalarını ve neden temel olarak farklı olduklarını açıklar (her ikisi de = işlecini sözdizimi olarak kullanır).
Naaff

Küçük nit, ama insanlar "A a (x)" ve "A a = x" eşit olduğunu söylediğinde gerçekten sevmiyorum. Kesinlikle öyle değiller. Birçok durumda tam olarak aynı şeyi yaparlar, ancak farklı kurucuların gerçekten çağrıldığı argümanına bağlı olarak örnekler oluşturmak mümkündür.
Richard Corden

"Sözdizimsel denklik" ten bahsetmiyorum. Anlamsal olarak, her iki başlatma yöntemi de aynıdır.
Mehrdad Afshari

@MehrdadAfshari Johannes'in yanıt kodunda, hangisini kullandığınıza bağlı olarak farklı çıktılar elde edersiniz.
Brian Gordon

1
@BrianGordon Evet, haklısın. Eşdeğer değiller. Uzun zaman önce düzenlememde Richard'ın yorumuna değinmiştim.
Mehrdad Afshari

22

double b1 = 0.5; örtük yapıcı çağrısıdır.

double b2(0.5); açık çağrıdır.

Farkı görmek için aşağıdaki koda bakın:

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

Sınıfınızın açık ve örtük çağrıları yoksa, açık ve örtülü çağrılar aynıdır.


5
+1. Güzel cevap. Açık sürümü de not etmek güzel. Bu arada, tek bir yapıcı aşırı yüklemesinin her iki sürümüne de aynı anda sahip olamayacağınızı not etmek önemlidir . Yani, açık bir durumda derlemek başarısız olur. İkisi de derlerse, benzer şekilde davranmaları gerekir.
Mehrdad Afshari

4

İlk gruplama: Neyin A_factory_funcgeri döndüğüne bağlıdır . İlk satır kopya başlatmaya bir örnek , ikinci satır doğrudan başlatmaya bir örnektir . Eğer A_factory_funcgetiriler bir Ao zaman eşdeğerdir nesne, her ikisi çağrı için kopyalama yapıcısı A, aksi ilk sürümü türünde bir rvalue yaratır Adönüş türü için kullanılabilir bir dönüşüm operatörleri A_factory_funcveya uygun Akurucular ve ardından yapı için kopya kurucu çağırır a1bundan geçici. İkinci sürüm, ne A_factory_funcdöndürürse onu döndüren veya döndürme değerinin örtük olarak dönüştürülebileceği bir şey alan uygun bir kurucu bulmaya çalışır .

İkinci gruplandırma: tam olarak aynı mantık bekletilir, ancak yerleşik türlerin herhangi bir egzotik kurucuları yoktur, bu yüzden pratikte aynıdırlar.

Üçüncü gruplama: c1varsayılan olarak başlatılır, c2geçici olarak başlatılan bir değerden kopyala başlatılır. Herhangi üyeleri c1kullanıcı kaynaklı varsayılan kurucular (varsa) onları açıkça başlatmak yoksa başlatılmamış olabilir (vs vs veya üyelerin üyeleri,) pod-türü var. Çünkü c2, kullanıcı tarafından sağlanan bir kopya oluşturucu olup olmadığına ve bunun bu üyeleri uygun şekilde başlatıp başlatmamasına, ancak geçici üyelerin tümü başlatılır (aksi açıkça belirtilmezse sıfır başlatılır) bağlıdır. LITB benekli gibi, c3bir tuzak. Aslında bir işlev bildirimi.


4

Notun:

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

Yani, kopya başlatma için.

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

Diğer bir deyişle, iyi bir derleyici olacak değil o önlenebilir kopya başlatılması için bir kopyasını oluşturmak; bunun yerine sadece kurucuya doğrudan çağrı yapacaktır - yani, doğrudan başlatma için olduğu gibi.

Diğer bir deyişle, kopya başlatma, çoğu durumda anlaşılabilir kodun yazıldığı <opinion> öğesinde doğrudan başlatma gibidir. Doğrudan başlatma potansiyel olarak isteğe bağlı (ve muhtemelen bilinmeyen) dönüşümlere neden olduğundan, mümkün olduğunda her zaman kopya başlatmayı kullanmayı tercih ederim. (Aslında bonusun başlatılmaya benzediği gibi.) </opinion>

Teknik goriness: [Yukarıdan 12.2 / 1 devamı] 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.

Sevindim C ++ derleyicisi yazmıyorum.


4

Bir nesneyi başlattığınızda farkını explicitve implicityapıcı türlerini görebilirsiniz :

Sınıflar:

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

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

Ve main fonksiyonda:

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
}

Varsayılan olarak, bir yapıcı, implicitonu başlatmak için iki yolunuz vardır:

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

Ve bir yapıyı explicitdoğrudan tanımlamanın bir yolu var:

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

Bu bölümle ilgili cevaplar:

A c2 = A (); C3 (A ());

Cevapların çoğu pre-c ++ 11 olduğundan c ++ 11'in bu konuda ne söyleyeceğini ekliyorum:

Basit tür belirteci (7.1.6.2) veya tür adı belirteci (14.6) ve ardından parantez içinde ifade listesi, ifade listesi verildiğinde belirtilen türde bir değer oluşturur. İfade listesi tek bir ifadeyse, tür dönüştürme ifadesi karşılık gelen döküm ifadesine (5.4) eşdeğerdir (tanım olarak ve anlam olarak tanımlanmışsa). Belirtilen tip bir sınıf tipiyse, sınıf tipi eksiksiz olmalıdır. İfade listesi tek bir değerden daha fazlasını belirtiyorsa, tür uygun olarak bildirilmiş bir yapıcıya (8.5, 12.1) sahip bir sınıf olmalı ve T (x1, x2, ...) ifadesi T t bildirimine eşdeğerdir. (x1, x2, ...); bazı icat edilmiş geçici değişken t için, sonuç bir ön değer olarak t değeridir.

Yani optimizasyon ya da değil standart göre eşdeğerdir. Bunun diğer yanıtların belirttiklerine uygun olduğunu unutmayın. Sadece doğruluk uğruna standardın söylediklerinden bahsetmek.


Örneklerinizden hiçbiri "ifade listesi tek bir değerden fazlasını belirtmiyor". Bunlardan herhangi biri nasıl alakalı?
underscore_d

0

Bu vakaların çoğu bir nesnenin uygulanmasına tabidir, bu nedenle size somut bir cevap vermek zordur.

Davayı düşünün

A a = 5;
A a(5);

Bu durumda, uygun bir atama operatörü varsayar ve tek bir tamsayı bağımsız değişkenini kabul eden kurucu başlatırken, adı geçen yöntemleri nasıl uyguladığım her satırın davranışını etkiler. Bununla birlikte, uygulamalardan birinin yinelenen kodu ortadan kaldırmak için diğerini çağırması yaygın bir uygulamadır (ancak bu kadar basit bir durumda gerçek bir amaç olmayacaktır).

Düzenleme: Diğer yanıtlarda belirtildiği gibi, ilk satır aslında kopya yapıcısını çağırır. Bağımsız bir atamaya ilişkin davranış olarak atama operatörü ile ilgili yorumları düşünün.

Bununla birlikte, derleyicinin kodu nasıl optimize ettiği, bunun kendi etkisi olacaktır. "=" İşlecini çağıran başlatıcı yapıcı varsa - derleyici herhangi bir optimizasyon yapmazsa, en üst satır, alt satırdakinin aksine 2 atlama gerçekleştirir.

Şimdi, en yaygın durumlar için, derleyiciniz bu durumlarda optimizasyon yapacak ve bu tür verimsizlikleri ortadan kaldıracaktır. Böylece, tanımladığınız tüm farklı durumlar aynı şekilde ortaya çıkacaktır. Tam olarak ne yapıldığını görmek istiyorsanız, derleyicinizin nesne koduna veya bir montaj çıktısına bakabilirsiniz.


Bu bir optimizasyon değil . Derleyici her iki durumda da yapıcıyı çağırmalıdır. Sonuç olarak, eğer varsa operator =(const int)ve hayır ise hiçbiri derlenmeyecektir A(const int). Daha fazla bilgi için @ jia3ep'in cevabına bakınız.
Mehrdad Afshari

Aslında doğru olduğuna inanıyorum. Ancak, varsayılan bir kopya oluşturucu kullanarak iyi derlenir.
dborba

Ayrıca, daha önce de belirttiğim gibi, bir kopya oluşturucunun atama işlecini çağırması yaygın bir uygulamadır, bu noktada derleyici optimizasyonları devreye girer.
dborba

0

Bjarne Stroustrup'un C ++ Programlama Dili'nden:

= İşaretiyle başlatma, kopya başlatma olarak kabul edilir . Prensip olarak, ilklendiricinin bir kopyası (kopyaladığımız nesne) başlatılan nesneye yerleştirilir. Bununla birlikte, böyle bir kopya uzağa optimize edilebilir (eldik) ve başlatıcı bir değerse, bir taşıma işlemi (taşıma semantiğine dayalı olarak) kullanılabilir. = 'Nin dışarıda bırakılması başlatmayı açık hale getirir. Açık başlatma doğrudan başlatma olarak bilinir .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.