İşlev yanlışlıkla referans parametresini geçersiz kılar - yanlış giden ne?


54

Bugün sadece zaman zaman belirli platformlarda meydana gelen iğrenç bir hatanın nedenini bulduk. Kaynatılmış, kodumuz şöyle görünüyordu:

class Foo {
  map<string,string> m;

  void A(const string& key) {
    m.erase(key);
    cout << "Erased: " << key; // oops
  }

  void B() {
    while (!m.empty()) {
      auto toDelete = m.begin();
      A(toDelete->first);
    }
  }
}

Sorun bu basitleştirilmiş durumda açık görünebilir: basmaya başlamadan önce harita girişini kaldıran Banahtara anahtar Agönderir. (Bizim durumumuzda, basılmadı, ancak daha karmaşık bir şekilde kullanıldı) Bu elbette tanımsız bir davranış, çünkü keyçağrıdan sonra sarkan bir referans erase.

Bunu düzeltmek önemsizdi - biz sadece parametre türünü olarak const string&değiştirdik string. Soru şu: En başta bu böceği nasıl önleyebilirdik? Her iki fonksiyon da doğru olanı yapmış görünüyor:

  • Akeyimha etmek üzere olduğu şeyi ifade ettiğini bilmenin hiçbir yolu yoktur .
  • Biletmeden önce bir kopyasını almış olabilirdi A, ancak parametreleri değere mi yoksa referansa göre mi almayacağına karar vermek Callee'nin işi değil mi?

Takip edemediğimiz bazı kurallar var mı?

Yanıtlar:


35

Akeyimha etmek üzere olduğu şeyi ifade ettiğini bilmenin hiçbir yolu yoktur .

Bu doğru olsa da A, aşağıdakileri biliyor:

  1. Amacı bir şeyi yok etmektir .

  2. Yok edeceği şeyin aynısı olan bir parametre alır .

Bu gerçekler göz önüne alındığında, bunun mümkün için Abir işaretçi / referans olarak parametre alırsa kendi parametreyi yok etmek. C ++ 'da bu gibi hususların ele alınması gereken tek yer burası değil.

Bu durum, bir operator=görevlendirme operatörünün niteliğinin, kendi kendine görevlendirme konusunda endişelenmeniz gerekebileceği anlamına benzer . Bu bir olasılıktır çünkü thisreferans parametresinin tipi ve tipi aynıdır.

Bunun sadece sorunlu olduğu unutulmamalıdır, çünkü Adaha keysonra girişi kaldırdıktan sonra parametreyi kullanmak niyetindedir . Olmasaydı, o zaman iyi olurdu. Tabii ki, o zaman her şey mükemmel, sonra birisi değiştirir çalışma olması kolaylaşır Akullanımı keypotansiyel tahrip edildikten sonra.

Bir yorum için iyi bir yer olurdu.

Takip edemediğimiz bazı kurallar var mı?

C ++ 'da, bir dizi kurala uymazsanız, kodunuzun% 100 güvenli olacağı varsayımı altında çalışamazsınız. Biz kuralları olamaz şeyi .

Yukarıdaki 2. maddeyi düşünün. Aanahtardan farklı türde bir parametre almış olabilir, ancak nesnenin kendisi haritada bir anahtarın alt nesnesi olabilir. C ++ 14'te, findaralarında geçerli bir karşılaştırma olduğu sürece, anahtar türünden farklı bir tür alabilir. Yani yaparsanız m.erase(m.find(key)), parametre türü anahtar tür olmasa da parametreyi yok edebilirsiniz.

Yani "parametre tipi ve anahtar tipi aynı ise, değere göre al" gibi bir kural sizi kurtarmaz. Bundan daha fazla bilgiye ihtiyacınız olacak.

Sonuç olarak, özel kullanım durumlarınıza ve egzersiz kararınıza dikkat edin, deneyimlerden haberdar olun.


10
Eh, "asla değişmez devleti paylaşma" kuralına sahip olabilirsiniz ya da çift "asla paylaşılmayan devleti paylaşma"
dır

7
@Caleth Bu kuralları kullanmak istiyorsanız, C ++ muhtemelen sizin için bir dil değildir.
kullanıcı253751

