İçeriğe geç
academia.sh

Ders 04 / 11

Kopyalanamaz ve Taşınamaz Tipler

= delete ile bir sınıfın kopyalanmasını, taşınmasını ya da ikisini birden yasaklamak; kaynağın standarttan değil sınıfın kendisinden gelen bir sözle boşalması.

Yayın Güncelleme

Bu derste

  • = delete ile kopyalama ve taşımayı ayrı ayrı yasaklayan bir sınıf yazar
  • sınıfın kendi taşıyan yapıcısının kaynağı nasıl boşalttığını açıklar
  • taşımanın sessizce kopyalamaya düşmesi ile silinmiş bir işlevin derleme hatasına düşmesi arasındaki farkı ayırt eder
  • kullanıcı tarafından bildirilen silinmiş bir kopyalayan yapıcının örtük taşıyan yapıcıyı nasıl bastırdığını doğrulayan bir örnek kurar
İçindekiler

Önceki ders, bir kaynağın taşıma sonrası durumunun bazen standardın (std::string), bazen de sınıfın kendisinin (Kayit, SadeKayit) belirlediğini gösterdi. Bu ders son sınıfa, boşa, odaklanıyor: standardın unique_ptr için verdiği nullptr sözü gibi, bir sınıfın kendi taşıyan yapıcısının da kaynağı açıkça ve öngörülebilir biçimde boşaltabileceğini. Ayrıca = delete’in kopyalama ile taşımayı birbirinden bağımsız olarak nasıl kapatabildiğini ele alıyor.

Buraya kadarki üç ders bir sınıfın kopyalanabilir ve taşınabilir olduğunu varsaydı; soru yalnızca taşıma sonrası kaynağın hangi sınıfa düştüğüydü. Ama bazı sınıflar için kopyalama ya da taşımanın kendisi hiç anlamlı değildir — bir dosya tanıtıcısını kopyalamak iki bağımsız kapatma çağrısına, bir kilidin sahipliğini paylaşmak birden fazla ipin aynı kilidi tuttuğunu sanmasına yol açabilir. Bu ders, bir sınıf yazarının kopyalamayı, taşımayı ya da ikisini birden tamamen kapatmak istediğinde kullandığı araç olan = delete’i ve bu kapatmanın üç farklı bileşimini (kopyalanamaz-ama-taşınabilir, kopyalanabilir-ama-taşınamaz, ikisi de değil) ele alıyor.

= delete ile Kısıtlı Sahiplik

