Geçici bir referansa dayanarak bu aralık için kim sorumlu olacak?


15

Aşağıdaki kod ilk bakışta oldukça zararsız görünüyor. Kullanıcı bu işlevi bar()bazı kütüphane işlevleriyle etkileşim kurmak için kullanır . ( bar()Geçici olmayan bir değere veya benzerine bir referans döndürdüğünden beri bu uzun bir süre çalışmış olabilir .) Ancak şimdi yeni bir örneğini döndürüyor B. Byine a()yinelenebilir türdeki bir nesneye başvuru döndüren bir işleve sahiptir A. Kullanıcı, Bdöndürülen geçici nesne bar()yineleme başlamadan önce yok edildiğinden , bir segfault'a yol açan bu nesneyi sorgulamak ister .

Bunun için kimin (kütüphane veya kullanıcı) suçlanacağına kararsızım. Tüm kütüphane dersleri bana temiz görünüyor ve kesinlikle diğer kodlardan çok farklı bir şey yapmıyor (üyelere referanslar döndürme, yığın örnekleri döndürme, ...). Kullanıcı da yanlış bir şey yapmıyor gibi görünüyor, sadece o nesnelerin ömrü boyunca herhangi bir şey yapmadan bazı nesneler üzerinde yineleniyor.

(İlgili bir soru şöyle olabilir: Kodun döngü başlığında birden fazla zincirleme çağrı tarafından alınan bir şey üzerinde kodun "yineleme-aralık-yinelemeli" olmaması gerektiğine dair genel bir kural oluşturulmalıdır. rvalue?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
Kimi suçlayacağınızı anladığınızda bir sonraki adım ne olacak? Ona bağırıyor musun?
JensG

7
Hayır neden yapayım? Aslında bu "programı" geliştirme sürecinin gelecekte bu problemden kaçınmak için başarısız olduğunu bilmekle daha çok ilgileniyorum.
hllnll

Bunun, değerlerle veya döngüler için aralık temelli olmasıyla hiçbir ilgisi yoktur, ancak kullanıcı nesnenin ömrünü düzgün bir şekilde anlamıyorsa.
James

Site açıklaması: Bu bir Hata Değil olarak kapatılan CWG 900 . Belki dakikalar biraz tartışma içerir.
dyp

8
Bunun için kim suçlanacak? Bjarne Stroustrup ve Dennis Ritchie, her şeyden önce.
Mason Wheeler

Yanıtlar:


14

Bence temel sorun C ++ dil özelliklerinin (veya eksikliğinin) bir kombinasyonudur. Hem kütüphane kodu hem de istemci kodu mantıklıdır (sorunun açık olmaktan çok uzak olduğu gerçeği ile kanıtlandığı gibi). Geçici ömrünün Buzatılması uygunsa (döngünün sonuna kadar) sorun olmazdı.

Geçici yaşamı yeterince uzun yapmak ve artık yapmak çok zor. Hatta "geçici olarak canlıya yönelik bir menzil tabanlı menzilin yaratılmasına dahil olan tüm geçiciler" yan etkiler olmadan oldukça geçici değildir. Nesneden değere göre B::a()bağımsız bir aralık döndürme durumunu düşünün B. Daha sonra geçici Bolarak hemen atılabilir. Kişi, yaşam boyu uzatmanın gerekli olduğu durumları tam olarak belirleyebilse bile, bu durumlar programcılar için açık olmadığından, etki (çok sonradan adlandırılan yıkıcılar) şaşırtıcı ve belki de eşit derecede ince bir hata kaynağı olacaktır.

Bu tür saçmalıkları tespit etmek ve yasaklamak, programcıyı bar()yerel bir değişkene açıkça yükselmeye zorlamak daha arzu edilir olacaktır . Bu C ++ 11'de mümkün değildir ve ek açıklama gerektirdiği için muhtemelen asla mümkün olmayacaktır. Rust, imzasının olduğu yerde bunu yapar .a():

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

Burada 'x, bir kaynağın kullanılabilir olduğu zaman dilimi için sembolik bir ad olan bir ömür boyu değişkeni veya bölge verilmiştir. Açıkçası, yaşamları açıklamak zordur - ya da henüz en iyi açıklamayı bulamadık - bu yüzden kendimi bu örnek için gereken minimum değerle sınırlayacağım ve eğimli okuyucuyu resmi belgelere yönlendireceğim .

Ödünç denetleyici bar().a(), döngü devam ettiği sürece yaşamak için gerekli sonucun farkına varır. Ömür boyu bir kısıtlama olarak ifadelerle 'xşunu yazabiliriz: 'loop <= 'x. Ayrıca yöntem çağrısının alıcısının bar()geçici olduğunu da fark eder . İki işaretçi aynı ömürle ilişkilidir, dolayısıyla 'x <= 'tempbaşka bir kısıtlamadır.

Bu iki kısıt çelişkilidir! İhtiyacımız var 'loop <= 'x <= 'tempama 'temp <= 'loopproblemi tam olarak yakalayan. Çakışan gereksinimler nedeniyle, buggy kodu reddedilir. Bunun bir derleme zamanı denetimi olduğunu ve Rust kodunun genellikle eşdeğer C ++ koduyla aynı makine koduyla sonuçlandığını unutmayın, bu nedenle bunun için bir çalışma zamanı maliyeti ödemeniz gerekmez.

Bununla birlikte, bu bir dile eklemek için büyük bir özelliktir ve yalnızca tüm kodlar kullanıyorsa çalışır. API'lerin tasarımı da etkilenir (C ++ 'da çok tehlikeli olabilecek bazı tasarımlar pratik hale gelir, diğerleri yaşamlarla güzel oynamak için yapılamaz). Ne yazık ki, bu C ++ 'a (veya herhangi bir dile) geriye dönük olarak eklenmenin pratik olmadığı anlamına gelir. Özetle, hata atalet başarılı dillerin ve 1983 yılında Bjarne'nin son 30 yıllık araştırma ve C ++ deneyiminin derslerini dahil etmek için kristal küre ve öngörü bulunmaması gerçeğidir ;-)

