Değere göre iletin. Parametreleri referansa ve değere göre geçirme. Varsayılan ayarları

Öyleyse, Faktöriyel(n), n sayısının faktöriyelini hesaplayan bir fonksiyon olsun. Daha sonra, faktöriyel 1'in 1 olduğunu bildiğimize göre aşağıdaki zinciri oluşturabiliriz:

Faktöriyel(4)=Faktöriyel(3)*4

Faktöriyel(3)=Faktöriyel(2)*3

Faktöriyel(2)=Faktöriyel(1)*2

Ancak, n=1 olduğunda Faktöriyel fonksiyonunun 1 döndürmesi gerektiği gibi bir terminal koşulumuz olmasaydı, o zaman böyle bir teorik zincir asla sona ermezdi ve bu bir Çağrı Yığını Taşması hatası - çağrı yığını taşması olabilirdi. Çağrı yığınının ne olduğunu ve nasıl taşabileceğini anlamak için fonksiyonumuzun özyinelemeli uygulamasına bakalım:

Fonksiyon faktöriyel(n: Tamsayı): LongInt;

Eğer n=1 ise

Faktöriyel:=Faktöriyel(n-1)*n;

Son;

Görüldüğü gibi zincirin doğru çalışması için her bir sonraki fonksiyon kendine çağrılmadan önce tüm yerel değişkenleri bir yere kaydetmek gerekir, böylece zincir ters çevrildiğinde sonuç doğru olur (hesaplanan değer) n-1'in faktöriyelinin değeri n ile çarpılır. Bizim durumumuzda faktöriyel fonksiyon kendisinden çağrıldığında, n değişkeninin tüm değerlerinin kaydedilmesi gerekir. Bir işlevin yerel değişkenlerinin kendisini yinelemeli olarak çağırırken depolandığı alana çağrı yığını denir. Elbette bu yığın sonsuz değildir ve özyinelemeli çağrıların yanlış oluşturulması durumunda tükenebilir. Örneğimizin yinelemelerinin sonluluğu, n=1 olduğunda işlev çağrısının durması gerçeğiyle garanti edilir.

Parametreleri değere ve referansa göre geçirme

Şu ana kadar alt programdaki değeri değiştiremedik gerçek parametre(yani altprogram çağrılırken belirtilen parametre) ve bazı uygulama görevlerinde bu uygun olabilir. Gerçek parametrelerinden ikisinin değerini aynı anda değiştiren Val prosedürünü hatırlayalım: Birincisi, string değişkeninin dönüştürülmüş değerinin yazılacağı parametre, ikincisi ise hatalı olanın sayısının yazılacağı Code parametresidir. Tür dönüşümü sırasında hata olması durumunda karakter yerleştirilir. Onlar. Bir altprogramın gerçek parametreleri değiştirebileceği bir mekanizma hâlâ mevcuttur. Bu, parametreleri aktarmanın çeşitli yolları sayesinde mümkündür. Bu yöntemlere daha yakından bakalım.

Pascal'da Programlama

Parametreleri değere göre geçirme

Esasen tüm parametreleri rutinlerimize bu şekilde aktardık. Mekanizma şu şekildedir: Gerçek bir parametre belirlendiğinde değeri, altprogramın bulunduğu hafıza alanına kopyalanır ve daha sonra fonksiyon veya prosedür işini tamamladıktan sonra bu alan temizlenir. Kabaca söylemek gerekirse, bir altprogram çalışırken parametrelerinin iki kopyası vardır: biri çağıran programın kapsamında, ikincisi ise fonksiyonun kapsamında.

Bu parametre aktarma yöntemiyle alt yordamı çağırmak daha fazla zaman alır, çünkü çağrının kendisine ek olarak tüm gerçek parametrelerin tüm değerlerinin kopyalanması da gerekir. Alt programa büyük miktarda veri aktarılırsa (örneğin, çok sayıda öğe içeren bir dizi), verilerin yerel alana kopyalanması için gereken süre önemli olabilir ve programlar geliştirilirken bu dikkate alınmalıdır. performanslarında darboğazlar bulmak.

Bu aktarım yöntemiyle, değişiklikler yalnızca izole edilmiş bir yerel alanı etkileyeceğinden ve işlev veya prosedür tamamlandıktan sonra yayınlanacak olduğundan, gerçek parametreler alt program tarafından değiştirilemez.

Parametreleri referansa göre aktarma

Bu yöntemle asıl parametrelerin değerleri altprograma kopyalanmaz, ancak bellekte bulundukları adresler (değişkenlere bağlantılar) aktarılır. Bu durumda, alt program zaten yerel kapsamda olmayan değerleri değiştirmektedir, dolayısıyla tüm değişiklikler çağıran program tarafından görülebilecektir.

Bir argümanın referans yoluyla iletilmesi gerektiğini belirtmek için, var anahtar sözcüğü, bildiriminin önüne eklenir:

Prosedür getTwoRandom(var n1, n2:Tamsayı; aralık: Tamsayı);

n1:=rasgele(aralık);

n2:=rasgele(aralık); son ;

var rand1, rand2: Tamsayı;

Başlamak getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Son.

Bu örnekte, iki değişkene yapılan referanslar getTwoRandom prosedürüne gerçek parametreler olarak aktarılır: Rand1 ve Rand2. Üçüncü gerçek parametre (10) değere göre iletilir. Prosedür resmi parametreleri kullanarak yazar

Dizeleri Kullanarak Programlama Yöntemleri

Laboratuvar çalışmasının amacı : C# dilindeki yöntemleri, karakter verileriyle çalışma kurallarını ve ListBox bileşenini öğrenin. Dizelerle çalışacak bir program yazın.

Yöntemler

Yöntem, program kodunu içeren bir sınıf öğesidir. Yöntem aşağıdaki yapıya sahiptir:

[nitelikler] [belirleyiciler] tür adı ([parametreler])

Yöntem gövdesi;

Nitelikler, derleyiciye bir yöntemin özellikleri hakkında verilen özel talimatlardır. Nitelikler nadiren kullanılır.

Niteleyiciler farklı amaçlara hizmet eden anahtar kelimelerdir; örneğin:

· Diğer sınıflar için bir yöntemin kullanılabilirliğinin belirlenmesi:

Ö özel– yöntem yalnızca bu sınıf içinde geçerli olacaktır

Ö korumalı– yöntem aynı zamanda alt sınıflar için de geçerli olacak

Ö halk– yöntem, bu sınıfa erişebilen diğer tüm sınıflar tarafından kullanılabilir olacaktır.

Bir sınıf oluşturmadan bir yöntemin kullanılabilirliğini belirtme

· Ayar türü

Tür, yöntemin döndürdüğü sonucu belirler: bu, C#'ta mevcut olan herhangi bir tür olabileceği gibi, sonuç gerekli değilse void anahtar sözcüğü de olabilir.

Yöntem adı, yöntemi çağırmak için kullanılacak tanımlayıcıdır. Değişken adlarıyla aynı gereksinimler tanımlayıcı için de geçerlidir: harflerden, rakamlardan ve alt çizgiden oluşabilir ancak sayıyla başlayamaz.

Parametreler, çağrıldığında bir yönteme aktarılabilecek değişkenlerin listesidir. Her parametre bir değişken türü ve adından oluşur. Parametreler virgülle ayrılır.

Bir yöntemin gövdesi normal program kodudur ancak diğer yöntemlerin, sınıfların, ad alanlarının vb. tanımlarını içeremez. Bir yöntemin bir sonuç döndürmesi gerekiyorsa, dönüş anahtar sözcüğünün sonunda dönüş değeriyle birlikte bulunması gerekir. . Sonuçların döndürülmesi gerekli değilse, izin verilmesine rağmen return anahtar sözcüğünün kullanılması gerekli değildir.

Bir ifadeyi değerlendiren bir yöntem örneği:

public double Calc(double a, double b, double c)

return Math.Sin(a) * Math.Cos(b);

double k = Math.Tan(a * b);

return k * Math.Exp(c / k);

Yöntem Aşırı Yüklemesi

C# dili, aynı adlara ancak farklı parametrelere sahip birden fazla yöntem oluşturmanıza olanak tanır. Derleyici, programı oluştururken en uygun yöntemi otomatik olarak seçecektir. Örneğin, bir sayının üssünü yükseltmek için iki ayrı yöntem yazabilirsiniz: bir algoritma tamsayılar için, diğeri ise gerçek sayılar için kullanılır:

///

/// Tamsayılar için X'in Y üssünü hesaplayın

///

özel int Pow(int X, int Y)

///

/// Gerçek sayılar için X'in Y üssünü hesaplayın

///

özel çift Pow(çift X, çift Y)

return Math.Exp(Y * Math.Log(Math.Abs(X)));

else if (Y == 0)

Bu kod aynı şekilde çağrılır, tek fark parametrelerdedir - ilk durumda derleyici Pow yöntemini tamsayı parametrelerle, ikincisinde ise gerçek parametrelerle çağırır:

Varsayılan ayarları

Sürüm 4.0'dan (Visual Studio 2010) başlayan C# dili, bazı parametreler için varsayılan değerleri ayarlamanıza olanak tanır, böylece bir yöntemi çağırırken bazı parametreleri atlayabilirsiniz. Bunu yapmak için, yöntemi uygularken gerekli parametrelere doğrudan parametre listesinden bir değer atanmalıdır:

özel void GetData(int Sayı, int İsteğe Bağlı = 5 )

Console.WriteLine("Sayı: (0)", Sayı);

Console.WriteLine("İsteğe Bağlı: (0)", İsteğe Bağlı);

Bu durumda metodu şu şekilde çağırabilirsiniz:

GetData(10, 20);

İlk durumda, İsteğe bağlı parametre açıkça belirtildiği için 20'ye eşit olacaktır ve ikinci durumda, 5'e eşit olacaktır çünkü açıkça belirtilmemiştir ve derleyici varsayılan değeri alır.

Varsayılan parametreler yalnızca parametre listesinin sağ tarafında ayarlanabilir; örneğin, böyle bir yöntem imzası derleyici tarafından kabul edilmeyecektir:

özel void GetData(int İsteğe bağlı = 5 , int Sayı)

Parametreler bir yönteme normal şekilde (ek ref ve out anahtar sözcükleri olmadan) iletildiğinde, yöntem içindeki parametrelerde yapılan herhangi bir değişiklik, onun ana programdaki değerini etkilemez. Diyelim ki aşağıdaki yöntemimiz var:

özel void Calc(int Number)

Metot içerisinde parametre olarak aktarılan Number değişkeninin değiştirildiği görülmektedir. Yöntemi çağırmayı deneyelim:

Console.WriteLine(n);

Ekranda 1 rakamı görünecektir yani Calc yöntemindeki değişkenin değişmesine rağmen değişkenin ana programdaki değeri değişmemiştir. Bunun nedeni, bir yöntem çağrıldığında, kopyala Geçilen değişken, yöntemin değiştiği bu değişkendir. Yöntem sonlandırıldığında kopyaların değeri kaybolur. Bu parametre aktarma yöntemine denir değere göre geçmek.

Bir yöntemin kendisine iletilen bir değişkeni değiştirmesi için, bunun ref anahtar sözcüğüyle iletilmesi gerekir - hem yöntem imzasında hem de çağrıldığında olmalıdır:

özel void Calc(ref int Number)

Console.WriteLine(n);

Bu durumda ekranda 10 sayısı görünecektir: Yöntemdeki değerin değişmesi ana programı da etkilemiştir. Bu yönteme transfer adı verilir referans yoluyla geçmek yani Artık iletilen bir kopya değil, bellekteki gerçek bir değişkene yapılan bir referanstır.

Bir yöntem, değişkenleri yalnızca değerleri döndürmek için referans olarak kullanıyorsa ve başlangıçta içlerinde ne olduğu umurunda değilse, bu tür değişkenleri başlatamazsınız, ancak bunları out anahtar sözcüğüyle iletebilirsiniz. Derleyici, değişkenin başlangıç ​​değerinin önemli olmadığını anlar ve başlatma eksikliğinden şikayet etmez:

özel void Calc(out int Number)

int n; // Hiçbir şey atmıyoruz!

dize veri türü

C# dili, dizeleri depolamak için dize türünü kullanır. Bir dize değişkenini bildirmek (ve kural olarak hemen başlatmak) için aşağıdaki kodu yazabilirsiniz:

string a = "Metin";

dize b = "dizeler";

Satırlarda ekleme işlemi gerçekleştirebilirsiniz; bu durumda, bir satırın metni diğerinin metnine eklenecektir:

dize c = a + " " + b; // Sonuç: String metni

String türü aslında String sınıfına ait bir takma addır ve dizeler üzerinde daha karmaşık bir dizi işlem gerçekleştirmenize olanak tanır. Örneğin, IndexOf yöntemi bir dizede bir alt dize arayabilir ve Substring yöntemi, belirtilen konumdan başlayarak belirtilen uzunluktaki dizenin bir kısmını döndürür:

dize a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int indeks = a.IndexOf("OP"); // Sonuç: 14 (0'dan itibaren sayılıyor)

string b = a.Substring(3, 5); // Sonuç: DEFGH

Bir dizeye özel karakterler eklemeniz gerekiyorsa bunu ters eğik çizgiyle başlayan kaçış dizilerini kullanarak yapabilirsiniz:

ListBox Bileşeni

Bileşen Liste kutusuöğeleri klavye veya fare kullanılarak seçilen bir listedir. Öğelerin listesi özellik tarafından belirtilir Öğeler. Öğeler, kendine has özellikleri ve yöntemleri olan bir öğedir. Yöntemler Eklemek, KaldırAt Ve SokmakÖğe eklemek, silmek ve eklemek için kullanılır.

Bir obje Öğeler nesneleri listede saklar. Nesne herhangi bir sınıf olabilir; sınıf verileri, ToString yöntemiyle görüntülenmek üzere bir dize temsiline dönüştürülür. Bizim durumumuzda dizeler nesne görevi görecek. Ancak, Items nesnesi type nesnesine dönüştürülen nesneleri sakladığından, onu kullanmadan önce onları orijinal türlerine geri döndürmeniz gerekir, bizim durum dizemizde:

string a = (string)listBox1.Items;

Seçilen öğenin sayısını belirlemek için özelliği kullanın Seçilen Dizin.

C++ ile programlamaya başladığımda ve kitap ve makaleleri yoğun bir şekilde incelediğimde, her zaman aynı tavsiyeyle karşılaştım: Eğer bir nesneyi, fonksiyonda değişmemesi gereken bir fonksiyona aktarmamız gerekiyorsa, o zaman bu her zaman iletilmelidir. bir sabite referansla(PPSK), ya ilkel bir türü ya da boyut olarak onlara benzer bir yapıyı geçmemiz gereken durumlar dışında. Çünkü C++'da 10 yıldan fazla programlama deneyimim boyunca, bu tavsiyeyle çok sık karşılaştım (ve ben de bunu bir kereden fazla verdim), uzun zamandır benim için "özümlendi" - tüm argümanları otomatik olarak bir sabite referansla aktarıyorum . Ancak zaman geçiyor ve hareket semantiği ile C++11'in elimizde olmasından bu yana 7 yıl geçti, bununla bağlantılı olarak eski güzel dogmayı sorgulayan sesleri giderek daha fazla duyuyorum. Birçoğu, bir sabite referans vermenin geçmişte kaldığını ve artık gerekli olduğunu tartışmaya başlıyor değere göre geçmek(PPZ). Bu konuşmaların arkasında ne olduğunu ve tüm bunlardan ne gibi sonuçlar çıkarabileceğimizi bu makalede tartışmak istiyorum.

Kitap bilgeliği

Hangi kurala uymamız gerektiğini anlamak için kitaplara yönelmenizi öneririm. Kitaplar, kabul etmek zorunda olmadığımız ama kesinlikle dinlemeye değer mükemmel bir bilgi kaynağıdır. Ve tarihle, kökenlerle başlayacağız. PPSC'den ilk özür dileyen kişinin kim olduğunu bulmayacağım, sadece örnek olarak PPSC kullanımı konusunda kişisel olarak benim üzerimde en büyük etkiye sahip olan kitabı vereceğim.

Mayer'lar

Tamam, burada tüm parametrelerin referans yoluyla iletildiği bir sınıfımız var, bu sınıfta herhangi bir sorun var mı? Ne yazık ki var ve bu sorun yüzeyde yatıyor. Sınıfımızda 2 işlevsel varlık var: Birincisi nesne oluşturma aşamasında bir değer alır, ikincisi ise önceden ayarlanmış bir değeri değiştirmenizi sağlar. İki varlığımız var ama dört işlevimiz var. Şimdi 2 benzer varlığa değil de 3, 5, 6'ya sahip olabileceğimizi hayal edin, o zaman ne olacak? O zaman ciddi kod şişkinliğiyle karşı karşıya kalacağız. Bu nedenle, çok sayıda işlev oluşturmamak için parametrelerdeki bağlantıların tamamen terk edilmesi önerisi vardı:

Şablon sınıf Tutucu ( public: açık Tutucu(T değeri): m_Value(move(value)) ( ) void setValue(T value) ( ​​m_Value = move(value); ) const T& value() const no Except ( return m_Value; ) özel: T m_Value; );