3
@Caleth Rust anlatıyor musunuz?
Malcolm

1
"Her şey için kurallarımız olamaz." Evet yapabiliriz. cstheory.stackexchange.com/q/4052
Ouroborus

23

Evet derdim, seni kurtaracak basit bir kural var: Tek sorumluluk ilkesi.

Şu anda, Abir öğeyi bir haritadan kaldırmak için kullandığı ve başka bir işlemden geçirilen bir parametre (yukarıda gösterildiği gibi yazdırılıyor, görünüşe göre gerçek kodda başka bir şey) geçiriliyor. Bu sorumlulukları birleştirmek bana sorunun kaynağını gösteriyor.

Biz bir o fonksiyonu varsa , sadece haritadan değerini siler ve bu başka sadece haritadan bir değerin işlemleri yapar, daha yüksek seviyeli kodundan her demelisiniz, bu yüzden böyle bir şey ile bitirmek istiyorum :

std::string &key = get_value_from_map();
destroy(key);
continue_to_use(key);

Kabul ediyorum, kullandığım isimler hiç kuşkusuz problemi gerçek isimlerden daha açık hale getiriyor, ancak isimler hiç anlamlı değilse, referansı kullanmaya devam etmeye çalıştığımızı açıkça belirtmek için neredeyse kesinler. geçersiz kılındı. Bağlamın basit bir şekilde değiştirilmesi sorunu daha açık hale getirir.


3
Bu geçerli bir gözlem, bu davaya ancak çok dar bir şekilde uygulanmaktadır. SRP'ye saygı duyulduğu çok sayıda örnek var ve hala kendi parametresini geçersiz kılma fonksiyonunun sorunları var.
Ben Voigt,

5
@BenVoigt: Sadece parametresini geçersiz kılmak bir soruna neden olmaz. Geçersiz kılındıktan sonra sorunlara yol açan parametreyi kullanmaya devam ediyor. Ama nihayetinde evet, haklısın: bu durumda onu kurtarmış olsa da, şüphesiz yetersiz olduğu durumlar var.
Jerry Coffin,

3
Basitleştirilmiş bir örnek yazarken, bazı ayrıntıları göz ardı etmeniz gerekir ve bazen bu ayrıntılardan birinin önemli olduğu ortaya çıkar. Bizim durumumuzda Aaslında keyiki farklı harita aradım ve bulursanız girişleri ve bazı ekstra temizlemeleri kaldırdık. Bu yüzden bizim ASRP'yi ihlal ettiğimiz belli değil . Bu noktada soruyu güncellemem gerekip gerekmediğini merak ediyorum.
Nikolai

2
@BenVoigt'in amacını genişletmek için: Nicolai'nin örneğinde, m.erase(key)ilk sorumluluğu ve cout << "Erased: " << keyikinci sorumluluğu vardır, bu nedenle bu cevapta gösterilen kodun yapısı aslında, örnekteki kodun yapısından farklı değildir. Gerçek dünya, sorunun gözden kaçmasıydı. Tek sorumluluk ilkesi, tek eylemlerin çelişkili dizilerinin gerçek dünya kodunda yakınlarda görünmesini sağlayacak ve hatta daha da muhtemel hale getirecek hiçbir şey yapmaz.
sdenham

10

Takip edemediğimiz bazı kurallar var mı?

Evet, işlevi belgelendirmediniz .

Parametre geçiren sözleşmenin bir açıklaması olmadan (özellikle parametrenin geçerliliği ile ilgili kısım - işlev çağrısının başında mı yoksa başından beri), hatanın uygulamada olup olmadığını söylemek mümkün değildir (çağrı sözleşmesi varsa) Çağrı başladığında parametrenin geçerli olması, işlevin parametreyi geçersiz kılabilecek herhangi bir eylemi gerçekleştirmeden önce bir kopyasını alması gerekir) veya arayanda (çağrı sözleşmesi parametrenin çağrı boyunca geçerli kalması gerekiyorsa, çağrı yapamaz. değiştirilen koleksiyonun içindeki verilere bir başvuru iletin).

Örneğin, C ++ standardının kendisi şunları belirtir:

Bir işleve yapılan argüman geçersiz bir değere sahipse (işlev alanının dışındaki bir değer veya amaçlanan kullanımı için geçersiz bir işaretçi gibi), davranış tanımsızdır.

ancak bunun yalnızca aramanın yapıldığı anda mı, yoksa işlevin yürütülmesi sırasında mı uygulanacağını belirleyemez. Ancak, çoğu durumda yalnızca ikincisinin mümkün olduğu açıktır - yani, bir kopya yaparak argüman geçerli olamayacağı zaman.

Bu ayrımın devreye girdiği birkaç gerçek dünya vakası vardır. Örneğin, kendine bir eklemestd::vector<T>


"bunun yalnızca aramanın yapıldığı anda mı, yoksa işlevin yürütülmesi sırasında mı uygulanacağını belirleyemez." Uygulamada, derleyiciler UB çağrıldığında işlev boyunca istedikleri her şeyi yaparlar. Programcı UB'yi yakalamazsa, bu gerçekten garip davranışlara yol açabilir.

Snowman, ilginç olsa da, UB'nin yeniden sıralanması bu cevapta tartıştığım konu ile tamamen ilgisiz, bu da geçerliliği sağlama sorumluluğu (UB'nin asla olmaması).
Ben Voigt

ki bu tam olarak benim açımdan: kodu yazan kişinin sorunlarla dolu bir tavşan deliğinden kaçınmak için UB'den kaçınmasından sorumlu olması gerekir.

@Snowman: Bir projedeki tüm kodu yazan "tek kişi" yok. Arayüz dokümantasyonunun bu kadar önemli olmasının bir nedeni budur. Bir diğeri de, iyi tanımlanmış arayüzlerin bir kerede neden olması gereken kod miktarını azalttığıdır - önemsiz olmayan herhangi bir proje için, birinin her ifadenin doğruluğunu düşünmekten "sorumlu olması" mümkün değildir.
Ben Voigt,

Bir kişinin tüm kodu yazdığını asla söylemedim. Bir noktada, bir programcı bir işleve bakıyor veya kod yazıyor olabilir. Söylemeye çalıştığım tek şey, koda kim bakıyorsa dikkatli olmak gerekiyor, çünkü pratikte UB bulaşıcıdır ve derleyici dahil edildiğinde daha geniş kapsamlarda bir kod satırından yayılır. Bu, bir işlevin sözleşmesini ihlal etme konusundaki amacınıza geri döner: Sizinle aynı fikirdeyim, ancak daha da büyük bir sorun olarak büyüyebileceğini belirtiyorum.

2

Takip edemediğimiz bazı kurallar var mı?

Evet, doğru şekilde test etmediniz. Yalnız değilsin ve öğrenmek için doğru yerdesin :)


C ++ 'un Tanımsız Davranışı, Tanımsız Davranışı ince ve can sıkıcı yollarla gösterir.

Muhtemelen% 100 güvenli C ++ kodu yazamazsınız, ancak bir takım araçlar kullanarak yanlışlıkla Tanımsız Davranışı kod tabanınıza sokma olasılığını azaltabilirsiniz.

  1. Derleyici uyarıları
  2. Statik Analiz (uyarıların genişletilmiş versiyonu)
  3. Aletli Test İkilileri
  4. Sertleştirilmiş Üretim İkilileri

Sizin durumunuzda, (1) ve (2) 'nin çok yardımcı olacağından şüpheliyim, genel olarak bunları kullanmanızı tavsiye ederim. Şimdilik diğer ikisine konsantre olalım.

Hem gcc hem de Clang -fsanitize, derlediğiniz programları çeşitli sorunları kontrol etmek için hangi enstrümanlara yerleştiren bir bayrağa sahiptir. -fsanitize=undefinedörneğin, işaretli tamsayı akış / taşma, çok yüksek miktar, vb. ile kayma yakalar ... Özel durumunuzda -fsanitize=addressve -fsanitize=memoryişlevi çağırmak için bir test yaptırmanız şartıyla, sorunun üstesinden gelebilir. Tamamlanması için, -fsanitize=threadçok iş parçacıklı bir kod tabanınız varsa kullanmaya değer. İkili dosyayı uygulayamıyorsanız (örneğin, kaynakları olmadan 3. taraf kütüphaneleriniz var), valgrindgenel olarak daha yavaş olmasına rağmen kullanabilirsiniz .

