Bu, döngüler için C ++ 11'in bilinen bir tuzağı mı?


89

Diyelim ki bazı üye işlevleriyle 3 çiftli tutmak için bir yapımız var:

struct Vector {
  double x, y, z;
  // ...
  Vector &negate() {
    x = -x; y = -y; z = -z;
    return *this;
  }
  Vector &normalize() {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  // ...
};

Bu biraz basitlik için yapılıyor, ancak eminim benzer kodların orada olduğunu kabul ediyorsunuz. Yöntemler, uygun şekilde zincirleme yapmanıza izin verir, örneğin:

Vector v = ...;
v.normalize().negate();

Ya da:

Vector v = Vector{1., 2., 3.}.normalize().negate();

Şimdi begin () ve end () fonksiyonlarını sağlasaydık, Vector'ümüzü yeni bir döngü için kullanabiliriz, örneğin x, y ve z 3 koordinatı üzerinden döngü yapmak için (hiç şüphesiz daha "faydalı" örnekler inşa edemezsiniz) Vector öğesini örneğin String ile değiştirerek):

Vector v = ...;
for (double x : v) { ... }

Hatta yapabiliriz:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }

ve ayrıca:

for (double x : Vector{1., 2., 3.}) { ... }

Ancak, şu (bana öyle geliyor) bozuk:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }

Önceki iki kullanımın mantıksal bir kombinasyonu gibi görünse de, bu son kullanımın sarkan bir referans oluşturduğunu ve önceki ikisi tamamen iyi olduğunu düşünüyorum.

  • Bu doğru mu ve geniş ölçüde takdir ediliyor mu?
  • Yukarıdakilerin hangi kısmı "kötü" kısımdır, bundan kaçınılması gerekir?
  • Dil, aralık tabanlı for döngüsünün tanımını değiştirerek, for-expression içinde oluşturulan geçicilerin döngü süresince var olmasını sağlayarak iyileştirilebilir mi?

Nedense daha önce sorulan çok benzer bir sorunun ne olduğunu hatırlıyorum, yine de adını unuttum.
Pubby

Bunu bir dil kusuru olarak görüyorum. Geçicilerin ömrü, for döngüsünün tüm gövdesine uzatılmaz, yalnızca for döngüsünün kurulumu için uzatılır. Sadece aralık sözdizimi değil, klasik sözdizimi de acı çekiyor. Bence init ifadesindeki geçicilerin ömrü döngünün tüm ömrü boyunca uzatılmalıdır.
edA-qa mort-ora-y

1
@ edA-qamort-ora-y: Burada gizlenen hafif bir dil kusuru olduğu konusunda hemfikirim, ancak bence ömür boyu uzatma, geçici bir referansı doğrudan bir referansa bağladığınızda örtük olarak gerçekleşir, ancak hiçbirinde değil diğer durum - bu, geçici yaşamların altında yatan soruna yarı pişmiş bir çözüm gibi görünüyor, ancak bu, daha iyi bir çözümün ne olacağının açık olduğu anlamına gelmiyor. Belki de geçici oluştururken açık bir 'ömür boyu uzatma' sözdizimi, bu da onu mevcut bloğun sonuna kadar sürmesini sağlar - ne düşünüyorsunuz?
ndkrempel

@ edA-qamort-ora-y: ... bu, geçici olanı bir referansa bağlamakla aynı anlama gelir, ancak okuyucu için 'ömür boyu uzatma'nın satır içi (bir ifadede) meydana geldiği konusunda daha açık olma avantajına sahiptir. (ayrı bir beyanname gerektirmek yerine) ve geçici olanı adlandırmanızı gerektirmez.
ndkrempel

Yanıtlar:


64

Bu doğru mu ve geniş ölçüde takdir ediliyor mu?

Evet, anlayışınız doğru.

Yukarıdakilerin hangi kısmı "kötü" kısımdır, bundan kaçınılması gerekir?

Kötü kısım, bir fonksiyondan geçici olarak döndürülen bir l-değeri referansı almak ve onu bir r-değeri referansına bağlamaktır. Bu kadar kötü:

auto &&t = Vector{1., 2., 3.}.normalize();