Hemen gözünüze çarpan ilk avantaj, kodun çok daha az olmasıdır. Const ve &'nin kaldırılması nedeniyle (move eklemiş olmalarına rağmen) ilk versiyona göre daha da azı var. Ancak bize her zaman referans yoluyla geçmenin, değere göre aktarmadan daha verimli olduğu öğretildi! C++11'den önce de böyleydi, hala da böyle ama şimdi bu koda bakarsak burada ilk versiyona göre daha fazla kopyalamanın olmadığını göreceğiz. T'nin bir hamle yapıcısına sahip olması şartıyla. Onlar. PPSC'nin kendisi PPZ'den daha hızlıydı ve daha hızlı olacak, ancak kod bir şekilde aktarılan referansı kullanıyor ve çoğu zaman bu argüman kopyalanıyor.

Ancak hikayenin tamamı bu değil. Yalnızca kopyalamanın olduğu ilk seçeneğin aksine, burada hareket de ekliyoruz. Ama taşınmak ucuz bir operasyon, değil mi? Bu konuyla ilgili olarak ele aldığımız Mayers kitabında ayrıca şu başlıklı bir bölüm (“Madde 29”) var: “Hareket işlemlerinin mevcut olmadığını, ucuz olmadığını ve kullanılmadığını varsayalım.” Ana fikir başlıktan açıkça anlaşılmalıdır, ancak ayrıntı istiyorsanız mutlaka okuyun - bunun üzerinde durmayacağım.

Burada ilk ve son yöntemlerin tam bir karşılaştırmalı analizini yapmak uygun olacaktır, ancak kitaptan sapmak istemediğim için analizi diğer bölümlere erteleyeceğiz ve burada Scott'ın argümanlarını değerlendirmeye devam edeceğiz. Peki, üçüncü seçeneğin açıkça ikinciden daha kısa olduğu gerçeğinin yanı sıra, Scott, modern kodda PPZ'nin PPSC'ye göre avantajı olarak ne görüyor?

Bunu bir değerin iletilmesi durumunda, yani. bazıları şöyle çağırır: Holderholder(string("me")); , PPSC seçeneği bize kopyalamayı, PPZ seçeneği ise bize hareket verecektir. Öte yandan eğer transfer şu şekilde ise: Hamil sahibi(birLdeğer); , o zaman PPZ hem kopyalamayı hem de taşımayı gerçekleştireceği için kesinlikle kaybeder, oysa PPSC'li versiyonda yalnızca bir kopyalama olacaktır. Onlar. Tamamen verimliliği düşünürsek PPZ'nin, kod miktarı ile hareket semantiği için "tam" ( && aracılığıyla) destek arasında bir tür uzlaşma olduğu ortaya çıktı.

Scott'ın tavsiyesini bu kadar dikkatli bir şekilde dile getirmesinin ve bu kadar dikkatli bir şekilde tanıtmasının nedeni budur. Hatta bana sanki baskı altındaymışçasına gönülsüzce konuyu gündeme getirmiş gibi geldi: Kitapta bu konu hakkındaki tartışmaları yapmaktan kendini alamıyordu, çünkü... oldukça geniş bir şekilde tartışıldı ve Scott her zaman kolektif deneyimlerin koleksiyoncusuydu. Ayrıca, PPZ'yi savunmak için çok az argüman sunuyor, ancak bu "tekniği" diyenlerin çoğunu sorguluyor. İlerleyen bölümlerde onun aleyhindeki argümanlara bakacağız, ancak burada Scott'ın PPP'yi savunmak için öne sürdüğü argümanı kısaca tekrarlayacağız (zihinsel olarak şunu ekliyor: “nesne hareketi destekliyorsa ve ucuzsa”): bir işlev argümanı olarak bir değer ifadesi iletirken kopyalamayı önlemenizi sağlar. Ama Meyers'in kitabı yeterince eziyet ediyor, hadi başka bir kitaba geçelim.

Bu arada, eğer kitabı okuyan biri varsa ve Mayers'in evrensel referanslar olarak adlandırdığı - artık yönlendirme referansları olarak bilinen - seçeneği buraya dahil etmememe şaşırırsa, bu kolayca açıklanabilir. Sadece PPZ ve PPSC'yi düşünüyorum çünkü... Her iki türün (rvalue/lvalue) referansıyla geçişi desteklemek adına şablon olmayan yöntemler için şablon işlevlerini tanıtmanın kötü bir biçim olduğunu düşünüyorum. Kodun farklı ortaya çıktığı (artık sabitlik olmadığı) ve beraberinde başka sorunlar getirdiği gerçeğinden bahsetmiyorum bile.

Josattis ve arkadaşları

Bakacağımız son kitap ise bu yazıda bahsedilen kitapların en yenisi olan “C++ Şablonları”dır. 2017 yılı sonunda yayımlanmıştır (ve kitabın içinde 2018 yılı belirtilmektedir). Diğer kitaplardan farklı olarak, bu kitap tamamen kalıplara ayrılmıştır ve tavsiyelere (Mayers gibi) ya da Stroustrup gibi genel olarak C++'a değildir. Bu nedenle burada şablon yazma açısından artılar/eksiler ele alınmıştır.

7. bölümün tamamı bu konuya ayrılmıştır ve bu başlığın anlamlı başlığı "Değer olarak mı yoksa referans olarak mı?" Bu bölümde yazarlar, tüm aktarım yöntemlerini tüm artıları ve eksileriyle birlikte oldukça kısa ama öz bir şekilde anlatıyorlar. Etkinliğin analizi pratikte burada verilmemektedir ve PPSC'nin PPZ'den daha hızlı olacağı varsayılmaktadır. Ancak tüm bunlarla birlikte, bölümün sonunda yazarlar şablon işlevleri için varsayılan PPP'nin kullanılmasını önermektedir. Neden? Çünkü bir bağlantı kullanıldığında, şablon parametreleri tamamen görüntülenir ve bir bağlantı olmadan bunlar "bozulur", bu da dizilerin ve dize değişmezlerinin işlenmesi üzerinde faydalı bir etkiye sahiptir. Yazarlar, bir PPP türü için etkisiz olduğu ortaya çıkarsa, her zaman std::ref ve std::cref kullanılabileceğine inanıyor. Dürüst olmak gerekirse bu bir tavsiye, yukarıdaki işlevleri kullanmak isteyen çok kişi gördünüz mü?

PPSC hakkında ne tavsiye ediyorlar? Performans kritik olduğunda veya başka sorunlar olduğunda PPSC'nin kullanılmasını önerirler. ağır PPP kullanmama nedenleri. Elbette burada yalnızca ortak koddan bahsediyoruz, ancak bu tavsiye programcılara on yıldır öğretilen her şeyle doğrudan çelişiyor. Bu sadece PPP'yi bir alternatif olarak değerlendirmeye yönelik bir tavsiye değildir - hayır, bu PPSC'yi bir alternatif haline getirmeye yönelik bir tavsiyedir.

Böylece kitap turumuz sona eriyor, çünkü... Bu konuda başvurabileceğimiz başka bir kitap bilmiyorum. Başka bir medya alanına geçelim.

Ağ bilgeliği

Çünkü İnternet çağında yaşıyoruz, o zaman yalnızca kitap bilgeliğine güvenmemelisiniz. Üstelik eskiden kitap yazan pek çok yazar artık sadece blog yazıyor ve kitapları terk ediyor. Bu yazarlardan biri, Mayıs 2013'te "GotW #4 Çözüm: Sınıf Mekaniği" adlı blogunda bir makale yayınlayan Herb Sutter'dır; bu makale, ele aldığımız soruna tamamen ayrılmış olmasa da, yine de bu soruna değinmektedir.

Yani makalenin orijinal versiyonunda Sutter basitçe eski bilgeliği tekrarladı: "parametreleri bir sabite referansla iletin", ancak artık makalenin bu versiyonunu görmeyeceğiz çünkü Makale tam tersi tavsiyeler içeriyor: “ Eğer parametre yine de kopyalanacak, ardından değere göre iletilecektir. Yine o meşhur "eğer". Sutter makaleyi neden değiştirdi ve bunu nasıl bildim? Yorumlardan. Makalesine yapılan yorumları okuyun; bu arada, makalenin kendisinden daha ilginç ve faydalılar. Doğru, Sutter makaleyi yazdıktan sonra nihayet fikrini değiştirdi ve artık böyle bir tavsiye vermiyor. Fikir değişikliğini 2014 yılında CppCon'daki konuşmasında görmek mümkün: “Temellere Dönüş! Modern C++ Stilinin Temelleri". Baktığınızdan emin olun, bir sonraki İnternet bağlantısına geçeceğiz.

