C ++ 'da hareket yapıcılarının motivasyonu ve kullanımı


17

Son zamanlarda C ++ 'ta hareket yapıcılar hakkında okuyordum (örneğin buraya bakın ) ve nasıl çalıştıklarını ve ne zaman kullanmaları gerektiğini anlamaya çalışıyorum.

Anladığım kadarıyla, büyük nesnelerin kopyalanmasından kaynaklanan performans sorunlarını hafifletmek için bir hareket oluşturucu kullanılır. Wikipedia sayfası şöyle diyor: "C ++ 03 ile ilgili kronik bir performans sorunu, nesneler değere göre iletildiğinde örtük olarak oluşabilecek maliyetli ve gereksiz derin kopyalardır."

Normalde bu tür durumlara hitap ediyorum

  • nesneleri referans olarak ileterek veya
  • nesnenin etrafından geçmek için akıllı işaretçiler (örn. boost :: shared_ptr) kullanarak (akıllı işaretçiler nesne yerine kopyalanır).

Yukarıdaki iki tekniğin yeterli olmadığı ve bir hareket yapıcısının kullanılmasının daha uygun olduğu durumlar nelerdir?


1
Hareket semantiğinin çok daha fazlasını başarabilmesinin yanı sıra (cevaplarda belirtildiği gibi), referans veya akıllı işaretçi ile geçmenin yeterli olmadığı durumları sormamalısınız, ancak bu teknikler gerçekten en iyi ve en temiz yolduysa bunu yapmak (tanrı shared_ptrsadece hızlı kopyalama uğruna sakının ) ve eğer hareket semantiği neredeyse hiç kodlama, anlambilim ve temizlik cezası olmadan bunu başarabilirse.
Chris, Reinstate Monica'nın

Yanıtlar:


16

Semantikleri taşı, C ++ 'a bir boyut getirir - değerleri ucuza döndürmenize izin vermek için orada değildir.

Örneğin, hareket semantiği olmadan std::unique_ptrişe yaramaz - bakın, std::auto_ptrhareket semantiğinin tanıtımı ile kaldırılmış ve C ++ 17'de kaldırılmıştır. Bir kaynağı taşımak, kopyalamaktan çok farklıdır. Benzersiz bir öğenin sahipliğinin aktarılmasına izin verir .

Örneğin std::unique_ptr, oldukça iyi tartışıldığı için bakmayalım. Diyelim ki OpenGL'de bir Vertex Buffer Nesnesi. Bir tepe tamponu GPU'daki belleği temsil eder - özel işlevler kullanılarak tahsis edilmesi ve yeniden yerleştirilmesi gerekir, muhtemelen ne kadar süre yaşayabileceğine dair sıkı kısıtlamalar vardır. Sadece bir sahibin kullanması da önemlidir.

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

Şimdi, bu olabilir bir ile yapılabilir std::shared_ptr- ama bu kaynağın paylaşılmasının değildir. Bu, paylaşılan bir işaretçi kullanmayı kafa karıştırıcı hale getirir. Kullanabilirsiniz std::unique_ptr, ancak yine de hareket semantiği gerekir.

Açıkçası, bir hamle yapıcısı uygulamadım, ama fikri anladınız.

Burada önemli olan, bazı kaynakların kopyalanamamasıdır . Taşımak yerine işaretçilerden geçebilirsiniz, ancak unique_ptr kullanmadığınız sürece sahiplik sorunu vardır. Kodun amacının ne olduğu konusunda olabildiğince açık olmak önemlidir, bu nedenle bir hareket oluşturucu muhtemelen en iyi yaklaşımdır.


Cevap için teşekkürler. Burada paylaşılan bir işaretçi kullanılırsa ne olur?
Giorgio

Kendime cevap vermeye çalışıyorum: paylaşılan bir işaretçi kullanmak, nesnenin yalnızca belirli bir süre yaşayabileceği bir gereklilik iken, nesnenin ömrünü denetlemeye izin vermez.
Giorgio