Tabii ki, bu gelecekte sorundan kaçınmak için hiç yararlı değil (Rust'a geçip C ++ 'ı bir daha kullanmazsanız). Birden fazla zincirleme yöntem çağrısı ile daha uzun ifadelerden kaçınılabilir (bu oldukça sınırlayıcıdır ve tüm yaşam boyu sorunları uzaktan bile düzeltmez). Ya da bir derleyici yardımı olmadan daha disiplinli mülkiyet politikası benimseyerek deneyebilirsiniz: Belge açıkça barsonucu değeriyle ve döner B::a()şıra uzun yaşamak değil Bhangi a()çağrılır. Daha uzun ömürlü bir referans yerine değere dönecek bir işlevi değiştirirken, bunun bir sözleşme değişikliği olduğunun farkında olun . Halen hataya açıktır, ancak bunun nedenini belirleme işlemini hızlandırabilir.


14

Bu sorunu C ++ özelliklerini kullanarak çözebilir miyiz?

C ++ 11, üye işlevinin çağrılabileceği sınıf örneğinin (ifade) değer kategorisinin kısıtlanmasını sağlayan üye işlevi ref niteleyicileri ekledi. Örneğin:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

beginÜye işlevini çağırırken, büyük olasılıkla endüye işlevini (veya sizearalığın boyutunu elde etmek için benzer bir şeyi) çağırmamız gerekeceğini biliyoruz . Bu, iki kez ele almamız gerektiğinden, bir değer üzerinde çalışmamızı gerektirir. Bu nedenle, bu üye işlevlerin lvalue-ref-nitelikli olması gerektiğini iddia edebilirsiniz.

Ancak, bu altta yatan sorunu çözmeyebilir: örtüşme. beginVe endüye işlev takma nesne veya obje tarafından yönetilen kaynaklar. Tek bir işlevle değiştirirsek beginve değerlerde çağrılabilecek bir endişlev rangesağlamalıyız:

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

Bu geçerli bir kullanım durumu olabilir, ancak yukarıdaki tanım rangebuna izin vermemektedir. Üye işlev çağrısından sonra geçici olana hitap edemediğimizden, bir kapsayıcı, yani sahip olma aralığı döndürmek daha makul olabilir:

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

Bunu OP'nin davasına uygulamak ve hafif kod incelemesi

struct B {
    A m_a;
    A & a() { return m_a; }
};

Bu üye işlevi, ifadenin değer kategorisini değiştirir: B()bir ön değerdir, ancak bir değerdir B().a(). Öte yandan, B().m_abir değerdir. Öyleyse bunu tutarlı hale getirerek başlayalım. Bunu yapmanın iki yolu vardır:

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

İkinci sürüm, yukarıda belirtildiği gibi, OP'deki sorunu çözecektir.

Ayrıca, Büye işlevlerini kısıtlayabiliriz :

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

Bu, OP'nin kodu üzerinde herhangi bir etkiye sahip olmayacaktır, çünkü :döngü için aralık temelli ifadeden sonraki ifadenin bir referans değişkenine bağlanması. Ve bu değişken (kendisine beginve endüye işlevlerine erişmek için kullanılan bir ifade olarak ) bir değerdir.

Kuşkusuz, soru, varsayılan kuralın "değerlere ilişkin üye işlevlerini yumuşatmak, yapmamak için iyi bir neden olmadığı sürece, tüm kaynaklarına sahip olan bir nesneyi döndürmelidir" olup olmadığıdır . Döndürdüğü takma ad yasal olarak kullanılabilir, ancak onu yaşadığınız şekilde tehlikelidir: "üst" geçici öğesinin ömrünü uzatmak için kullanılamaz:

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

C ++ 2a, bence bu (veya benzeri) bir sorunu aşağıdaki gibi çözmek gerekiyor:

for( B b = bar(); auto i : b.a() )

OP yerine

for( auto i : bar().a() )

Geçici çözüm, kullanım ömrünün bfor döngüsünün tüm bloğu olduğunu belirtir .

Bu init-ifadesini sunan teklif

Canlı Demo


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.