Ve şimdi 21. yüzyılın ana programlama kaynağına sahibiz: StackOverflow. Daha doğrusu bu yazının yazıldığı sırada olumlu tepkilerin sayısı 1700'ü aşmıştı. Soru şu: Kopyala ve değiştir deyimi nedir? ve başlıktan da anlaşılacağı gibi, baktığımız konuyla pek ilgili değil. Ancak yazar bu soruya verdiği yanıtta bizi ilgilendiren bir konuya da değiniyor. Ayrıca "eğer argüman yine de kopyalanacaksa" PPZ'nin kullanılmasını tavsiye ediyor (Tanrı adına bunun için de bir kısaltma getirmenin zamanı geldi). Ve genel olarak, bu tavsiye, kendi cevabı ve burada tartışılan operatör çerçevesinde oldukça uygun görünüyor, ancak yazar bu tür tavsiyeleri daha geniş bir şekilde verme özgürlüğünü kullanıyor ve sadece bu özel durumda değil. Üstelik daha önce tartıştığımız tüm ipuçlarının daha da ötesine geçiyor ve bunun C++03 kodunda bile yapılması gerektiğini söylüyor! Yazarı bu tür sonuçlara varmaya iten şey neydi?

Görünüşe göre, cevabın yazarı ana ilhamı başka bir kitap yazarı ve Boost.MPL'nin yarı zamanlı geliştiricisi Dave Abrahams'ın bir makalesinden almış. Makalenin adı “Hız mı İstiyorsunuz? Değere Göre Geçin.” ve Ağustos 2009'da yayınlandı, yani. C++11'in benimsenmesinden ve hareket semantiğinin tanıtılmasından 2 yıl önce. Önceki durumlarda olduğu gibi, okuyucunun makaleyi kendi başına okumasını öneriyorum, ancak Dave'in PPZ lehine sunduğu ana argümanları (aslında sadece bir argüman var) vereceğim: PPZ'yi kullanmanız gerekiyor , çünkü "kopyayı atla" optimizasyonu onunla iyi çalışıyor ( kopya seçimi), bu PPSC'de yoktur. Makaleye yapılan yorumları okursanız, sunduğu tavsiyelerin evrensel olmadığını görebilirsiniz; yorumcuların eleştirilerine yanıt verirken yazarın kendisi de bunu onaylıyor. Ancak makale, eğer argüman yine de kopyalanacaksa, PPP'nin kullanılmasına yönelik açık bir tavsiye (yönerge) içermektedir. Bu arada ilgilenen varsa “Hız mı istiyorsunuz? (Her zaman) değere göre geçmeyin. . Başlığın da belirttiği gibi, bu makale Dave'in makalesine bir yanıttır, bu nedenle ilkini okuduysanız bunu da mutlaka okuyun!

Ne yazık ki (bazıları için ne mutlu ki), popüler sitelerdeki bu tür makaleler ve (hatta daha da fazlası) popüler yanıtlar, şüpheli tekniklerin (önemsiz bir örnek) kitlesel kullanımına yol açıyor çünkü bu daha az yazı gerektiriyor ve eski dogma artık sarsılmaz değil - Eğer duvara itilirseniz her zaman "bu popüler tavsiyeye" başvurabilirsiniz. Şimdi, çeşitli kaynakların bize kod yazma önerileri konusunda neler sunduğunu öğrenmenizi öneririm.

Çünkü Artık çeşitli standartlar ve tavsiyeler çevrimiçi olarak da yayınlandığından, bu bölümü "ağ bilgeliği" olarak sınıflandırmaya karar verdim. Burada iki kaynaktan bahsetmek istiyorum; bunların amacı, C++ programcılarına bu kodun nasıl yazılacağına dair ipuçları (yönergeler) sağlayarak kodlarını daha iyi hale getirmektir.

Göz önünde bulundurmak istediğim ilk kurallar dizisi, beni bu makaleyi yazmaya zorlayan bardağı taşıran son damla oldu. Bu set, clang-tidy yardımcı programının bir parçasıdır ve onun dışında mevcut değildir. Clang ile ilgili her şey gibi, bu yardımcı program da çok popüler ve zaten CLion ve Resharper C++ ile entegrasyona sahip (ben de bu şekilde karşılaştım). Dolayısıyla, clang-tydy, PPSC yoluyla argümanları kabul eden yapıcılar üzerinde çalışan, değere göre modernize etme kuralı içerir. Bu kural PPSC'yi PPZ ile değiştirmemizi önerir. Ayrıca makalenin yazıldığı sırada bu kuralın açıklamasında bu kuralın geçerli olduğu yönünde bir açıklama yer almaktadır. Hoşçakal yalnızca inşaatçılar için çalışır, ancak onlar (kim bunlar?) bu kuralı diğer varlıklara da uygulayanların yardımını memnuniyetle kabul edeceklerdir. Açıklamada Dave'in makalesine bir bağlantı var - bacakların nereden geldiği açık.

Son olarak, diğer insanların bilgeliği ve yetkili görüşleri hakkındaki bu incelemeyi sonuçlandırmak için, C++ kodu yazmaya ilişkin resmi yönergelere bakmanızı öneririm: C++ Temel Yönergeleri, bunların ana editörleri Herb Sutter ve Bjarne Stroustrup'tur (fena değil, değil mi?). Dolayısıyla, bu öneriler şu kuralı içerir: "İçerideki" parametreler için, ucuza kopyalanan türleri değere göre ve diğerlerini const'a referansla iletin", bu da eski bilgeliği tamamen tekrarlıyor: her yerde PPSK ve küçük nesneler için PPP. Bu ipucu dikkate alınması gereken çeşitli alternatifleri özetlemektedir. argüman aktarımının optimizasyona ihtiyaç duyması durumunda. Ancak alternatifler listesinde PPZ yer almıyor!

Dikkate değer başka kaynağım olmadığından, her iki aktarım yönteminin doğrudan analizine geçmeyi öneriyorum.

Analiz

Önceki metnin tamamı benim için biraz alışılmadık bir şekilde yazılmış: Başkalarının fikirlerini sunuyorum ve hatta kendi fikrimi ifade etmemeye çalışıyorum (kötü sonuçlandığını biliyorum). Büyük ölçüde başkalarının görüşleri olduğundan ve amacım bunlara kısa bir genel bakış yapmak olduğundan, diğer yazarlarda bulduğum bazı argümanların ayrıntılı bir şekilde değerlendirilmesini erteledim. Bu bölümde otoritelere atıfta bulunup görüş belirtmeyeceğim, burada PPSC ve PPZ'nin subjektif algılarımla tatlandırılacak bazı objektif avantaj ve dezavantajlarına bakacağız. Elbette daha önce tartışılanların bir kısmı tekrarlanacak, ancak ne yazık ki bu makalenin yapısı bu.

PPP'nin bir avantajı var mı?

Bu nedenle, lehte ve aleyhte olan argümanları değerlendirmeden önce, değere göre geçmenin bize ne ve hangi durumlarda avantaj sağladığına bakmayı öneriyorum. Diyelim ki şöyle bir sınıfımız var:

Sınıf CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotMover(Accounter byValuerAndNotMover) ( m_ByValuerAndNotMover = byVal u erAndNotMover; ) geçersiz setRvaluer (Hesap&& değerleyici) ( m_Rvaluer = std::move(değerleyici); ) );

Bu makalenin amaçları doğrultusunda yalnızca ilk iki işlevle ilgilenmemize rağmen, bunları kontrast olarak kullanmak için dört seçenek ekledim.

Accounter sınıfı, kaç kez kopyalandığını/taşındığını sayan basit bir sınıftır. CopyMover sınıfında ise aşağıdaki seçenekleri değerlendirmemize olanak tanıyan işlevler uyguladık:

    hareketli argümanı geçti.

    Değere göre iletin ve ardından kopyalama argümanı geçti.

Şimdi, bu işlevlerin her birine bir değer iletirsek, örneğin şöyle:

MuhasebeciRefer; Değerleme Uzmanı; ValuerAndNotMover'dan Muhasebeci; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

sonra aşağıdaki sonuçları elde ederiz:

Açıkça kazanan PPSC'dir, çünkü... yalnızca bir kopya verirken, PPZ bir kopya ve bir hamle verir.

Şimdi bir değer aktarmayı deneyelim:

CopyMover copyMover; copyMover.setByRefer(Hesap()); copyMover.setByValuer(Accounter()); copyMover.setByValuerAndNotMover(Accounter()); copyMover.setRvaluer(Hesap());

Aşağıdakileri alıyoruz:

Burada net bir kazanan yok çünkü... hem PPZ hem de PPSK'nın birer işlemi vardır, ancak PPZ'nin hareketi, PPSK'nın ise kopyalamayı kullanması nedeniyle zaferi PPZ'ye verebiliriz.

Ancak deneylerimiz burada bitmiyor; dolaylı bir çağrıyı simüle etmek için (sonraki argüman geçişiyle) aşağıdaki işlevleri ekleyelim:

Void setByValuer(Accounter byValuer, CopyMover& copyMover) ( copyMover.setByValuer(std::move(byValuer)); ) void setByRefer(const Accounter& byRefer, CopyMover& copyMover) ( copyMover.setByRefer(byRefer); ) ...

Bunları tam olarak onlarsız kullandığımız gibi kullanacağız, bu yüzden kodu tekrarlamayacağım (gerekirse depoya bakın). Yani lvalue için sonuçlar şöyle olacaktır:

PPSC'nin PPZ ile arasındaki farkı artırdığını ve tek bir kopya ile kaldığını, PPZ'nin ise zaten 3 işleme (bir hareket daha) sahip olduğunu unutmayın!

Şimdi rvalue'yi geçiyoruz ve aşağıdaki sonuçları alıyoruz:

Artık PPZ'nin 2 hareketi var ve PPSC'nin hâlâ bir kopyası var. Artık PPZ'yi kazanan olarak aday göstermek mümkün mü? Hayır çünkü Bir hamlenin en azından bir kopyadan daha kötü olmaması gerekiyorsa, 2 hamle için aynı şeyi söyleyemeyiz. Dolayısıyla bu örnekte kazanan olmayacak.

Bana şöyle itiraz edebilirler: “Yazar, ön yargılı bir düşüncen var ve kendine faydalı olanı çekiyorsun. 2 hamle bile kopyalamaktan daha ucuz olacak!” Bu ifadeye katılmam mümkün değil Her şeyi hesaba katarak, Çünkü Taşımanın kopyalamaya göre ne kadar hızlı olduğu belirli sınıfa bağlıdır, ancak "ucuz" taşıma konusuna ayrı bir bölümde bakacağız.

Burada ilginç bir şeye değindik: Bir dolaylı çağrı ekledik ve PPP "ağırlık" konusunda tam olarak bir operasyon ekledi. Ne kadar dolaylı çağrı yaparsak, PPZ kullanırken o kadar fazla işlem gerçekleştirileceğini, PPSC için ise sayının değişmeyeceğini anlamak için MSTU'dan diploma almanıza gerek olmadığını düşünüyorum.

Yukarıda tartışılan her şeyin kimseye açıklanması pek olası değildi, hatta deneyler bile yapmamış olabiliriz - tüm bu sayılar çoğu C++ programcısı için ilk bakışta açık olmalıdır. Doğru, bir nokta hala açıklığa kavuşturulmayı hak ediyor: neden değer söz konusu olduğunda PZ'nin bir kopyası (veya başka bir hamlesi) yok, sadece bir hamlesi var.

Kopya ve hamle sayısını ilk elden gözlemleyerek PPZ ve PPSC arasındaki aktarım farkına bir göz attık. Her ne kadar bu kadar basit örneklerde bile PPZ'nin PPSC'ye göre avantajı açık olsa da, en hafif deyimle Olumsuz Açıkçası, biraz iddialı bir şekilde şu sonuca varıyorum: Eğer hala fonksiyon argümanını kopyalayacaksak, o zaman argümanı fonksiyona değere göre aktarmayı düşünmek mantıklı olacaktır. Neden bu sonuca vardım? Bir sonraki bölüme sorunsuz bir şekilde geçmek için.

Eğer kopyalarsak...

Böylece meşhur “eğer”e geliyoruz. Karşılaştığımız argümanların çoğu, PPSC yerine PPP'nin evrensel olarak uygulanmasını talep etmiyordu; yalnızca "argüman yine de kopyalanırsa" bunu yapmayı talep ediyorlardı. Bu argümanda neyin yanlış olduğunu bulmanın zamanı geldi.

Kodu nasıl yazdığıma dair küçük bir açıklama ile başlamak istiyorum. Son zamanlarda kodlama sürecim giderek daha çok TDD'ye benziyor. Herhangi bir sınıf yöntemini yazmak, bu yöntemin yer aldığı bir test yazmakla başlar. Buna göre test yazmaya başladığımda ve testi yazdıktan sonra yöntem oluştururken argümanı kopyalayıp kopyalayamayacağımı hala bilmiyorum. Elbette tüm işlevler bu şekilde oluşturulmaz; çoğu zaman bir test yazma sürecinde bile ne tür bir uygulamanın olacağını tam olarak bilirsiniz. Ancak bu her zaman olmaz!

Birisi bana, yöntemin orijinal olarak nasıl yazıldığının bir önemi olmadığını, yöntem şekillendiğinde ve orada ne olduğu bizim için tamamen açık olduğunda argümanı iletme şeklimizi değiştirebileceğimizi söyleyerek itiraz edebilir (örn. kopyaladığımız veya kopyaladığımız veya kopyaladığımız veya Olumsuz ). Buna kısmen katılıyorum - aslında bunu bu şekilde yapabilirsiniz, ancak bu bizi sırf uygulama değiştiği için arayüzleri değiştirmek zorunda kaldığımız bir tür tuhaf oyuna dahil ediyor. Bu da bizi bir sonraki ikileme getiriyor.

Arayüzü nasıl uygulanacağına bağlı olarak değiştirdiğimiz (hatta planladığımız) ortaya çıktı. Kendimi OOP ve yazılım mimarisinin diğer teorik hesaplamaları konusunda uzman olarak görmüyorum, ancak bu tür eylemler, uygulamanın arayüzü etkilememesi gerektiğinde temel kurallarla açıkça çelişiyor. Tabii ki, belirli uygulama ayrıntıları (ister dilin ister hedef platformun özellikleri olsun) hala öyle ya da böyle arayüzden sızıyor, ancak bu tür şeylerin sayısını artırmak değil, azaltmaya çalışmalısınız.

Tanrı onu korusun, bu yola devam edelim ve argümanı kopyalama açısından uygulamada ne yaptığımıza bağlı olarak arayüzleri değiştirelim. Diyelim ki bu yöntemi yazdık:

Void setName(Ad adı) ( m_Name = move(name); )

ve değişikliklerimizi depoya aktardık. Zaman geçtikçe yazılım ürünümüz yeni işlevler kazandı, yeni çerçeveler entegre edildi ve sınıfımızdaki değişiklikler hakkında dış dünyayı bilgilendirme görevi ortaya çıktı. Onlar. Yöntemimize bazı bildirim mekanizmaları ekleyeceğiz, Qt sinyallerine benzer bir şey olsun:

Void setName(Name name) ( m_Name = move(name); emit nameChanged(m_Name); )

Bu kodla ilgili bir sorun mu var? Yemek yemek. setName'e yapılan her çağrı için bir sinyal göndeririz, böylece sinyal şu ​​durumlarda bile gönderilir: Anlam m_Name değişmedi. Performans sorunlarının yanı sıra, yukarıdaki bildirimi alan kodun bir şekilde setName çağrısına gelmesi nedeniyle bu durum sonsuz bir döngüye yol açabilir. Tüm bu sorunlardan kaçınmak için bu tür yöntemler çoğunlukla şuna benzer:

Void setName(Name name) ( if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); )

Yukarıda anlatılan sorunlardan kurtulduk ama artık "yine de kopyalarsak..." kuralımız başarısız oldu - artık argümanın koşulsuz kopyalanması yok, artık onu yalnızca değişirse kopyalıyoruz! Öyleyse şimdi ne yapmalıyız? Arayüz değiştirilsin mi? Tamam, bu düzeltme nedeniyle sınıf arayüzünü değiştirelim. Ya sınıfımız bu yöntemi soyut bir arayüzden miras aldıysa? Orada da değiştirelim! Uygulama değiştiği için çok fazla değişiklik var mı?

Yine bana itiraz edebilirler, diyorlar yazar, bu durum orada işe yarayacakken neden maçlardan para biriktirmeye çalışıyorsunuz? Evet, çağrıların çoğu yanlış olacak! Buna güven var mı? Nerede? Ve eğer maçlardan tasarruf etmeye karar verdiysem, PPZ kullanmamız tam olarak bu tür tasarrufların bir sonucu değil miydi? Verimliliği savunan “parti çizgisi”ni sürdürüyorum.

İnşaatçılar

Özellikle clang-tidy'de onlar için özel bir kural olduğu için diğer metodlar/fonksiyonlar için henüz işe yaramayan yapıcılara kısaca göz atalım. Diyelim ki şöyle bir sınıfımız var:

Class JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) özel: string m_JustString; );

Açıkçası, parametre kopyalanmıştır ve clang-tidy bize yapıcıyı şu şekilde yeniden yazmanın iyi bir fikir olacağını söyleyecektir:

JustClass(string justString): m_JustString(move(justString)) ( )

Ve açıkçası, burada tartışmak benim için zor - sonuçta, gerçekten her zaman kopyalıyoruz. Ve çoğu zaman, bir şeyi yapıcı aracılığıyla ilettiğimizde onu kopyalarız. Ancak daha sık olması her zaman anlamına gelmez. İşte başka bir örnek:

Class TimeSpan ( public: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(bitiş); ​​) Private: DateTime m_Start; DateTime m_End; );

Burada her zaman kopyalamıyoruz, yalnızca tarihler doğru şekilde sunulduğunda kopyalıyoruz. Elbette vakaların büyük çoğunluğunda durum böyle olacaktır. Ancak her zaman değil.