3
You @Giorgio olabilir paylaşılan gösterici kullanmak, ancak semantik yanlış olur. Bir arabellek paylaşmak mümkün değildir. Ayrıca, bu aslında bir işaretçiyi bir işaretçiye geçirmenizi sağlayacaktır (vbo temel olarak GPU belleğine benzersiz bir işaretçi olduğundan). Kodunuzu daha sonra görüntüleyen biri 'Neden burada paylaşılan bir işaretçi var? Paylaşılan bir kaynak mı? Bu bir hata olabilir! '' Orijinal niyetin ne olduğu konusunda olabildiğince açık olmak daha iyidir.
Maksimum

@Giorgio Evet, bu da şartın bir parçası. Bu durumda 'oluşturucu' bazı kaynakları ayırmak istediğinde (GPU'daki yeni nesneler için yeterli bellek olmayabilir), bellekte başka bir tanıtıcı olmamalıdır. Kapsam dışından geçen bir paylaşılan_ptr kullanmak, başka bir yerde tutmazsanız işe yarayacaktır, ancak neden yapabildiğinizde tamamen açık hale getirmeyesiniz?
Maksimum

@Giorgio Düzenleme konusundaki başka bir denemeye bakın.
Maksimum

5

Hareket semantiği, bir değeri döndürürken bir o kadar büyük bir gelişme olmayabilir - ve bir shared_ptr(veya benzer bir şey) kullandığınızda / kullandığınızda , muhtemelen erken kararlaştırırsınız. Gerçekte, neredeyse tüm makul modern derleyiciler Dönüş Değeri Optimizasyonu (RVO) ve Adlandırılmış Dönüş Değeri Optimizasyonu (NRVO) olarak adlandırılanları yapar. Bu araçlar Bir değer döndüren yerine aslında değerini kopyalayarak yaparken o hiç, yalnızca dönüşten sonra değerin atanacağı yere gizli bir işaretçi / referans iletirler ve işlev, sonuna kadar değer oluşturmak için bunu kullanır. C ++ standardı buna izin vermek için özel hükümler içerir, bu nedenle (örneğin) kopya kurucunuzun görünür yan etkileri olsa bile, değeri döndürmek için kopya kurucusunu kullanmanız gerekmez. Örneğin:

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

Buradaki temel fikir oldukça basittir: Mümkünse kopyalamaktan kaçınmak için yeterli içeriğe sahip bir sınıf oluşturun ( std::vector32767 rastgele ints ile dolduruyoruz). Ne zaman kopyalanıp kopyalanmayacağını bize gösteren açık bir kopyalayıcıya sahibiz. Ayrıca, nesnede rastgele değerlerle bir şeyler yapmak için biraz daha kodumuz var, bu nedenle optimizer, hiçbir şey yapmadığı için sınıfla ilgili her şeyi (en azından kolayca) ortadan kaldırmayacak.

Daha sonra, bu nesnelerden birini bir işlevden döndürmek için bazı kodlarımız var ve daha sonra, nesnenin gerçekten tamamen yok sayılmakla kalmayıp, gerçekten oluşturulduğundan emin olmak için toplamı kullanıyoruz. Bunu çalıştırdığımızda, en azından en yeni / modern derleyicilerle, yazdığımız kopya yapıcısının hiç bir zaman çalışmadığını görüyoruz - ve evet, a ile hızlı bir kopyanın bile kopyalama yapmaktan shared_ptrdaha yavaş olduğundan eminim hiç.

Taşınma, onlar olmadan (doğrudan) yapamayacağınız çok sayıda şeyi yapmanızı sağlar. Harici bir birleştirme türünün "birleştirme" bölümünü düşünün; örneğin, birleştireceğiniz 8 dosyanız vardır. İdeal olarak, bu dosyaların 8 tanesini bir vector- içine koymak istersiniz, ancak vector(C ++ 03 itibarıyla) öğeleri kopyalayabilmesi ifstreamgerektiğinden ve kopyalanamayacağı için bazı unique_ptr/ shared_ptr, veya bunları bir vektöre koyabilmek için bu düzende bir şey. Not bile biz (örneğin) eğer reserveuzayda vectoremin bizim ediyoruz böylece ifstreamkod rağmen derlemek olmayacak şekilde, s gerçekten kopyalanmış asla, derleyici bunu bilmeyecek biz kopya yapıcı asla biliyorum yine de kullanılır.

Hala kopyalanamaz bile, C ++ 11 bir ifstream olabilir taşınacak. Bu durumda, nesneler muhtemelen olmaz hiç taşınacak, ama bizim koymak, böylece gerekirse olabilecekleri gerçeği, derleyici mutlu ediyor ifstreambir nesne vectordoğrudan herhangi akıllı işaretçi hacks.

Bir vektör yapar genişletmek hareket semantik gerçekten olabileceğini bir zamanın oldukça iyi bir örnektir / kullanışlı olsa vardır. Bu durumda, RVO / NRVO yardımcı olmaz, çünkü bir işlevden (veya çok benzer bir şeyden) dönüş değeri ile ilgilenmiyoruz. Bazı nesneleri tutan bir vektörünüz var ve bu nesneleri yeni, daha büyük bir bellek yığınına taşımak istiyoruz.

C ++ 03'te bu, yeni bellekteki nesnelerin kopyalarını oluşturarak ve ardından eski bellekteki eski nesneleri yok ederek yapılır. Ancak tüm bu kopyaları eskilerini atmak için yapmak oldukça zaman kaybıydı. C ++ 11'de, bunun yerine taşınmasını bekleyebilirsiniz. Bu genellikle özünde, (genellikle çok daha yavaş) derin bir kopya yerine sığ bir kopya yapmamızı sağlar. Başka bir deyişle, bir dize veya vektörle (yalnızca birkaç örnek için), işaretçilerin başvurduğu tüm verilerin kopyalarını yapmak yerine yalnızca işaretçiyi / nesneleri nesnelere kopyalarız.


Çok ayrıntılı açıklama için teşekkürler. Doğru anlarsam, hareketin devreye girdiği tüm durumlar normal işaretçiler tarafından ele alınabilir, ancak her seferinde tüm işaretçiyi hokkabazlık programlamak güvenli değildir (karmaşık ve hata eğilimli). Bu nedenle, bunun yerine, kaputun altında bazı unique_ptr (veya benzer bir mekanizma) vardır ve hareket semantiği, günün sonunda sadece işaretçi kopyalamanın ve nesne kopyalamanın olmamasını sağlar.
Giorgio

@Giorgio: Evet, bu oldukça doğru. Dil, hareket semantiği eklemez; değer referansları ekler. Bir rvalue referansı (açıkçası yeterli) bir rvalue'ya bağlanabilir, bu durumda verilerin dahili sunumunu "çalmanın" ve derin bir kopya yapmak yerine sadece işaretleyicilerini kopyalamanın güvenli olduğunu bilirsiniz.
Jerry Coffin

4

Düşünmek:

vector<string> v;

V'ye dizeler eklenirken, gerektiğinde genişler ve her yeniden tahsisde dizelerin kopyalanması gerekir. Move yapıcıları ile, bu temelde bir sorun değildir.

Tabii ki, şöyle de yapabilirsiniz:

vector<unique_ptr<string>> v;

Ama bu sadece iyi çalışır çünkü std::unique_ptruygulayıcılar yapıcıyı hareket ettirir.

Kullanımı std::shared_ptryalnızca (nadiren) sahiplik paylaştığınız durumlarda mantıklıdır.


Peki ya bunun yerine 30 veri üyesi stringolan bir örneğimiz olsaydı Foo? unique_ptrVersiyon daha verimli olmaz mı?
Vassilis

2

Dönüş değerleri, çoğunlukla bir tür referans yerine değere göre geçmek istediğim yerdir. Büyük bir performans cezası olmadan bir nesneyi 'yığıntaki' hızlı bir şekilde iade edebilmek güzel olurdu. Öte yandan, bunun üstesinden gelmek özellikle zor değil (paylaşılan işaretçilerin kullanımı çok kolay ...), bu yüzden sadece bunu yapabilmek için nesnelerim üzerinde ekstra iş yapmaya değdiğinden emin değilim.


Ayrıca normalde bir işlev / yöntemden döndürülen nesneleri sarmak için akıllı işaretçiler kullanın.
Giorgio

1
@Giorgio: Bu kesinlikle hem şaşırtıcı hem de yavaş.
DeadMG

Yığın üzerinde basit bir nesne döndürürseniz, modern derleyiciler otomatik bir hareket gerçekleştirmelidir, bu nedenle paylaşılan ptr'lere vb. Gerek yoktur
Christian Severin
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.