Değer kategorileri ve bunlara başvurular

Bu konu C++'ta bulunan çeşitli değer kategorilerini (ve değerlere başvurularını) tanıtır ve açıklar:

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • Rvalue

Lvalue'ları ve rvalue'ları duymuşsunuzdur. Ancak bunları bu konunun sunduğu terimlerle düşünmeyebilirsiniz.

C++ içindeki her ifade, yukarıda listelenen beş kategoriden birine ait bir değer verir. C++ dilinin özellikleri ve kuralları, bu değer kategorilerinin ve bunlara yönelik başvuruların düzgün bir şekilde anlaşılmasını talep eder. Bu özellikler arasında bir değerin adresini almak, bir değeri kopyalamak, bir değeri taşımak ve başka bir işleve iletmek yer alır. Bu konu, bu yönlerin tümüne ayrıntılı olarak girmez, ancak bunların sağlam bir şekilde anlaşılması için temel bilgiler sağlar.

Bu konudaki bilgiler, Stroustrup'un değer kategorilerini kimlik ve taşınabilirliğin iki bağımsız özelliğiyle çözümlemesi açısından çerçevelendirilir [Stroustrup, 2013].

Bir lvalue'nun kimliği vardır

Bir değerin kimliğe sahip olması ne anlama gelir? Bir değerin bellek adresine sahipseniz (veya alabilirseniz) ve bunu güvenli bir şekilde kullanıyorsanız, değerin kimliği vardır. Bu şekilde, değerlerin içeriğini karşılaştırmaktan daha fazlasını yapabilirsiniz; bunları kimliğe göre karşılaştırabilir veya ayırt edebilirsiniz.

Bir lvalue'nun kimliği vardır. Artık "lvalue" içindeki "l" harfinin "left" sözcüğünün kısaltması olması (yani bir atamanın sol tarafında yer alması anlamında) yalnızca tarihsel açıdan ilgi çekicidir. C++'ta, atamanın solunda veya sağında bir lvalue görüntülenebilir. "lvalue" içindeki "l", aslında ne olduklarını anlamanıza veya tanımlamanıza yardımcı olmaz. Yalnızca lvalue olarak adlandırdığımız değerin kimliğe sahip bir değer olduğunu anlamanız gerekir.

lvalues ifadelerine örnek olarak şunlar verilebilir: adlandırılmış değişken veya sabit; veya başvuru döndüren bir işlev. lvalue olmayan ifadelere örnek olarak şunlar verilebilir: geçici bir değer; veya değere göre döndüren bir işlev.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

Şimdi, lvalue'ların kimliği olduğunu söylemek doğru olsa da, bu xvalue'lar için de geçerlidir. Bu konunun ilerleyen bölümlerinde tam olarak bir xvalue değerinin ne olduğunu göreceğiz. Şimdilik glvalue adlı bir değer kategorisi ("genelleştirilmiş lvalue " için) olduğunu unutmayın. glvalues kümesi hem lvalues ( klasik lvalues olarak da bilinir) hem de xvalues üst kümesidir. Bu nedenle, "bir lvalue'nun kimliği vardır" doğru olsa da, bu çizimde gösterildiği gibi, kimliği olan öğelerin tam kümesi glvalues kümesidir.

Bir lvalue'nun kimliği vardır

Bir rvalue taşınabilir; bir lvalue taşınabilir değildir.

Ancak glvalue olmayan değerler vardır. Başka bir deyişle, bellek adresini alamayacağınız değerler vardır (veya bu adresin geçerli olacağına güvenemezsiniz). Yukarıdaki kod örneğinde bu tür değerler gördük.

Güvenilir bir bellek adresi olmaması bir dezavantaj gibi görünüyor. Ancak bunun gibi bir değerin avantajı, onu kopyalamak yerine (genellikle ucuzdur) taşıyabilmenizdir (genellikle pahalıdır). Bir değerin taşınması, değerin artık eskiden olduğu yerde olmadığı anlamına gelir. Bu nedenle, eskiden olduğu yerde erişmeye çalışmak kaçınılması gereken bir şeydir. Bir değerin ne zaman ve nasıl taşınacaklarına ilişkin bir tartışma, bu konu başlığının kapsamı dışındadır. Bu konu için, taşınabilir bir değerin rvalue (veya klasik rvalue) olarak bilindiğini bilmemiz yeterlidir.

"rvalue" içindeki "r", "sağ" kısaltmasıdır (atamanın sağ tarafında olduğu gibi). Ancak, atama işlemlerinin dışında rvalue'ları ve rvalue referanslarını kullanabilirsiniz. O hâlde, "rvalue" içindeki "r", odaklanılması gereken şey değildir. Yalnızca rvalue olarak adlandırdığımız değerin taşınabilir bir değer olduğunu anlamanız gerekir.