Başka bir örnek verebilirsiniz ama bu sefer kodsuz. Büyük bir nesneyi kabul eden bir sınıfınız olduğunu hayal edin. Sınıf uzun zamandır var ve şimdi uygulamasını güncellemenin zamanı geldi. Büyük bir tesisin (yıllar geçtikçe büyüyen) yarısından fazlasına, hatta belki daha azına ihtiyacımızın olmadığının farkındayız. Değere göre geçiş yaparak bu konuda bir şeyler yapabilir miyiz? Hayır, hiçbir şey yapamayacağız çünkü yine de bir kopya oluşturulacak. Ancak PPSC kullansaydık yaptığımız şeyi değiştirirdik içeri tasarımcı. Ve kilit nokta da budur: PPSC'yi kullanarak fonksiyonumuzun (yapıcı) uygulanmasında ne ve ne zaman olacağını kontrol ederiz, ancak PPZ kullanırsak kopyalama üzerindeki tüm kontrolümüzü kaybederiz.

Bu bölümden ne çıkarabilirsiniz? "Eğer yine de kopyalarsak..." argümanının oldukça tartışmalı olduğu gerçeği, çünkü Neyi kopyalayacağımızı her zaman bilemeyiz ve bilsek bile çoğu zaman bunun gelecekte de devam edeceğinden emin olamayız.

Taşınmak ucuz

Hareketin semantiğinin ortaya çıktığı andan itibaren, modern C++ kodunun yazılma şekli üzerinde ciddi bir etkisi olmaya başladı ve zamanla bu etki daha da yoğunlaştı: Bunda şaşılacak bir şey yok, çünkü hareket öyle bir şeydir ki ucuz kopyalamayla karşılaştırılmıştır. Ama öyle mi? Hareketin olduğu doğru mu? Her zaman ucuz ameliyat mı? Bu bölümde anlamaya çalışacağımız şey budur.

İkili Büyük Nesne

Önemsiz bir örnekle başlayalım, diyelim ki aşağıdaki sınıfa sahibiz:

Struct Blob ( std::array veri; );

Sıradan damla(BDO, İngilizce BLOB), çeşitli durumlarda kullanılabilir. Referans ve değer bazında geçmenin bize maliyetinin ne olacağına bakalım. BDO'muz şu şekilde kullanılacak:

Void Depolama::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) void Storage::setBlobByVal(Blob blob) ( m_Blob = move(blob); )

Ve bu fonksiyonları şu şekilde çağıracağız:

Const Blob blob(); depolamak; depolama.setBlobByRef(blob); depolama.setBlobByVal(blob);

Diğer örneklerin kodu bununla aynı olacak, yalnızca farklı isimler ve türlerle olacak, bu yüzden geri kalan örnekler için bunu vermeyeceğim - her şey depoda.

Ölçümlere geçmeden önce sonucu tahmin etmeye çalışalım. Yani, Storage sınıfı nesnesinde depolamak istediğimiz 4 KB'lık bir std::array'imiz var. Daha önce öğrendiğimiz gibi, PPSC için bir kopyamız olacak, PPZ için ise bir kopyamız ve bir hamlemiz olacak. Diziyi taşımanın imkansız olduğu gerçeğine dayanarak, PPSC için bir kopyaya karşı PPZ için 2 kopya olacaktır. Onlar. PPSC'nin performansında iki kat üstünlük bekleyebiliriz.

Şimdi test sonuçlarına bakalım:

Bu ve sonraki tüm testler, MSVS 2017 (15.7.2) ve /O2 bayrağı kullanılarak aynı makinede gerçekleştirildi.

Uygulama varsayımla örtüşüyordu - değere göre geçiş 2 kat daha pahalıdır, çünkü bir dizi için hareket etmek tamamen kopyalamaya eşdeğerdir.

Astar

Başka bir örneğe bakalım, normal bir std::string . Ne bekleyebiliriz? Modern uygulamaların iki tür dize arasında ayrım yaptığını biliyoruz (bunu makalede tartıştım): kısa (yaklaşık 16 karakter) ve uzun (kısadan daha uzun olanlar). Kısa olanlar için, char'ın normal bir C dizisi olan dahili bir arabellek kullanılır, ancak uzun olanlar zaten yığına yerleştirilecektir. Kısa çizgilerle ilgilenmiyoruz çünkü... sonuç BDO ile aynı olacaktır, o yüzden uzun çizgilere odaklanalım.

Dolayısıyla, uzun bir dizeye sahip olduğunuzda, onu taşımanın oldukça ucuz olması gerektiği açıktır (sadece işaretçiyi hareket ettirin), böylece dizeyi hareket ettirmenin sonuçları hiç etkilemeyeceğine ve PPZ'nin bir sonuç vermesi gerektiğine güvenebilirsiniz. PPSC'den daha kötü değil. Bunu pratikte kontrol edelim ve aşağıdaki sonuçları elde edelim:

Bu “olgusu” açıklamaya devam edeceğiz. Peki mevcut bir dizeyi zaten var olan bir dizeye kopyaladığımızda ne olur? Önemsiz bir örneğe bakalım:

Önce dize(64, "C"); string saniye(64, "N"); //... ikinci = birinci;

İki adet 64 karakterlik dizemiz var, bu nedenle bunları oluştururken dahili arabellek yetersiz kalıyor, bu da her iki dizenin de yığında tahsis edilmesine neden oluyor. Şimdi birinciden ikinciye kopyalıyoruz. Çünkü satır boyutlarımız aynı, açıkçası birinciden gelen tüm verileri barındırmak için ikinciye yeterli alan ayrılmış, yani ikinci = birinci; sıradan bir anı olacak, başka bir şey değil. Ancak biraz değiştirilmiş bir örneğe bakarsak:

Önce dize(64, "C"); dize ikinci = birinci;

o zaman artık operatör= çağrısı olmayacak, ancak kopya yapıcısı çağrılacak. Çünkü Bir kurucu ile karşı karşıya olduğumuz için onun içinde mevcut bir hafıza yoktur. Önce seçilmesi ve ancak daha sonra kopyalanması gerekir. Onlar. bu bellek ayırma ve ardından memcpy'dir. Sizin ve benim bildiğimiz gibi, küresel yığına bellek tahsis etmek genellikle pahalı bir işlemdir, dolayısıyla ikinci örnekten kopyalamak, ilkinden kopyalamaktan daha pahalı olacaktır. Yığın bellek tahsisi başına daha pahalıdır.

Bunun konumuzla ne alakası var? En doğrudan olanı, çünkü ilk örnek tam olarak PPSC'de ne olduğunu gösterir ve ikincisi PPZ'de ne olduğunu gösterir: PPZ için her zaman yeni bir satır oluşturulur, PPSC için ise mevcut satır yeniden kullanılır. Uygulama süresindeki farkı zaten gördünüz, dolayısıyla buraya eklenecek bir şey yok.

Burada yine PPP'yi kullanırken bağlam dışında çalıştığımız ve bu nedenle sağlayabileceği tüm faydalardan yararlanamadığımız gerçeğiyle karşı karşıyayız. Ve eğer daha önce teorik olarak gelecekteki değişiklikler açısından mantık yürüttüysek, burada üretkenlikte çok somut bir başarısızlık gözlemliyoruz.

Elbette birisi bana string'in ayrı durduğu ve çoğu türün bu şekilde çalışmadığı konusunda itiraz edebilir. Buna şu cevabı verebilirim: Daha önce açıklanan her şey, bir öğe paketi için yığında hemen bellek ayıran herhangi bir kap için geçerli olacaktır. Ayrıca, diğer türlerde başka hangi bağlama duyarlı optimizasyonların kullanıldığını kim bilebilir?

Bu bölümden ne çıkarmalısınız? Taşımanın gerçekten ucuz olması, kopyalamayı kopyalama+taşımayla değiştirmenin performans açısından her zaman karşılaştırılabilir bir sonuç vereceği anlamına gelmez.

Karmaşık tip

Son olarak birden fazla nesneden oluşacak bir türe bakalım. Bu, bir kişinin doğasında bulunan verilerden oluşan Person sınıfı olsun. Genellikle bu adınız, soyadınız, posta kodunuz vb.'dir. Tüm bunları dizeler olarak temsil edebilir ve Person sınıfının alanlarına koyduğunuz dizelerin büyük olasılıkla kısa olacağını varsayabilirsiniz. Her ne kadar gerçek hayatta kısa sicimleri ölçmenin en yararlısı olacağına inansam da, daha eksiksiz bir resim elde etmek için yine de farklı boyutlardaki sicimlere bakacağız.

Person'ı da 10 alanlı kullanacağım ancak bunun için doğrudan sınıf gövdesinde 10 alan oluşturmayacağım. Person'ın uygulanması, bir kabı derinliklerinde gizler; bu, Person gerçek bir sınıf olsaydı nasıl çalışacağından pratik olarak sapmadan, test parametrelerini değiştirmeyi daha kolay hale getirir. Ancak uygulama mevcuttur ve her zaman kodu kontrol edebilir ve yanlış bir şey yapıp yapmadığımı bana söyleyebilirsiniz.

Hadi başlayalım: PPSC ve PPZ kullanarak Storage'a aktardığımız string türünde 10 alanlı kişi:

Gördüğünüz gibi performansta çok büyük bir fark var ve bu önceki bölümlerden sonra okuyucular için sürpriz olmamalı. Ayrıca Person sınıfının, bu tür sonuçların soyut olarak göz ardı edilmeyeceği kadar "gerçek" olduğuna inanıyorum.

Bu arada, bu makaleyi hazırlarken başka bir örnek daha hazırladım: birkaç std::function nesnesi kullanan bir sınıf. Benim fikrime göre, PPSC'nin PPZ'ye göre performansında da bir avantaj göstermesi gerekiyordu, ancak tam tersi çıktı! Ancak sonuçları beğenmediğim için değil, neden bu tür sonuçların elde edildiğini anlamaya vaktim olmadığı için bu örneği burada vermiyorum. Bununla birlikte, depoda (Yazıcılar) kodlar ve testler de var, eğer biri bunu çözmek isterse, araştırmanın sonuçlarını duymaktan memnuniyet duyarım. Bu örneğe daha sonra dönmeyi planlıyorum ve bu sonuçları benden önce kimse yayınlamazsa ayrı bir makalede ele alacağım.

Sonuçlar

Dolayısıyla değere göre geçmenin ve bir sabite referansla geçmenin çeşitli artılarını ve eksilerini inceledik. Bazı örneklere baktık ve bu örneklerde her iki yöntemin performansına baktık. Elbette bu makale kapsamlı olamaz ve kapsamlı değildir, ancak bana göre hangi yöntemin kullanılmasının en iyi olduğu konusunda bağımsız ve bilinçli bir karar vermek için yeterli bilgi içermektedir. Birisi itiraz edebilir: "Neden tek bir yöntem kullanalım, hadi görevden başlayalım!" Genel olarak bu teze katılıyorum ama bu durumda katılmıyorum. Bir dilde argümanları iletmenin yalnızca tek bir yolu olabileceğine inanıyorum: varsayılan olarak kullanılan.

Varsayılan ne anlama geliyor? Bu, bir fonksiyon yazdığımda argümanı nasıl iletmem gerektiğini düşünmediğim, sadece "varsayılanı" kullandığım anlamına geliyor. C++ dili birçok insanın kaçındığı oldukça karmaşık bir dildir. Ve benim görüşüme göre, karmaşıklık, dilde var olan dil yapılarının karmaşıklığından değil (tipik bir programcı bunlarla asla karşılaşmayabilir), dilin sizi çok fazla düşünmeye sevk etmesinden kaynaklanıyor: özgürleştim mi? hafızayı doldurun, bu işlevi burada kullanmak pahalı mı? vb.

Birçok programcı (C, C++ ve diğerleri) 2011'den sonra ortaya çıkmaya başlayan C++'a güvenmiyor ve korkuyor. Dilin daha karmaşık hale geldiği, artık yalnızca “guruların” yazı yazabildiği vb. gibi pek çok eleştiri duydum. Şahsen ben bunun böyle olmadığına inanıyorum; tam tersine, komite dili yeni başlayanlar için daha kolay hale getirmek için çok zaman ayırıyor ve böylece programcıların dilin özellikleri hakkında daha az düşünmesi gerekiyor. Sonuçta, eğer dille uğraşmak zorunda değilsek, o zaman görev hakkında düşünecek zamanımız olur. Bu basitleştirmeler arasında akıllı işaretçiler, lambda işlevleri ve dilde görünen çok daha fazlası yer alıyor. Aynı zamanda artık daha fazla çalışmamız gerektiği gerçeğini de inkar etmiyorum ama çalışmanın nesi yanlış? Yoksa diğer popüler dillerde öğrenilmesi gereken herhangi bir değişiklik olmuyor mu?

Ayrıca yanıt olarak şunu söyleyebilecek züppelerin olacağından hiç şüphem yok: “Düşünmek istemiyor musun? O zaman git PHP'de yaz.” Böyle insanlara cevap bile vermek istemiyorum. Oyunun gerçekliğinden sadece bir örnek vereceğim: Starcraft'ın ilk bölümünde, bir binada yeni bir işçi yaratıldığında, mineralleri (veya gazı) çıkarmaya başlaması için oraya manuel olarak gönderilmesi gerekiyordu. Dahası, her bir mineral paketinin, işçilerdeki artışın faydasız olduğu bir sınırı vardı ve hatta birbirlerine müdahale ederek üretimi kötüleştirebiliyorlardı. Bu, Starcraft 2'de değiştirildi: işçiler otomatik olarak maden (veya gaz) çıkarmaya başlıyor ve aynı zamanda şu anda kaç işçinin madencilik yaptığını ve bu depozitonun limitinin ne kadar olduğunu da gösteriyor. Bu, oyuncunun üsle etkileşimini büyük ölçüde basitleştirerek oyunun daha önemli yönlerine odaklanmasını sağladı: üs inşa etmek, asker toplamak ve düşmanı yok etmek. Görünüşe göre bu sadece harika bir yenilik, ama internette başlayan şey! İnsanlar (onlar kim?) oyunun "berbat olduğunu" ve "Starcraft'ı öldürdüklerini" bağırmaya başladılar. Açıkçası, bu tür mesajlar yalnızca "gizli bilginin koruyucularından" ve bazı "seçkin" kulüplerde olmayı seven "yüksek APM ustalarından" gelebilirdi.

Konumuza dönersek, nasıl kod yazacağımı düşünmeye ne kadar az ihtiyacım olursa, acil sorunu çözmeyi düşünmeye o kadar çok zamanım olur. Hangi yöntemi kullanmam gerektiğini düşünmek - PPSC veya PPZ - sorunu çözmeye beni bir nebze olsun yaklaştırmıyor, bu yüzden bu tür şeyler hakkında düşünmeyi reddediyorum ve bir seçeneği seçiyorum: bir sabite referansla geçmek. Neden? Çünkü genel durumlarda PPP'nin bir avantajını görmüyorum, özel durumların ayrı değerlendirilmesi gerekiyor.

Bu özel bir durum, sadece bazı yöntemlerde PPSC'nin bir darboğaz haline geldiğini ve iletimi PPZ'ye değiştirerek performansta önemli bir artış elde edeceğimizi fark ettiğimden, kullanmaktan çekinmiyorum. PPZ. Ancak varsayılan olarak PPSC'yi hem normal işlevlerde hem de yapıcılarda kullanacağım. Ve eğer mümkünse, mümkün olan her yerde bu özel yöntemi tanıtacağım. Neden? Çünkü PPP'yi teşvik etme uygulamasının, programcıların aslan payının çok bilgili olmaması (ya prensip olarak ya da henüz işlerin gidişatına girmemiş olmaları) ve sadece tavsiyelere uymaları nedeniyle kısır olduğunu düşünüyorum. Ayrıca, birbiriyle çelişen birkaç tavsiye varsa, daha basit olanı seçerler ve bu da sırf birisi bir yerlerde bir şeyler duymuş diye kodda karamsarlığa yol açar. Ah evet, bu kişi aynı zamanda Abrahams'ın haklı olduğunu kanıtlamak için makalesine bir bağlantı da sağlayabilir. Ve sonra oturup kodu okursunuz ve düşünürsünüz: parametrenin buraya değere göre iletilmesinin nedeni bunu yazan programcının Java'dan gelmesi mi, sadece çok sayıda "akıllı" makale okuması mı, yoksa gerçekten bir ihtiyaç var mı? teknik özellik?

PPSC'nin okunması çok daha kolaydır: kişi C++'ın "iyi biçimini" açıkça bilir ve biz yolumuza devam ederiz - bakış oyalanmaz. PPSC kullanma pratiği C++ programcılarına yıllardır öğretiliyor, bundan vazgeçmenin nedeni nedir? Bu beni başka bir sonuca götürüyor: Eğer bir yöntem arayüzü PPP kullanıyorsa, o zaman bunun neden böyle olduğuna dair bir yorum da olmalıdır. Diğer durumlarda PPSC uygulanmalıdır. Elbette istisna türleri var, ancak burada yalnızca ima edildikleri için bunlardan bahsetmiyorum: string_view , başlatıcı_list , çeşitli yineleyiciler vb. Ancak bunlar istisnalardır ve projede hangi türlerin kullanıldığına bağlı olarak listesi genişleyebilir. Ancak özü C++98'den bu yana aynı kalıyor: Varsayılan olarak her zaman PPCS kullanıyoruz.

Std::string için küçük stringlerde büyük ihtimalle bir fark olmayacak, buna daha sonra değineceğiz.

"Nokta yerleştirme" konusundaki iddialı açıklama için şimdiden özür dilerim, ancak sizi bir şekilde makaleye çekmemiz gerekiyor)) Kendi adıma, özetin hala beklentilerinizi karşıladığından emin olmaya çalışacağım.

Kısaca neyden bahsediyoruz

Bunu zaten herkes biliyor, ancak başlangıçta size yöntem parametrelerinin 1C'de nasıl aktarılabileceğini hatırlatacağım. “Referansa göre” veya “değere göre” iletilebilirler. İlk durumda, yönteme çağrı noktasındaki değerin aynısını, ikincisinde ise onun bir kopyasını iletiriz.

Varsayılan olarak, 1C'de argümanlar referans olarak iletilir ve bir yöntemin içindeki bir parametrede yapılan değişiklikler, yöntemin dışından görülebilir. Burada sorunun daha iyi anlaşılması, “parametre değişikliği” sözcüğünden tam olarak ne anladığınıza bağlıdır. Yani bu, yeniden atama anlamına gelir, başka bir şey değil. Ayrıca atama, örneğin çıktı parametresinde bir şey döndüren bir platform yönteminin çağrılması gibi örtülü olabilir.

Ancak parametremizin referans olarak iletilmesini istemiyorsak o zaman parametreden önce bir anahtar kelime belirtebiliriz. Anlam

Prosedür ByValue(Değer Parametresi) Parametre = 2; Prosedür Sonu Parametresi = 1; ByValue(Parametre); Rapor(Parametre); // 1 yazdıracak

Her şey söz verildiği gibi çalışır; parametre değerinin değiştirilmesi (veya daha doğrusu "değiştirilmesi"), yöntemin dışındaki değeri değiştirmez.

Peki şaka ne?

Parametre olarak ilkel türleri (dizeler, sayılar, tarihler vb.) değil, nesneleri aktarmaya başladığımızda ilginç anlar başlar. Bir nesnenin "sığ" ve "derin" kopyaları gibi kavramların yanı sıra işaretçilerin (C++ terimleriyle değil, soyut tanıtıcılar olarak) devreye girdiği yer burasıdır.

Bir nesneyi (örneğin bir Değer Tablosu) referans olarak iletirken, nesneyi platformun belleğinde "tutan" işaretçi değerinin kendisini (belirli bir tanıtıcı) iletiriz. Değere göre iletildiğinde platform bu işaretçinin bir kopyasını oluşturacaktır.

Başka bir deyişle, bir yöntemde bir nesneyi referans olarak ileterek parametreye "Array" değerini atarsak, çağrı noktasında bir dizi alırız. Referans tarafından iletilen değerin yeniden atanması çağrı konumundan görülebilir.

Prosedür ProcessValue(Parameter) Parametre = Yeni Dizi; Prosedür Sonu Tablosu = Yeni Değer Tablosu; ProcessValue(Tablo); Rapor(ValueType(Tablo)); // bir Dizi çıktısı alacak

Nesneyi değere göre iletirsek, çağrı noktasında Değer Tablomuz kaybolmayacaktır.

Nesne içeriği ve durumu

Değere göre geçerken nesnenin tamamı kopyalanmaz, yalnızca işaretçisi kopyalanır. Nesne örneği aynı kalır. Nesneyi referansa veya değere göre nasıl ilettiğiniz önemli değildir; değer tablosunu temizlemek tablonun kendisini temizleyecektir. Bu temizlik her yerde görülecek çünkü... yalnızca tek bir nesne vardı ve yönteme tam olarak nasıl aktarıldığı önemli değildi.

Prosedür ProcessValue(Parameter) Parameter.Clear(); Prosedür Sonu Tablosu = Yeni Değer Tablosu; Table.Add(); ProcessValue(Tablo); Rapor(Table.Quantity()); // çıktı 0 olacak

Nesneleri yöntemlere geçirirken platform işaretçilerle çalışır (koşulludur, C++'ın doğrudan analogları değil). Bir nesne referans olarak iletilirse, nesnenin bulunduğu 1C sanal makinenin bellek hücresinin üzerine başka bir nesne yazılabilir. Bir nesne değere göre iletilirse, işaretçi kopyalanır ve nesnenin üzerine yazmak, orijinal nesnenin bellek konumunun üzerine yazılmasıyla sonuçlanmaz.

Aynı zamanda herhangi bir değişiklik durum nesne (temizleme, özellik ekleme vb.) nesnenin kendisini değiştirir ve nesnenin nasıl ve nereye aktarıldığıyla hiçbir ilgisi yoktur. Bir nesne örneğinin durumu değişti; ona yönelik birçok "referans" ve "değer" olabilir, ancak örnek her zaman aynıdır. Bir nesneyi bir yönteme aktararak nesnenin tamamının bir kopyasını oluşturmayız.

Ve bu her zaman doğrudur, ancak...

İstemci-sunucu etkileşimi

Platform, sunucu çağrılarını oldukça şeffaf bir şekilde uygular. Basitçe bir yöntem çağırırız ve platform, yöntemin tüm parametrelerini serileştirir (bir dizeye dönüştürür), bunları sunucuya iletir ve ardından çıktı parametrelerini, seri durumdan çıkarılıp aynı şekilde yayınlandıkları istemciye geri gönderir. eğer herhangi bir sunucuya hiç gitmemiş olsalardı.

Bildiğiniz gibi tüm platform nesneleri serileştirilebilir değildir. Sınırlamanın büyüdüğü yer burasıdır: tüm nesneler istemciden sunucu yöntemine aktarılamaz. Serileştirilemeyen bir nesneyi iletirseniz platform kötü kelimeler kullanmaya başlayacaktır.

  • Programcının niyetinin açık bir beyanı. Yöntem imzasına bakarak hangi parametrelerin girildiğini ve hangilerinin çıktı olduğunu açıkça anlayabilirsiniz. Bu kodun okunması ve bakımı daha kolaydır
  • Sunucudaki “referansa göre” parametresindeki bir değişikliğin istemcideki çağrı noktasında görülebilmesi için, p Platformun kendisi, makalenin başında açıklanan davranışı sağlamak için istemciye bağlantı yoluyla sunucuya iletilen parametreleri mutlaka döndürecektir. Parametrenin döndürülmesine gerek yoksa trafik taşması yaşanacaktır. Veri alışverişini optimize etmek için çıktıda değerlerine ihtiyaç duymadığımız parametreler Değer kelimesiyle işaretlenmelidir.

Burada ikinci nokta dikkat çekicidir. Trafiği optimize etmek için, parametre Değer sözcüğüyle işaretlenmişse platform, parametre değerini istemciye döndürmez. Bunların hepsi harika, ama ilginç bir etkiye yol açıyor.

Daha önce de söylediğim gibi, bir nesne sunucuya aktarıldığında serileştirme gerçekleşir, yani. nesnenin "derin" bir kopyası gerçekleştirilir. Ve eğer bir kelime varsa Anlam nesne sunucudan istemciye geri gitmeyecektir. Bu iki gerçeği eklersek aşağıdakileri elde ederiz:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Prosedürü ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Prosedürü CheckValue() List1= New ListValues; List1.Add("merhaba"); Liste2 = Liste1.Kopya(); Liste3 = Liste1.Kopya(); // nesne tamamen kopyalanır, // sunucuya aktarılır ve sonra geri döndürülür. // listenin temizlenmesi çağrı noktasında görülebilir ByRef(List1); // nesne tamamen kopyalanır, // sunucuya aktarılır. Geri gelmiyor. // ByValue(List2) çağrıldığında listenin temizlenmesi GÖRÜNÜR DEĞİLDİR; // yalnızca nesne işaretçisi kopyalanır // listenin temizlenmesi ByValueClient(List3) çağrısının yapıldığı noktada görünür; Rapor(List1.Quantity()); Rapor(List2.Quantity()); Rapor(List3.Quantity()); Prosedürün Sonu

Özet

Kısaca şu şekilde özetlenebilir:

  • Referansa göre geçmek, bir nesnenin üzerine tamamen farklı bir nesne yazmanıza olanak tanır
  • Değere göre geçmek, nesnenin "üzerine yazmanıza" izin vermez, ancak nesnenin iç durumundaki değişiklikler görünür olacaktır çünkü aynı nesne örneğiyle çalışıyoruz
  • Bir sunucu çağrısı yapılırken, nesnenin FARKLI örnekleriyle iş yapılır, çünkü Derin bir kopyalama gerçekleştirildi. Anahtar kelime Anlam sunucu örneğinin istemci örneğine geri kopyalanmasını engelleyecektir ve sunucudaki bir nesnenin dahili durumunu değiştirmek, istemcide benzer bir değişikliğe yol açmayacaktır.

Bu basit kural listesinin meslektaşlarınızla parametrelerin "değere göre" ve "referans olarak" iletilmesine ilişkin anlaşmazlıkları çözmenizi kolaylaştıracağını umuyorum.