Vector{1., 2., 3.}Derleyicinin kendisine normalizebaşvuran dönüş değerinin ne olduğuna dair hiçbir fikri olmadığı için, geçici süre uzatılamaz .

Dil, aralık tabanlı for döngüsünün tanımını değiştirerek, for-expression içinde oluşturulan geçicilerin döngü süresince var olmasını sağlayarak iyileştirilebilir mi?

Bu, C ++ 'nın çalışma şekliyle oldukça tutarsız olacaktır.

İnsanların geçicilere zincirlenmiş ifadeler veya ifadeler için çeşitli tembel değerlendirme yöntemleri kullanmaları tarafından yapılan bazı kavramaları engeller mi? Evet. Ama aynı zamanda özel durum derleyici kodu gerektirecek ve diğerleriyle neden çalışmadığı konusunda kafa karıştırıcı olacaktır. ifade yapılarıyla .

Çok daha makul bir çözüm, derleyiciye bir işlevin dönüş değerinin her zaman bir referans olduğunu bildirmenin bir yolu olabilir thisve bu nedenle, dönüş değeri geçici olarak genişleyen bir yapıya bağlıysa, o zaman doğru geçiciyi genişletecektir. Bu, dil düzeyinde bir çözüm olsa da.

(Derleyici bunu destekliyorsa) böylece Halen, bunu yapabilir normalize edemez geçici çağrılacak:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

Bu Vector{1., 2., 3.}.normalize()bir derleme hatası vermesine neden olurken v.normalize()iyi çalışacaktır. Açıkçası, böyle şeyleri düzeltemeyeceksiniz:

Vector t = Vector{1., 2., 3.}.normalize();

Ama aynı zamanda yanlış şeyler de yapamazsınız.

Alternatif olarak, yorumlarda önerildiği gibi, rvalue referans sürümünün referans yerine bir değer döndürmesini sağlayabilirsiniz:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

Taşınacak Vectorgerçek kaynakları olan bir tür olsaydı , Vector ret = std::move(*this);onun yerine kullanabilirdin . Adlandırılmış dönüş değeri optimizasyonu, bunu performans açısından makul ölçüde optimum hale getirir.


1
Bunu daha "aldatıcı" yapan şey, yeni for döngüsünün sözdizimsel olarak kapakların altında referans bağlamanın devam ettiği gerçeğini gizlemesidir - yani yukarıdaki "aynı derecede kötü" örneklerinizden çok daha az barizdir. Bu nedenle, sadece yeni for döngüsü için ekstra ömür boyu uzatma kuralı önermek makul göründü.
ndkrempel

1
@ndkrempel: Evet, ancak bunu düzeltmek için bir dil özelliği önerecekseniz (ve bu nedenle en azından 2017'ye kadar beklemeniz gerekiyorsa), daha kapsamlı, her yerde geçici uzantı sorununu çözebilecek bir şey olmasını tercih ederim .
Nicol Bolas

3
+1. Son yaklaşımda, deletebir rvalue döndüren alternatif bir işlem sağlayabilmeniz yerine : Vector normalize() && { normalize(); return std::move(*this); }( normalizeFonksiyonun içine yapılan çağrının lvalue aşırı yüklemesine gönderileceğine inanıyorum , ancak birisi bunu kontrol etmeli :)
David Rodríguez - dribeas

3
Bunu &/ &&yöntemlerin niteliğini hiç görmedim . Bu C ++ 11'den mi yoksa bu bazı (belki yaygın) tescilli bir derleyici uzantısı mı? İlgi çekici olasılıklar verir.
Christian Rau

1
@ChristianRau: C ++ 11 için yeni ve statik olmayan üye işlevlerinin C ++ 03 "const" ve "uçucu" niteliklerine benziyor, çünkü bir anlamda "bunu" nitelendiriyor. g ++ 4.7.0 ancak bunu desteklemez.
ndkrempel

25

for (double x: Vector {1., 2., 3.}. normalize ()) {...}

Bu, dilin bir sınırlaması değil, kodunuzla ilgili bir sorundur. İfade Vector{1., 2., 3.}bir geçici oluşturur, ancak normalizeişlev bir lvalue referansı döndürür . Çünkü ifade bir değerdir , derleyici nesne hayatta olacağını varsayar, ama bu geçici bir referans olduğu için bir sarkan referansla bırakılır böylece, tam ifadeden sonra nesne ölür, değerlendirilir.

Şimdi, tasarımınızı mevcut nesneye bir başvuru yerine değere göre yeni bir nesne döndürecek şekilde değiştirirseniz, herhangi bir sorun olmayacak ve kod beklendiği gibi çalışacaktır.


1
constBu durumda bir referans nesnenin ömrünü uzatır mı?
David Stone

5
Bu, normalize()mevcut bir nesnede bir mutasyon işlevi olarak açıkça istenen anlambilimini bozacaktır. Böylece soru. Geçici bir yinelemenin belirli bir amacı için kullanıldığında "uzatılmış bir ömre" sahip olması, aksi halde kafa karıştırıcı bir yanlışlık olduğunu düşünüyorum.
Andy Ross

2
@AndyRoss: Neden? Bir r-değeri referansına (veya ) geçici olarak bağlanan herhangi bir geçici sürenin const&ömrü uzar.
Nicol Bolas

2
@ndkrempel: Yine de, değil bir sınırlama aralığı tabanlı döngü, aynı sorun eğer bir referansa bağlama gelirdi: Vector & r = Vector{1.,2.,3.}.normalize();. Tasarımınız bu sınırlamaya sahiptir ve bu, ya değere göre geri dönmeye istekli olduğunuz anlamına gelir (ki bu, birçok durumda mantıklı olabilir ve daha çok, rvalue referansları ve hareket ile mantıklı olabilir ), ya da problemi yerine getirmeniz gerekir. call: uygun bir değişken oluşturun, ardından onu for döngüsünde kullanın. Ayrıca ifadenin iki nesne Vector v = Vector{1., 2., 3.}.normalize().negate();yarattığına dikkat edin ...
David Rodríguez - dribeas

1
@ DavidRod Rodríguez-dribeas: const referansı bağlamadaki sorun şudur: T const& f(T const&);tamamen iyidir. T const& t = f(T());tamamen iyi. Ve sonra, başka bir OG'de bunu keşfedersiniz T const& f(T const& t) { return t; }ve ağlarsınız ... operator+Değerler üzerinde çalışıyorsa, daha güvenlidir ; daha sonra derleyici kopyayı optimize edebilir (Hız İstiyor mu? Değerlere Göre Geçiş), ancak bu bir bonus. İzin verebileceğim tek geçicilerin bağlanması r-değerleri referanslarına bağlanmaktır, ancak işlevler daha sonra güvenlik için değerler döndürmeli ve Kopya Eleme / Hareket Semantiğine güvenmelidir.
Matthieu M.

4

IMHO, ikinci örnek zaten kusurlu. Değiştirici operatörlerin geri dönmesi *this, bahsettiğiniz şekilde uygundur: değiştiricilerin zincirlenmesine izin verir. Bu olabilir modifikasyon sonucuna basitçe teslim için kullanılan, ancak kolayca göz ardı edilebilir, çünkü bunu hataya eğilimli olması. Gibi bir şey görürsem

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

Fonksiyonların vbir yan etki olarak değiştiğinden otomatik olarak şüphelenmem . Elbette yapabilirlerdi ama kafa karıştırıcı olurdu. Yani böyle bir şey yazacak olsaydım, bunun vsabit kalmasını sağlardım. Örneğiniz için ücretsiz işlevler eklerim

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

ve sonra döngüleri yaz

for( double x : negated(normalized(v)) ) { ... }

ve

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

Bu, IMO'nun daha iyi okunabilirliği ve daha güvenli. Tabii ki, fazladan bir kopya gerektirir, ancak yığın tahsisli veriler için bu muhtemelen ucuz bir C ++ 11 taşıma işleminde yapılabilir.


Teşekkürler. Her zamanki gibi birçok seçenek var. Önerinizin uygun olmayabileceği bir durum, örneğin Vector'un 1000 çiftli bir dizi (yığın tahsis edilmemiş) olmasıdır. Verimlilik, kullanım kolaylığı ve kullanım güvenliği arasında bir denge.
ndkrempel

2
Evet, ancak yine de yığında> 100 boyutunda yapılara sahip olmak nadiren yararlıdır.
soltaroundabout
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.