// tutamac.cpp — kismen silinmis ozel uye islevlerin turden turedigi haller
#include <cstdio>
#include <type_traits>
#include <utility>
class Tutamac {
public:
    explicit Tutamac(int no) : no_(no) {}
    Tutamac(const Tutamac&) = delete;
    Tutamac& operator=(const Tutamac&) = delete;
    Tutamac(Tutamac&& diger) noexcept : no_(diger.no_) {
        diger.no_ = -1;
    }
    Tutamac& operator=(Tutamac&&) = delete;
    int no() const { return no_; }
private:
    int no_;
};
static void tutamac_ozellikleri() {
    std::printf("Tutamac kopyalanabilir mi: %s\n", std::is_copy_constructible_v<Tutamac> ? "evet" : "hayir");
    std::printf("Tutamac tasima-atanabilir mi: %s\n", std::is_move_assignable_v<Tutamac> ? "evet" : "hayir");
    std::printf("Tutamac tasima-yapilabilir mi: %s\n", std::is_move_constructible_v<Tutamac> ? "evet" : "hayir");
}
static void tutamac_tasima() {
    Tutamac a(7);
    Tutamac b(std::move(a));
    std::printf("b.no() = %d\n", b.no());
    std::printf("a.no() (tasimadan sonra) = %d\n", a.no());
}
class SadeceKopya {
public:
    explicit SadeceKopya(int deger) : deger_(deger) {}
    SadeceKopya(const SadeceKopya&) = default;
    SadeceKopya& operator=(const SadeceKopya&) = default;
    SadeceKopya(SadeceKopya&&) = delete;
    SadeceKopya& operator=(SadeceKopya&&) = delete;
private:
    int deger_;
};
static void sadece_kopya_ozellikleri() {
    std::printf("SadeceKopya kopyalanabilir mi: %s\n", std::is_copy_constructible_v<SadeceKopya> ? "evet" : "hayir");
    std::printf("SadeceKopya tasima-yapilabilir mi: %s\n", std::is_move_constructible_v<SadeceKopya> ? "evet" : "hayir");
}
class Kilit {
public:
    Kilit() = default;
    Kilit(const Kilit&) = delete;
    Kilit& operator=(const Kilit&) = delete;
private:
    int durum_ = 0;
};
static void kilit_ozellikleri() {
    std::printf("Kilit kopyalanabilir mi: %s\n", std::is_copy_constructible_v<Kilit> ? "evet" : "hayir");
    std::printf("Kilit tasima-yapilabilir mi: %s\n", std::is_move_constructible_v<Kilit> ? "evet" : "hayir");
}
int main() {
    tutamac_ozellikleri();
    tutamac_tasima();
    sadece_kopya_ozellikleri();
    kilit_ozellikleri();
}
Tutamac kopyalanabilir mi: hayir
Tutamac tasima-atanabilir mi: hayir
Tutamac tasima-yapilabilir mi: evet
b.no() = 7
a.no() (tasimadan sonra) = -1
SadeceKopya kopyalanabilir mi: evet
SadeceKopya tasima-yapilabilir mi: hayir
Kilit kopyalanabilir mi: hayir
Kilit tasima-yapilabilir mi: hayir

KY7. Tutamac kopyalayan yapıcısını ve kopyalayan atamasını = delete ile açıkça kapatıyor — bu ikisini kullanmaya çalışan kod derlenmez. Taşıyan yapıcı ise elle yazılmış: kaynağın no_ üyesini hedefe taşıyor ve kaynağa -1 yazıyor. Bu, unique_ptr’ın nullptr sözünden farklı bir yerden geliyor: unique_ptr’ı boşaltan standardın kendisidir, Tutamac’ı boşaltan ise sınıfın yazarının seçtiği bir değerdir (-1). İkisi de boş sınıfına girer, ama sözü veren taraf farklıdır — biri standart, öbürü sınıf. Taşıyan atama ise ayrıca = delete ile kapatılmış: Tutamac taşıyan yapıcıyla inşa edilebilir ama taşıyan atamayla var olan bir nesneye aktarılamaz. Bu asimetri kasıtlı olabilir — örneğin Tutamac bir kez kurulduktan sonra yeniden atanmasının anlamsız olduğu bir kaynağı sarıyor olabilir.

Tutamac’ın kopyalanamaz ama taşınabilir olması rastgele bir bileşim değil; standart kütüphanenin unique_ptr’ının izlediği tam olarak aynı desendir. unique_ptr de kopyalayan yapıcısını ve kopyalayan atamasını = delete ile kapatır, taşıyan yapıcısını ve taşıyan atamasını ise elle yazar ve kaynağı (tuttuğu ham işaretçiyi) kaynaktan hedefe devredip kaynakta nullptr bırakır. Tutamac’ın no_ üyesini -1’e çekmesi, unique_ptr’ın işaretçisini nullptr’a çekmesinin küçük ölçekli bir tekrarıdır: ikisi de “bu kaynağın sahibi aynı anda yalnızca bir nesne olabilir, ama sahiplik bir nesneden diğerine devredilebilir” kuralını, kopyalamayı tamamen yasaklayıp taşımayı açıkça izin vererek uyguluyor. Bir sınıf yazarı unique_ptr benzeri bir sahiplik modeli istediğinde kullandığı somut yöntem budur: kopyalayan işlevleri sil, taşıyan işlevleri elle yaz ve kaynağı kaynakta öngörülebilir bir değere çek.

