C ++ 11 Tekdüzen Başlatma eski stil sözdiziminin yerine geçiyor mu?


172

C ++ 11'in tek tip başlatılmasının dilin bazı sözdizimsel belirsizliğini çözdüğünü biliyorum, ancak birçok Bjarne Stroustrup'un sunumlarında (özellikle GoingNative 2012 görüşmelerinde olanlar), örnekleri her zaman nesneler oluştururken bu sözdizimini kullanıyor.

Tüm durumlarda tek tip başlatma kullanmanız tavsiye edilir mi? Kodlama stili ve genel kullanım açısından bu yeni özellik için genel yaklaşım ne olmalıdır? Bazı nedenleri nelerdir değil kullanmak?

Aklımda öncelikle benim kullanımım olarak nesne yapmayı düşünüyorum, ancak dikkate alınması gereken başka senaryolar varsa lütfen bana bildirin.


Bu, Programmers.se sitesinde daha iyi tartışılan bir konu olabilir. İyi Öznel tarafa doğru eğimli görünüyor.
Nicol Bolas,

6
@NicolBolas: Öte yandan, mükemmel cevabınız c ++ - faq etiketi için çok iyi bir aday olabilir. Daha önce bu konuda bir açıklama yaptığımızı sanmıyorum.
Matthieu M.

Yanıtlar:


233

Kodlama stili sonuçta özneldir ve önemli performans avantajlarının bundan kaynaklanması pek olası değildir. Ancak, liberal tek tip başlatmanın kullanımından kazandığınızı söyleyeceğim şey:

Yedekli Typenames'i En Aza İndirir

Aşağıdakileri göz önünde bulundur:

vec3 GetValue()
{
  return vec3(x, y, z);
}

Neden vec3iki kez yazmam gerekiyor ? Bunun bir anlamı var mı? Derleyici, fonksiyonun ne döndürdüğünü iyi ve iyi bilir. Neden sadece "bu değerlerle döndüğümün yapıcısını çağırın ve geri döndürün" diyemiyorum? Tek tip başlatma ile şunları yapabilirim:

vec3 GetValue()
{
  return {x, y, z};
}

Herşey çalışıyor.

Daha da iyisi, fonksiyon argümanları içindir. Bunu düşün:

void DoSomething(const std::string &str);

DoSomething("A string.");

Bu bir yazım adı yazmak zorunda kalmadan çalışır, çünkü std::stringkendisini const char*dolaylı olarak nasıl oluşturacağını bilir . Bu harika. Ama eğer o dize geldiyse, RapidXML de. Veya bir Lua ipi. Diyelim ki dize kadar olan uzunluğu gerçekten biliyorum. Sadece std::stringbir a const char*iletirseniz, uzun süren bir kurucu dizenin uzunluğunu alır const char*.

Açıkça uzun süren bir aşırı yük var. Ama kullanmak, bunu yapmak gerekir: DoSomething(std::string(strValue, strLen)). Neden ekstra daktilo yazmış? Derleyici türünün ne olduğunu bilir. Tıpkı olduğu gibi auto, fazladan daktilo isimleri kullanmaktan kaçınabiliriz:

DoSomething({strValue, strLen});

Sadece işe yarıyor. Yazım hatası yok, yaygara yok, hiçbir şey yok. Derleyici işini yapar, kod daha kısa ve herkes mutlu.

Verilen, ilk sürümün ( DoSomething(std::string(strValue, strLen))) daha okunaklı olacağına dair iddialar var . Yani, neler olduğu ve kimin ne yaptığı açıktır. Bu, bir ölçüde doğrudur; tekdüze başlatma tabanlı kodun anlaşılması, işlev prototipine bakmayı gerektirir. Bazılarının const referansı olmadan parametreleri asla geçmemesi gerektiğini söylemesinin nedeni aynıdır: böylece bir değişkenin değişip değişmediğini görebilmek için arama sitesinde görebilirsiniz.

