C ++ 11'de bir yazı ne zaman taşınabilir hale getirilir?


127

Bunun arama sonuçlarımda görünmemesine şaşırdım, C ++ 11'deki hareket anlambiliminin kullanışlılığı göz önüne alındığında, birisinin bunu daha önce soracağını düşündüm:

C ++ 11'de bir sınıfı ne zaman hareketsiz hale getirmeliyim (ya da bu benim için iyi bir fikir mi)?

(Nedenleri diğer ise mevcut kodu ile uyumluluk sorunları daha.)


2
güçlendirme her zaman bir adım öndedir - "türleri taşımak pahalı" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin

1
Bence bu çok iyi ve yararlı bir soru ( +1benden) Herb'ten (ya da göründüğü gibi ikizinden) çok kapsamlı bir cevapla , bu yüzden bunu bir SSS girişi yaptım. Birisi bana salonda ping atarsa , bu konu orada tartışılabilir.
sbi

1
AFAIK taşınabilir sınıfları yine de dilimlemeye tabi olabilir, bu nedenle tüm polimorfik temel sınıflar (yani sanal işlevlere sahip tüm temel sınıflar) için hareket etmeyi (ve kopyalamayı) yasaklamak mantıklıdır.
Philipp

1
@Mehrdad: "T'nin bir hamle yapıcısı var" ve " T x = std::move(anotherT);yasal olmanın" eşdeğer olmadığını söylüyorum. İkincisi, T'nin hareket ctoru olmaması durumunda kopya ctor'una geri dönebilecek bir taşıma isteğidir. Peki, "taşınabilir" tam olarak ne anlama geliyor?
sellibitze

1
@Mehrdad: "MoveConstructible" ın ne anlama geldiğiyle ilgili C ++ standart kitaplık bölümüne bakın. Bazı yineleyicilerin bir taşıma oluşturucusu olmayabilir, ancak yine de MoveConstructible'dır. Aklındaki "taşınabilir" insanların değişen tanımlarına dikkat edin.
sellibitze

Yanıtlar:


110

Herb cevabı (düzenlemeden önceki) aslında bir tür iyi bir örnek verdi olmamalıdır hareket edebilir: std::mutex.

İşletim sisteminin yerel muteks türü (örneğin pthread_mutex_t, POSIX platformlarında) "konum değişmezi" olmayabilir, yani nesnenin adresi değerinin bir parçasıdır. Örneğin, işletim sistemi başlatılmış tüm muteks nesnelerine işaret edenlerin bir listesini tutabilir. Eğer std::mutex(OS onun muteksler işaretçilerin bir listesini tutar çünkü) olduğunda ya sabit kalan gereken bir veri üyesi ve yerli türünün adresi olarak bir doğal OS muteks türü içerdiği std::mutexo kalacaktı böylece yığın üzerinde yerli muteks türü saklamak zorunda kalacak std::mutexnesneler arasında hareket ettirildiğinde aynı konum veya std::mutexhareket etmemelidir. Yığın üzerinde saklamak mümkün değildir, çünkü bir std::mutexkurucuya sahiptir constexprve sürekli başlatma için uygun olmalıdır (yani statik başlatma), böylece globalstd::mutexprogramın yürütülmesi başlamadan önce oluşturulması garanti edilir, bu nedenle kurucusu kullanamaz new. Yani kalan tek seçenek std::mutextaşınmaz olmaktır.

Aynı mantık, sabit bir adres gerektiren bir şey içeren diğer türler için de geçerlidir. Kaynağın adresinin sabit kalması gerekiyorsa, hareket ettirmeyin!

Hareket etmemek için başka bir argüman std::mutexdaha var ki bu, bunu güvenli bir şekilde yapmanın çok zor olacağıdır, çünkü hareket edildiği anda hiç kimsenin muteksi kilitlemeye çalışmadığını bilmeniz gerekir. Muteksler, veri yarışlarını önlemek için kullanabileceğiniz yapı taşlarından biri olduğundan, yarışlara karşı güvenli olmasalar talihsiz olurdu! Bir taşınmaz ile, bir std::mutexkez inşa edildikten ve yok edilmeden önce, herhangi birinin ona yapabileceği tek şeyin onu kilitlemek ve kilidini açmak olduğunu bilirsiniz ve bu işlemlerin iş parçacığı açısından güvenli olduğu ve veri yarışları getirmediği açıkça garanti edilir. Aynı argüman std::atomic<T>nesneler için de geçerlidir : atomik olarak hareket ettirilmedikçe, onları güvenli bir şekilde taşımak mümkün olmazdı, başka bir iş parçacığı aramaya çalışıyor olabilircompare_exchange_stronghareket ettiği anda nesnenin üzerinde. Dolayısıyla, türlerin taşınabilir olmaması gereken başka bir durum, güvenli eşzamanlı kodun düşük seviyeli yapı taşları oldukları ve üzerlerindeki tüm işlemlerin atomikliğini sağlamaları gerektiği durumlardır. Nesne değeri herhangi bir zamanda yeni bir nesneye taşınabilirse, her atomik değişkeni korumak için bir atomik değişken kullanmanız gerekir, böylece onu kullanmanın güvenli olup olmadığını veya taşınmış olduğunu bilirsiniz ... ve korumak için bir atomik değişken bu atomik değişken ve benzeri ...

Sanırım bir nesne, bir değerin bir değerinin veya bir değerin soyutlamasının sahibi olarak hareket eden bir tür değil, yalnızca saf bir bellek parçası olduğunda, onu hareket ettirmenin bir anlam ifade etmediğini söyleyeceğim. intHareket edememe gibi temel türler : onları taşımak sadece bir kopyadır. Bağırsakları sökemezsiniz int, değerini kopyalayabilir ve sonra sıfıra ayarlayabilirsiniz, ancak yine intde bir değeri vardır, sadece bayt bellek. Ama inthala hareketlidil açısından, çünkü bir kopya geçerli bir taşıma işlemidir. Kopyalanamayan türler için, bellek parçasını hareket ettirmek istemiyorsanız veya hareket ettiremiyorsanız ve değerini de kopyalayamıyorsanız, o zaman taşınabilir değildir. Bir muteks veya bir atomik değişken, belleğin belirli bir yeridir (özel özelliklerle işlenir), bu nedenle taşınması mantıklı değildir ve aynı zamanda kopyalanamaz, bu nedenle taşınamaz.


17
+1, özel bir adresi olduğu için taşınamayan bir şeyin daha az egzotik bir örneği, yönlendirilmiş bir grafik yapısındaki bir düğümdür.
Potatoswatter

3
Muteks kopyalanamaz ve hareket ettirilemezse, muteks içeren bir nesneyi nasıl kopyalayabilirim veya taşıyabilirim? (Senkronizasyon için kendi muteksine sahip iş parçacığı güvenli bir sınıf gibi ...)
tr3w

4
@ tr3w, yığın üzerinde muteksi oluşturmadıkça ve onu benzersiz bir_ptr veya benzeri bir yolla tutmadıkça yapamazsınız
Jonathan Wakely

2
@ tr3w: Muteks kısmı hariç tüm sınıfı taşımaz mısınız ?
user541686

3
@BenVoigt, ancak yeni nesnenin kendi mutex'i olacaktır. Sanırım muteks üyesi dışındaki tüm üyeleri hareket ettiren kullanıcı tanımlı taşıma işlemlerine sahip olmak demek. Peki ya eski nesnenin süresi doluyorsa? Mutex'i onunla birlikte sona erer.
Jonathan Wakely

57

Kısa cevap: Bir tür kopyalanabiliyorsa, taşınabilir de olmalıdır. Ancak bunun tersi doğru değildir: gibi bazı türler std::unique_ptrtaşınabilir, ancak bunları kopyalamak mantıklı değildir; bunlar doğal olarak yalnızca hareket eden türlerdir.

Biraz daha uzun cevap aşağıdaki gibidir ...

İki ana tür türü vardır (özellikler gibi daha özel amaçlı olanlar arasında):

  1. intVeya gibi değer benzeri türler vector<widget>. Bunlar değerleri temsil eder ve doğal olarak kopyalanabilir olmalıdır. C ++ 11'de, genellikle taşımayı bir kopyanın optimizasyonu olarak düşünmelisiniz ve bu nedenle tüm kopyalanabilir türler doğal olarak taşınabilir olmalıdır ... taşımak, genellikle yapmadığınız ve genellikle yapmadığınız durumlarda bir kopya yapmanın etkili bir yoludur. Artık orijinal nesneye ihtiyacım yok ve onu yine de yok edecekler.

  2. Temel sınıflar ve sanal veya korumalı üye işlevlerine sahip sınıflar gibi miras hiyerarşilerinde bulunan başvuru benzeri türler. Bunlar normalde işaretçi veya referans ile tutulur, genellikle a base*veya base&, ve bu nedenle dilimlemeyi önlemek için kopya yapısı sağlamaz; tıpkı mevcut bir nesne gibi başka bir nesne almak istiyorsanız, genellikle gibi bir sanal işlevi çağırırsınız clone. Bunların iki nedenden ötürü taşıma oluşturma veya atama ihtiyacı yoktur: Kopyalanamazlar ve zaten daha verimli bir doğal "taşıma" işlemine sahiptirler - yalnızca işaretçiyi nesneye kopyalar / taşırsınız ve nesnenin kendisi olmaz yeni bir hafıza konumuna taşınmak zorunda.

Çoğu tür bu iki kategoriden birine girer, ancak yararlı olan başka türler de vardır, daha nadirdir. Özellikle burada, bir kaynağın benzersiz sahipliğini ifade eden std::unique_ptrtürler, doğal olarak yalnızca taşınır türlerdir, çünkü bunlar değer benzeri değildir (kopyalamak mantıklı değildir), ancak bunları doğrudan kullanırsınız (her zaman değil işaretçi veya referans ile) ve bu türden nesneleri bir yerden başka bir yere taşımak isteyebilirsiniz.


61
Misiniz gerçek Herb Sutter lütfen ayağa kalkar? :)
fredoverflow

6
Evet, bir OAuth Google hesabını kullanmaktan diğerine geçtim ve bana burada veren iki girişi birleştirmenin bir yolunu aramaktan rahatsızlık duymam. (Çok daha zorlayıcı olanlar arasında OAuth aleyhine bir başka argüman.) Muhtemelen diğerini bir daha kullanmayacağım, bu yüzden şimdilik ara sıra SO gönderisi için kullanacağım.
Herb Sutter

7
std::mutexPOSIX muteksleri adresler tarafından kullanıldığı için bunun taşınmaz olduğunu düşündüm .
Puppy

9
@SChepurin: Aslında buna HerbOverflow deniyor.
sbi

26
Bu çok fazla olumlu oy alıyor, kimse fark etmedi mi bir tipin ne zaman sadece hareket etmesi gerektiğini söyledi, soru bu değil mi? :)
Jonathan Wakely

18

Aslında etrafta arama yaptığımda, C ++ 11'deki bazı türlerin hareketli olmadığını gördüm:

  • Tüm mutextürleri ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • her atomictür
  • once_flag

Görünüşe göre Clang'da bir tartışma var: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4


1
... yineleyiciler taşınabilir olmamalı ?! Ne neden?
user541686

evet, bence iterators / iterator adaptorsC ++ 11 move_iterator'a sahip olduğundan kaldırılmalı
billz

Tamam şimdi sadece kafam karıştı. Hedeflerini hareket ettiren yineleyicilerden mi yoksa yineleyicileri kendilerinin hareket ettirmesinden mi bahsediyorsunuz ?
user541686

1
Öyle std::reference_wrapper. Tamam, diğerleri gerçekten hareketsiz görünüyor.
Christian Rau

1
Bu üç kategoriye ayrılır görünmektedir: 1. düşük seviyeli eşzamanlılık ilgili türleri (atomics, muteksler), 2. polimorfik baz sınıfları ( ios_base, type_info, facet), 3. çeşitli garip şeyler ( sentry). Muhtemelen ortalama bir programcının yazacağı hareketsiz sınıflar ikinci kategoridedir.
Philipp

0

Bulduğum başka bir neden - performans. Bir değeri olan bir 'a' sınıfınız olduğunu varsayalım. Bir kullanıcının değeri sınırlı bir süre için (bir kapsam için) değiştirmesine olanak tanıyan bir arabirim çıktı almak istiyorsunuz.

Bunu başarmanın bir yolu, "a" dan bir "kapsam koruma" nesnesi döndürmektir, bu da değeri yıkıcıya geri döndürür, örneğin:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Eğer change_value_guard'ı taşınabilir yaparsam, yıkıcısına korumanın taşındığını kontrol edecek bir 'eğer' eklemem gerekir - bu fazladan bir if ve bir performans etkisidir.

Evet, elbette, muhtemelen herhangi bir aklı başında optimize edici tarafından optimize edilebilir, ancak yine de dilin (bunun için C ++ 17 gerektirir, ancak taşınabilir olmayan bir türü geri döndürebilmek için garantili kopya elisyonu gerektirir) bizi gerektirmez eğer korumayı yaratma işlevinden geri döndürmek dışında herhangi bir şekilde hareket ettirmeyeceksek bunu ödemek için (kullanmadığın şey için ödeme yapma ilkesi).

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.