Kopyalanabilir Ama Taşınamaz: Tersten Bir Kısıtlama

SadeceKopya, önceki dersin Kayit’inden farklı bir yoldan taşınamaz hâle geliyor. Kayit taşıma işlevlerini hiç bildirmiyordu; taşıma denendiğinde aşırı yükleme çözümlemesi sessizce kopyalayan yapıcıya düşüyordu, program derleniyor ve çalışıyordu. SadeceKopya ise taşıma işlevlerini bildiriyor ve siliyor. Aradaki fark, bir taşıma denendiğinde ortaya çıkıyor: silinmiş bir işlev aşırı yükleme çözümlemesine tam olarak katılır, adaydır; eğer en iyi eşleşen aday oysa, program derlenmez. SadeceKopya’nın bir nesnesini std::move ile ilklendirmeye çalışan bir satır bu yüzden Kayit’teki gibi sessizce kopyaya düşmez, doğrudan bir derleme hatası verir. Bu ders o satırı ayrı bir kod bloğuna koymuyor — çünkü derlenmeyen bir blok, bu kursun her bloğun gerçekten derlenip koşturulduğu kuralını ihlal eder — ama is_move_constructible_v<SadeceKopya>’nın hayir basması, bu yasağın gerçek olduğunun dolaylı kanıtıdır.

Buradaki ayrım şudur: taşıma işlevi hiç yoksa aşırı yükleme çözümlemesi geriye, var olan başka bir adaya (kopyalayan yapıcıya) düşer — sessiz ve derlenen bir sonuç. Taşıma işlevi var ama silinmişse, aşırı yükleme çözümlemesi onu en iyi eşleşen aday olarak seçer ve orada durur — gürültülü ve derlenmeyen bir sonuç. İkisi de “taşınamaz” olarak tarif edilebilir, ama biri programı sessizce farklı bir işleme yönlendirir, öbürü programı hiç derletmez.

SadeceKopya’nın kopyalanabilir-ama-taşınamaz bileşimi, Tutamac’ın tersidir ve genellikle farklı bir gerekçeyle seçilir: bir sınıf, değeri paylaşılabilir ama sahipliği devredilemez bir kaynağı (örneğin sabit bir yapılandırma değeri, ya da kopyalanması ucuz ama taşınmasının ekstra bir anlamı olmayan küçük bir veri) tutuyorsa, taşıma işlevlerini kapatıp yalnızca kopyalamaya izin vermek bilinçli bir tasarım kararı olabilir. Bu, deger_ üyesinin bir int olduğu SadeceKopya örneğinde açıkça görülüyor: int taşınabilir bir türdür, sınıf taşımayı üye türünden değil kendi tercihinden dolayı kapatıyor. Bir sınıfın taşınabilirliği, tıpkı kopyalanabilirliği gibi, üyelerinin taşınabilirliğinden değil sınıfın kendi bildirdiği özel üye işlevlerden çıkar.

Ne Kopyalanabilir Ne Taşınabilir

Bazı türlerin kopyalanması da taşınması da anlamsızdır — bir kilidin sahipliğini kopyalamak iki bağımsız kilide, taşımak ise kilit tutan bir ipin kimin tuttuğunun karışmasına yol açabilir. Standart kütüphanenin std::mutex’i bu sınıfa örnektir (bu ders eşzamanlılığı öğretmiyor, yalnız adını anıyor): hem kopyalayan hem taşıyan işlevleri silinmiştir, ne kopyalanabilir ne taşınabilir.

Bu üç örnek — Tutamac (kopyalanamaz, taşınabilir), SadeceKopya (kopyalanabilir, taşınamaz), std::mutex ve Kilit (ne kopyalanabilir ne taşınabilir) — aynı soruya üç farklı yanıttır: bu tür için hangi sahiplik devri anlamlıdır? Bu yanıt hiçbir zaman çağıran koddan, derleme bayrağından ya da çalışma zamanı ayarından gelmez; sınıfın kendi tanımına, = default ve = delete satırlarına yazılır. Bir sınıfın kopyalanabilir mi, taşınabilir mi olduğunu öğrenmek için o sınıfı kullanan kodu okumak gerekmez — sınıfın kendi bildirimine bakmak yeterlidir. Bu, sahiplik kısıtlamalarının türde ilan edildiği anlamına gelir: kısıtlama bir belgede, bir yorumda ya da bir kullanım kuralında değil, derleyicinin denetleyebileceği bir bildirimde durur.