Son derleyicilerde ayrıca zenginleştirme olanakları bulunur . Aletli ikili sistemlerle temel fark, sertleştirme kontrollerinin performans üzerinde düşük bir etkiye sahip olacak şekilde tasarlanmaları (<% 1), genel olarak üretim koduna uygun olmalarıdır. En iyi bilinen, yığın çökmesini önleme saldırılarını ve sanal işaretçi hi-jackingini kontrol akışını düşürmenin diğer yollarının arasına yerleştirmek için tasarlanmış CFI kontrolleridir (Kontrol Akışı Bütünlüğü).

Hem (3) hem de (4) ' ün amacı aralıklı bir arızayı belirli bir arızanın içine dönüştürmektir : her ikisi de arızalı hızlı prensibini izler . Bu şu demek:

  • mayına bastığınızda her zaman başarısız olur
  • hemen başarısızlığa uğrar , hatayı rasgele bozmak yerine sizi hatayı işaret eder, vb.

İyi bir test kapsamı ile (3) birleştirmek, üretime başlamadan önce çoğu sorunu yakalamalıdır. Üretimde (4) kullanımı can sıkıcı bir hata ve bir istismar arasındaki fark olabilir.


0

@ not: bu yayın sadece Ben Voigt'in cevabının üstüne daha fazla argüman ekler .

Soru şu: En başta bu böceği nasıl önleyebilirdik? Her iki fonksiyon da doğru olanı yapmış gibi görünüyor:

  • Bir anahtarın, imha etmek üzere olduğu şeyi ifade ettiğini bilmenin bir yolu yoktur.
  • B, A'ya geçmeden önce bir kopya yapmış olabilir, ancak parametrelerin değere mi yoksa referansa göre mi alınacağına karar vermek, çalışanın işi değil mi?

Her iki fonksiyon da doğru olanı yaptı.

Sorun, A çağrısının yan etkilerini dikkate almayan müşteri kodunda .

C ++ 'ın dilde yan etkileri belirtmenin doğrudan bir yolu yoktur.

Bu, yan etkiler gibi şeylerin kodda (dokümantasyon olarak) görünür olduğundan ve kodla korunulduğundan (muhtemelen ön koşulları, post-koşullarını ve değişmezleri belgelemeyi düşünmelisiniz) aynı zamanda görünürlük nedenleriyle de).

Kod değişikliği:

class Foo {
  map<string,string> m;

  /// \sideeffect invalidates iterators
  void A(const string& key) {
    m.erase(key);
    cout << "Erased: " << key; // oops
  }
  ...

Bu noktadan sonra, size bunun için bir birim testi yaptırmanız gerektiğini söyleyen API'nin üzerinde bir şey vardır; Ayrıca, API'nin nasıl kullanılacağını (ve kullanılmayacağını) da söyler.


-4

İlk başta bu böceği nasıl önleyebilirdik?

Hatalardan kaçınmanın tek bir yolu var: kod yazmayı bırakın. Her şey bir şekilde başarısız oldu.

Bununla birlikte, kodun çeşitli düzeylerde test edilmesi (birim testleri, fonksiyonel testler, entegrasyon testleri, kabul testleri, vb.) Yalnızca kod kalitesini iyileştirmez, aynı zamanda hataları azaltır.


1
Bu tamamen saçmalık. Orada değil hataları önlemek için tek bir yolu. Hataların varlığından tamamen kaçınmanın tek yolunun asla kod yazmamak olduğu doğru olsa da, aynı zamanda, hem başlangıçta hem de kod yazarken izleyebileceğiniz çeşitli yazılım mühendisliği prosedürleri olduğu da doğrudur (ve çok daha faydalıdır). Test ederken, bu önemli hataların varlığını azaltabilir. Herkes test aşamasını bilir, ancak en büyük etki, kodu ilk etapta yazarken, sorumlu tasarım uygulamaları ve deyimleri takip ederek en düşük maliyette olabilir.
Cody Gray
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.