Buna karşılık, bu çizimde gösterildiği gibi bir lvalue taşınamaz. Eğer bir lvalue taşınacaksa, bu lvalue'nun tanımıyla çelişir. Ayrıca bu, lvalue'ya erişmeye devam edebilmeyi gayet makul biçimde bekleyen kod için beklenmedik bir sorun olurdu.

Bir rvalue taşınabilir; bir lvalue taşınabilir değildir

Yani bir lvalue'yi taşıyamazsınız. Ancak, ne yaptığınızı biliyorsanız (taşımadan sonra erişmemeye dikkat etmek dahil) taşıyabileceğiniz bir tür glvalue (kimlikli öğe kümesi) vardır ve bu da xvalue değeridir. Bu konuda, değer kategorilerinin tam resmine baktığımızda bu fikri bir kez daha tekrar ele alacağız.

Rvalue başvuruları ve başvuru bağlama kuralları

Bu bölüm, rvalue referansının söz dizimini tanıtır. Taşıma ve iletme konularını ayrıntılı biçimde ele almak için başka bir konuyu beklememiz gerekecek, ancak şimdilik rvalue referanslarının bu sorunların çözümünün gerekli bir parçası olduğunu söylemek yeterli olacaktır. Ancak rvalue referanslarına bakmadan önce, daha önce yalnızca "referans" dediğimiz şey hakkında T& daha net olmamız gerekir. Bu, gerçekten de referansı kullanan kişinin yazabildiği bir değeri ifade eden "bir lvalue (const olmayan) referansı"dır.

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

Bir lvalue referansı, bir lvalue'ya bağlanabilir, ancak bir rvalue'ya bağlanamaz.

Bir de, kullanıcının yazamadığı nesnelere (örneğin, bir sabite) başvuran lvalue const referanslar (T const&) vardır; referansı kullanan kullanıcı bu nesnelere yazamaz.

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

Bir lvalue const başvurusu bir lvalue'ya veya rvalue'ya bağlanabilir.

T türündeki bir rvalue başvurusunun söz dizimi T&& şeklinde yazılır. Rvalue başvurusu, taşınabilen bir değeri ifade eder; bu değeri kullandıktan sonra içeriğini korumamız gerekmeyen bir değerdir (örneğin, geçici bir değer). Asıl amaç, bir rvalue referansına bağlı değeri ondan taşıyarak (böylece onu değiştirerek) kullanmak olduğundan, const ve volatile niteleyicileri (cv-niteleyicileri olarak da bilinir) rvalue referanslarına uygulanmaz.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

Bir rvalue referansı, bir rvalue'ya bağlanır. Aslında, aşırı yükleme çözümlemesi açısından bir rvalue, lvalue const başvurusu yerine rvalue başvurusuna bağlanmayı tercih eder . Ancak rvalue başvurusu bir lvalue'ya bağlanamaz çünkü dediğimiz gibi rvalue başvurusu, içeriğini korumamız gerekmediği varsayılan bir değere (taşıma oluşturucusunun parametresi gibi) başvuruyor.

Ayrıca, kopya oluşturma yoluyla (veya rvalue bir xvalue ise taşıma yapısı aracılığıyla) bir değere göre bağımsız değişkenin beklendiği bir rvalue geçirebilirsiniz.

Bir glvalue'nun kimliği vardır; bir prvalue'nun ise yoktur.

Bu aşamada neyin kimliğe sahip olduğunu biliyoruz. Taşınabilen ve olmayan şeyleri de biliyoruz. Ancak henüz kimliği olmayan değerler kümesini adlandıramadık . Bu küme prvalue veya saf rvalue olarak bilinir.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

glvalue'ın kimliği vardır; prvalue'ın kimliği yoktur

Değer kategorilerinin tam resmi

Geriye yalnızca yukarıdaki bilgileri ve çizimleri tek bir büyük bütün hâlinde birleştirmek kalıyor.

Değer kategorilerinin tam resmi

glvalue (i)

Bir glvalue’nin (genelleştirilmiş lvalue) bir kimliği vardır. "Kimlik var" kısaltması olarak "i" kullanacağız.

lvalue (i&!m)

Bir lvalue (bir tür glvalue) özdeşliğe sahiptir, ancak taşınabilir değildir. Bunlar genellikle referansla, const referansla ya da kopyalamanın maliyeti düşükse değer olarak aktardığınız okunup yazılabilen değerlerdir. Bir lvalue, bir rvalue referansına bağlanamaz.

xvalue (i&m)

Bir xvalue (bir tür glvalue, ancak aynı zamanda bir tür rvalue) kimliğine sahiptir ve aynı zamanda taşınabilir. Bu, kopyalamak maliyetli olduğu için taşımaya karar verdiğiniz eski bir lvalue olabilir; sonrasında ona erişmemeye de dikkat edeceksiniz. Bir lvalue değerini xvalue'ya dönüştürme burada açıklanabilir.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

Yukarıdaki kod örneğinde henüz hiçbir şey taşımadık. Bir lvalue değerini adsız rvalue başvurusuna dönüştürerek yalnızca bir xvalue oluşturduk. Yine de lvalue adıyla ayırt edilebilir; ancak artık bir xvalue olarak taşınabilme yeteneğine sahiptir. Onu taşımanın nedenleri ve bunun pratikte ne anlama geldiği başka bir başlığın konusu olmak zorunda kalacak. Ama yardımcı olacaksa, "xvalue" içindeki "x"i "yalnızca uzmanlara yönelik" anlamında düşünebilirsiniz. Bir lvalue’yi bir xvalue’ye dönüştürerek (unutmayın, bu bir rvalue türüdür), değer artık bir rvalue referansına bağlanabilir.

Burada iki xvalue örneği daha verilmiştir: adlandırılmamış rvalue başvurusu döndüren bir işlevi çağırma ve bir xvalue üyesine erişme.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

Prvalue (saf rvalue; bir rvalue türü) bir kimliğe sahip değildir, ancak taşınabilir. Bunlar genellikle geçicidir veya değere göre döndüren bir işlevi çağırmanın sonucu ya da glvalue olmayan diğer ifadeleri değerlendirmenin sonucu olur.

rvalue (m)

Rvalue taşınabilir. "m" değerini "taşınabilir" kısaltması olarak kullanacağız.

Bir rvalue referansı her zaman bir rvalue’ya (içeriğini korumamız gerekmediği varsayılan bir değer) işaret eder.

Ancak, bir rvalue referansının kendisi bir rvalue mudur? İsimsiz bir rvalue referansı (yukarıdaki xvalue kod örneklerinde gösterilenler gibi) bir xvalue olduğundan, evet, bu bir rvalue'dur. Örneğin bir taşıma oluşturucusunun parametresi gibi, bir rvalue referans fonksiyon parametresine bağlanmayı tercih eder. Buna karşılık (ve belki de sezgilere aykırı olarak), bir rvalue referansının bir adı varsa, o addan oluşan ifade bir lvalue olur. Bu nedenle bir rvalue başvuru parametresine bağlanamaz . Ancak bunu yaptırmak kolaydır—yalnızca onu yeniden isimsiz bir rvalue referansına (xvalue) dönüştürün.

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

Kimliği olmayan ve taşınamayan değer türü, henüz ele almadığımız tek birleşimdir. Ancak bu kategori C++ dilinde yararlı bir fikir olmadığından bunu göz ardı edebiliriz.

Referans daraltma kuralları

bir ifadedeki birden çok benzer başvuru (lvalue başvurusuna lvalue başvurusu veya rvalue başvurusuna rvalue başvurusu) birbirini iptal eder.

  • A& &, A& içinde daraltılır.
  • A&& &&, A&& olarak daralır.

Bir ifadedeki aynı türden olmayan birden çok referans, bir lvalue referansına indirgenir.

  • A& &&, A& içine daraltılır.
  • A&& &, A& içine daraltılır.

Referansları iletme

Bu son bölüm, daha önce ele aldığımız rvalue referanslarını, farklı bir kavram olan iletme başvurusu ile karşılaştırır. "İletme referansı" terimi ortaya atılmadan önce, bazıları "evrensel referans" terimini kullanıyordu.

void foo(A&& a) { ... }
  • A&&, gördüğümüz gibi, bir rvalue referansıdır. Const ve volatile, rvalue referanslarına uygulanmaz.
  • foo yalnızca A türünde rvalue'ları kabul eder.
  • Rvalue başvurularının (örneğin A&&) var olmasının nedeni, geçici bir değerin (veya başka bir rvalue’nun) geçirildiği durum için optimize edilmiş bir aşırı yükleme yazabilmenizdir.
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&&, bir iletme referansıdır. bar öğesine ne geçirdiğinize bağlı olarak, _Ty türü volatile/volatile olmayandan bağımsız olarak const/non-const olabilir.
  • bar _Ty türünde herhangi bir lvalue veya rvalue değerini kabul eder.
  • Bir lvalue iletmek, yönlendirme başvurusunun _Ty& && haline gelmesine neden olur; bu da lvalue başvurusuna _Ty& indirgenir.
  • Bir rvalue geçirilmesi, yönlendirme başvurusunun rvalue başvurusu _Ty&& hâline gelmesine neden olur.
  • Yönlendirme referanslarının (örneğin _Ty&&) var olma nedeni, optimizasyon değil, onlara aktardığınız şeyi şeffaf ve verimli bir biçimde olduğu gibi aktarmaktır. Bir yönlendirme başvurusuyla büyük olasılıkla yalnızca kitaplık kodu yazarsanız (veya yakından incelerseniz) karşılaşırsınız; örneğin, oluşturucu argümanlarını ileten bir fabrika işlevi yazarken.

Sources

  • [Stroustrup, 2013] B. Stroustrup: The C++ Programming Language, Fourth Edition. Addison-Wesley. 2013.