Fakat aynı şey söylenebilir auto; Ne elde edeceğinizi bilmek auto v = GetSomething();, tanımına bakmayı gerektirir GetSomething. Ancak, buna autoeriştikten sonra, bu umursamaz terkedilmiş vaziyette kullanılmayı durdurmadı . Şahsen, alıştıktan sonra iyi olacağını düşünüyorum. Özellikle iyi bir IDE ile.

Asla En Vexing Ayrıştırmasını Almayın

İşte bazı kodlar.

class Bar;

void Func()
{
  int foo(Bar());
}

Pop sınavı: nedir foo? Eğer bir değişkene cevap verdiyseniz yanılıyorsunuz. Aslında parametre olarak a döndüren bir işlevi alan bir işlevin prototipidir Barve fooişlevin dönüş değeri bir int'dir.

Buna C ++ 'ın "En Vexing Ayrıştırması" denir, çünkü kesinlikle bir insana hiç mantıklı gelmez. Fakat C ++ kuralları ne yazık ki bunu gerektiriyor: eğer bir işlev prototipi olarak yorumlanabilirse, o zaman olacaktır . Sorun şudur Bar(); bu iki şeyden biri olabilir. Adında bir tür olabilir Bar, bu geçici olduğu anlamına gelir. Veya parametre almayan ve a döndüren bir işlev olabilir Bar.

Tek tip başlatma, bir fonksiyon prototipi olarak yorumlanamaz:

class Bar;

void Func()
{
  int foo{Bar{}};
}

Bar{}her zaman geçici oluşturur. int foo{...}her zaman bir değişken oluşturur.

Kullanmak istediğiniz Typename()ancak C ++ 'ın ayrıştırma kuralları nedeniyle yapamayacağınız birçok durum vardır . Ile Typename{}, belirsizlik yoktur.

Olmama Nedenleri

Vazgeçtiğiniz tek gerçek güç daralmaktır. Tek tip başlatma ile daha küçük bir değeri başlatamazsınız.

int val{5.2};

Bu derlenmeyecek. Bunu eski moda başlatma ile yapabilirsiniz, ancak tek biçimli başlatma ile yapabilirsiniz.

Bu, başlatıcı listelerinin gerçekten çalışmasını sağlamak için yapıldı. Aksi takdirde, başlatıcı listelerinin türleriyle ilgili olarak pek çok belirsiz durum olacaktır.

Elbette, bazıları böyle bir kodun derlenmeyi hak etmediğini iddia edebilir . Şahsen kabul ediyorum; daraltmak çok tehlikelidir ve nahoş davranışlara yol açabilir. Derleyici aşamasında bu problemleri erkenden yakalamak muhtemelen en iyisidir. En azından, daralma birisinin kod hakkında çok fazla düşünmediğini gösteriyor.

Uyarı seviyeniz yüksekse, derleyicilerin sizi bu tür şeyler hakkında genellikle uyaracağını unutmayın. Yani gerçekten, tüm bunlar zorla yapılan bir hatayı uyarmaktır. Bazıları yine de yapmanız gerektiğini söyleyebilir.

Yapmamak için bir neden daha var:

std::vector<int> v{100};

Bu ne işe yarıyor? Bu bir yaratabileceği vector<int>yüz varsayılan inşa öğelerle. Veya vector<int>değeri 1 olan bir öğe yaratabilir 100. Her ikisi de teorik olarak mümkün.

Gerçekte, ikincisini yapar.

Neden? Başlatıcı listeleri tek tip başlatma ile aynı sözdizimini kullanır. Bu yüzden belirsizlik durumunda ne yapılacağını açıklamak için bazı kurallar olmalıdır. Kural oldukça basittir: derleyici eğer olabilir bir bağ-başlatıldı listesi ile bir başlatıcı listesi yapıcı kullanmak, o zaman olacak . Yana vector<int>götüren bir başlatıcı listesi yapıcı sahiptir initializer_list<int>ve {100} geçerli olabilir initializer_list<int>, bu nedenle olmalıdır .

Boyutlandırma yapıcısını almak için ()yerine kullanmanız gerekir {}.

Bunun vectorbir tam sayıya dönüştürülemeyen bir şey olsaydı, bunun olmayacağını unutmayın. Bir başlatıcı_ listesi, bu vectortürden başlatıcı liste yapıcısına uymaz ve bu nedenle derleyici diğer yapıcılardan seçim yapmakta serbesttir.


11
+1 Çiviledi. Cevabımı siliyorum, çünkü sizinki aynı noktaları çok daha ayrıntılı olarak ele alıyor.
R. Martinho Fernandes,

21
Son nokta, neden gerçekten hoşlanıyorum std::vector<int> v{100, std::reserve_tag};. Benzer şekilde std::resize_tag. Şu anda vektör alanı ayırmak için iki adım gerekiyor.
Xeo

6
@NicolBolas - İki nokta: Vexing ayrıştırıcılığındaki problemin Bar () değil, foo () olduğunu sanıyordum. Başka bir deyişle, int foo(10)yapsaydınız aynı problemle karşılaşmaz mıydınız? İkincisi, bunu kullanmamak için başka bir neden daha fazla mühendislik meselesi gibi görünüyor, ama peki tüm nesnelerimizi kullanarak inşa edersek {}, fakat bir gün sonra yolun aşağısında başlatma listeleri için bir kurucu eklerim? Artık tüm inşaat ifadelerim başlatıcı listesi ifadelerine dönüştü. Yeniden yapılanma açısından çok kırılgan görünüyor. Bu konuda yorumunuz var mı?
void.pointer

7
@RobertDailey: "Yapsaydın int foo(10)aynı problemle karşılaşmaz mıydın?" 10 numara bir tamsayı değişmezdir ve bir tamsayı değişmezi asla bir yazım adı olamaz. Vexing ayrıştırma, Bar()bir yazım adı veya geçici bir değer olabilir gerçeğinden geliyor . Derleyici için belirsizliği yaratan şey budur.
Nicol Bolas

8
unpleasant behavior- hatırlanması gereken yeni bir standart terim var:>
12'de

64

Nicol Bolas'ın cevabı, Redundant Typenames Minimize Ediyor adlı bölüme katılmıyorum . Kod bir kez yazıldığından ve birden çok kez okunduğundan , kod yazmak için gereken süreyi değil kodu okumak ve anlamak için gereken süreyi en aza indirmeye çalışmalıyız . Yalnızca yazmayı en aza indirmeye çalışmak yanlış şeyi optimize etmeye çalışıyor.

Aşağıdaki koda bakınız:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

Yukarıdaki kodu ilk defa okuyan bir kişi muhtemelen return cümlesini hemen anlamaz, çünkü o hatta ulaştığında, dönüş tipini unutmuş olacaktır. Şimdi, iade türünü görmek ve return ifadesini tam olarak anlamak için işlev imzasına geri dönmek veya IDE özelliğini kullanmak zorunda kalacak.

Ve burada yine birisinin ilk defa kodu okuyan birinin aslında neyin inşa edildiğini anlaması kolay değil:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

Birisi DoSomething'in başka bir dize türünü de desteklemesi gerektiğine karar verdiğinde ve bu aşırı yüklemeyi eklediğinde yukarıdaki kod kırılacak:

void DoSomething(const CoolStringType& str);

CoolStringType, const char * ve size_t (std :: string gibi) alan bir yapıcıya sahipse, DoSomething ({strValue, strLen}) çağrısı belirsizlik hatasına neden olur.

Asıl soruya cevabım:
Hayır, Tekdüze Başlatma eski stil kurucu sözdiziminin yerine geçmiştir.

Ve benim akıl yürütmem şudur:
İki ifadenin aynı niyeti yoksa, aynı görünmemeleri gerekir. İki tür nesne başlatma kavramı vardır:
1) Tüm bu öğeleri alıp bunları başlattığım nesneye dökün.
2) Bu nesneyi, rehber olarak verdiğim bu argümanları kullanarak oluşturun.

1 numaralı kavramın kullanımına örnekler:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

2 numaralı kavramın kullanımına örnek:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Yeni standardın insanların Merdivenleri bu şekilde başlatmasına izin vermesinin kötü bir şey olduğunu düşünüyorum:

Stairs s {2, 4, 6};

... çünkü bu yapıcının anlamını bozuyor. Bunun gibi bir ilklendirme sadece 1 no'lu kavram gibi görünüyor, ama değil. Öyle görünüyor olsa bile, nesnenin içine üç farklı adım yükseklik değeri dökmemek. Ve ayrıca, daha önemlisi, yukarıdaki gibi Merdivenlerin bir kütüphane uygulaması yayınlanmışsa ve programcılar onu kullanıyorsa ve daha sonra kütüphane uygulayıcı daha sonra Merdivenlere bir initializer_list yapıcısı eklerse, o zaman Üniforma Başlatma ile Merdivenleri kullanan tüm kodlar Sözdizimi kırılacak.

C ++ topluluğunun, Üniforma Başlatmanın nasıl kullanıldığına, yani bütün başlatılmalara eşit olarak eşit bir şekilde uygulandığına ya da bu iki başlatma fikrinin ayrıştırılması ve programcının niyetinin okuyucusuna açık bir şekilde önerilmesi gerektiği gibi, ortak bir sözleşmeye katılması gerektiğini düşünüyorum. kod.


SONRASI:
İşte Üniforma Başlatma'yı eski sözdiziminin yerine geçiyormuş gibi düşünmemeniz ve neden tüm başlatmalar için ayraç gösterimini kullanamamanızın başka bir nedeni:

Diyelim ki, bir kopya çıkarmak için tercih ettiğiniz sözdizimi:

T var1;
T var2 (var1);

Şimdi tüm başlatmaları yeni ayraç sözdizimi ile değiştirmelisiniz, böylece daha tutarlı olabilirsiniz (ve kod görünecektir). Ancak, T tipi bir toplu ise, ayraçları kullanan sözdizimi çalışmıyor:

T var2 {var1}; // fails if T is std::array for example

48
"Burada <çok ve çok sayıda kod>> varsa, sözdiziminden bağımsız olarak kodunuzu anlamak zor olacaktır.
kevin cline

8
IMO'nun yanı sıra, hangi türün geri döndüğünü size bildirmek IDE'nizin görevidir (örn. Gezdirme yoluyla). Elbette bir IDE kullanmazsanız, yükü kendiniz üstlendiniz :)
abergmeier

4
@TommiT Söylediklerinizin bazı bölümlerine katılıyorum. Ancak, aynı ruhla autokarşı açık tür bildirimi tartışma, bir denge için iddia ediyorum: tekdüze initializers oldukça büyük zaman kaya şablon meta-programlama tipi zaten genellikle oldukça açıktır durumlar. -> decltype(....)Lanet olası kompantasyon için tekrar etmekten kaçınacaktır, örneğin basit oneline fonksiyon şablonları (beni ağlattı).
12'de

5
" Ancak, T tipi bir küme ise parantez kullanan sözdizimi çalışmıyor: " Bunun kasıtlı olarak beklenen bir davranıştan ziyade standartta bildirilen bir hata olduğunu unutmayın.
Nicol Bolas

5
"Şimdi, işlev imzasına geri dönmek zorunda kalacak", kaydırmanız gerekiyorsa, işleviniz çok büyük.
Miles Rout

-3

Senin kurucular varsa merely copy their parameters, ilgili sınıf değişkenleri in exactly the same orderhangi onlar sınıf içinde bildirilmek edilir, daha sonra tek tip başlatma kullanarak sonuçta daha hızlı olabilir (ama aynı zamanda kesinlikle aynı olabilir) kurucu çağırmak daha.

Açıkçası, bu her zaman yapıcıyı bildirmeniz gerektiği gerçeğini değiştirmez.


2
Neden daha hızlı olabileceğini söylüyorsun?
jbcoe

Bu yanlış. Bir kurucu bildirmek için bir gereksinim yoktur: struct X { int i; }; int main() { X x{42}; }. Aynı zamanda yanlıştır, üniform başlatmanın değer başlatmadan daha hızlı olabileceği de yanlıştır.
Tim
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.