Kilit sınıfı burada daha ince bir noktayı gösteriyor: kopyalayan yapıcı elle = delete ile bildirilmiş, ama taşıma işlevlerinden hiçbiri hiç yazılmamış — ne = default ne = delete. Buna rağmen is_move_constructible_v<Kilit> hayir basıyor.

KY8. Bunun nedeni, taşıma işlevlerinin örtük üretilme kuralının kopyalayan yapıcının var olup olmadığına, silinmiş olup olmadığına değil, bakmasıdır: bir sınıf kopyalayan yapıcısını, kopyalayan atamasını ya da yıkıcısını kullanıcı olarak bildirirse — bu bildirim = delete bile olsa — derleyici artık taşıma işlevlerini örtük üretmez. Kilit taşımayı hiç ağzına almadı, ama kopyalayan yapıcıyı silerek dolaylı yoldan taşınamaz hâle geldi. Bu, boş ile belirsiz arasındaki ayrımı da netleştiriyor: her iki sınıfta da kaynaktan bir değer alınıp hedefe götürülür — kaynak taşımadan önceki değerini kaybeder, ikisi de bu bakımdan aynıdır. Ayrım, kaynağın geride bıraktığı değerin nereye vardığının söylenip söylenmediğidir. Boş, kaynağın taşımadan sonra alacağı değerin standart ya da sınıfın kendisi tarafından açıkça vaat edildiği, yani nereye vardığı söylenen durumdur (Tutamac’ın -1’i, kendi taşıyan yapıcısının bir sözü; unique_ptr‘ın nullptr’ı, standardın bir sözü). Belirsiz ise böyle bir vaadin hiç verilmediği, kaynağın nereye vardığının söylenmediği durumdur (std::string’in taşınmış içeriği — geçerlidir ama hangi değeri tuttuğu belirtilmez). Kilit’in durumu ikisinden de değil — taşıma zaten hiç mümkün olmadığı için kaynağın “taşıma sonrası durumu” sorusu hiç sorulmuyor.

Sınırlayıcı Ölçüm

Kilit’in durum_ adlı int üyesi taşınabilir bir türdür, sınıfın kendisi de başka hiçbir açık taşıma yasağı içermez — üye tabanlı akıl yürütmeyle “elbette taşınabilir” denebilirdi. Ama sonuç hayir. Bunun nedeni üyelerin türü değil, Kilit’in kendi bildirdiği tek bir özel üye işlevdir: silinmiş kopyalayan yapıcı. Bu ders boyunca görülen kural burada en sıkıştırılmış hâliyle ortaya çıkıyor: taşınabilirlik sorusu üyelere değil, sınıfın “büyük üçlü”sünden (yıkıcı, kopyalayan yapıcı, kopyalayan atama) herhangi birinin kullanıcı tarafından dokunulup dokunulmadığına bakar. Dokunulmuşsa — silinmiş olsa bile — taşıma örtük üretimden çıkar.

Bu kural, önceki dersteki Kayit örneğiyle aynı köktendir, ama sonucu tam tersidir. Kayit bir yıkıcı yazarak taşımayı örtük üretimden çıkarmıştı; aşırı yükleme çözümlemesi geriye, hâlâ örtük var olan kopyalayan yapıcıya düşüyordu — sonuç, sessizce çalışan bir kopyalama idi. Kilit ise kopyalayan yapıcıyı silerek taşımayı örtük üretimden çıkarıyor; ama bu kez geriye düşülecek bir kopyalayan yapıcı da yok, çünkü o da silinmiş durumda. Sonuç, ne kopyalanabilen ne taşınabilen bir sınıf. Aynı temel kural — kullanıcının bildirdiği bir özel üye işlev, diğerlerinin örtük üretimini bastırır — iki farklı sınıfta iki farklı yöne, birinde sessiz bir kopyalamaya, öbüründe tam bir kapanmaya varıyor. Bu, dördüncü dersin üçüncü dersle kurduğu doğrudan bağdır: aynı mekanizma, hangi işlevin dokunulduğuna göre bambaşka bir sonuç üretebiliyor.

Bu üç bileşim — Tutamac, SadeceKopya, Kilit — bu kursun dört dersi boyunca izlenen sorunun (bir işlem kaynağa ne bırakıyor) bir önceki katmanına, yani “bu işlem kaynak için hiç mümkün mü” sorusuna karşılık geliyor. Önceki üç ders taşımanın her zaman mümkün olduğunu varsayıp sonucunu ölçtü; bu ders taşımanın (ya da kopyalamanın) hiç mümkün olmayabileceğini, ve bu mümkün olmayışın da tıpkı bir taşımanın sonucu gibi sınıfın kendi bildirimine yazıldığını gösterdi. Bir sınıfın API’sini okuyan biri, = default ve = delete satırlarına bakarak o sınıfla hangi devirlerin yapılabileceğini, hangi kod deseninin derlenmeyeceğini, hangi kaynağın kaç nesne arasında paylaşılabileceğini önceden bilir — bunun için sınıfın belgesine ya da kullanım kılavuzuna bakmasına gerek yoktur.

Özet

  • = delete, kopyalama ve taşımayı birbirinden bağımsız olarak kapatabilir; Tutamac taşıma-yapılabilir ama taşıma-atanamaz, kopyalanamaz bir bileşimi gösteriyor.
  • Sınıfın kendi taşıyan yapıcısı bir kaynağı açıkça boşaltabilir (Tutamac’ın -1’i); bu, standardın verdiği bir sözden (unique_ptr‘ın nullptr’ı) farklı bir kaynaktan gelen aynı boş sınıfıdır.
  • Taşıma işlevi hiç bildirilmemişse aşırı yükleme çözümlemesi sessizce kopyalayan yapıcıya düşer (Kayit); taşıma işlevi bildirilip silinmişse aynı çağrı derleme hatası verir (SadeceKopya) — ikisi de “taşınamaz” ama sonuçları farklıdır.
  • Kullanıcının bildirdiği bir kopyalayan yapıcı — silinmiş olsa bile — derleyicinin taşıma işlevlerini örtük üretmesini engeller; Kilit taşımayı hiç anmadan taşınamaz olur.
  • Bazı türler (std::mutex gibi) ne kopyalanabilir ne taşınabilir olacak biçimde tasarlanır; bu, sahipliğin o türde hiçbir yolla devredilmemesi gerektiği anlamına gelir.

Bu üç deseni tanımak, bir üçüncü taraf kütüphanesinin ya da standart kütüphanenin bir türüyle karşılaşıldığında da işe yarar: std::thread kopyalanamaz ama taşınabilir, std::ifstream da öyle; std::atomic ise ne kopyalanabilir ne taşınabilir. Bu davranışların hiçbiri belgeye bakmadan tahmin edilemez, ama sınıfın bildirimindeki = delete ve = default satırları görülebiliyorsa oradan doğrudan okunur.

Sonraki Adım

Bu dört ders, tek bir nesnenin taşınmasının dört olası sonucunu (aynen, değişti, boş, belirsiz) ve bu sonucun nereden — standarttan mı, sınıftan mı, hiç mümkün olmayışından mı — geldiğini ölçtü. Bir sonraki konu bu soruyu tek bir nesneden çoğul bir yapıya taşıyor: bir kap (container) elemanlarını nasıl tutuyor, bir eleman kaptan alınıp başka bir kaba taşındığında kabın kendisi ve elemanları hangi sınıfa düşüyor.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat