V dnešním článku se ponoříme do fascinujícího světa Reference (C++). Toto téma je již léta předmětem studia a zájmu a není se čemu divit. Reference (C++) upoutal pozornost vědců, výzkumníků, fandů a podobně. V průběhu historie hrál Reference (C++) klíčovou roli v různých aspektech každodenního života, kultury, technologie a společnosti obecně. V tomto článku prozkoumáme různé aspekty Reference (C++), od jeho původu až po jeho dopad na dnešní svět. Jsme si jisti, že na konci tohoto čtení budete mít širší a bohatší pochopení Reference (C++). Připravte se na cestu objevování a učení!
Reference v programovacím jazyce C++ je jednoduchý referenční datový typ, který poskytuje část funkčnosti typu ukazatel z jazyka C, ale je bezpečnější. Název reference může způsobovat nedorozumění, protože v matematické informatice je reference obecný koncept datového typu, jehož konkrétní implementací je ukazatel i zde popisovaná C++ reference. Definice reference v C++ je taková, že nemusí existovat. Může být implementována jako nové jméno pro existující objekt[pozn. 1] (podobně jako klíčové slovo rename
v jazyce Ada).
Deklarace tvaru:
typ& identifikátor
deklaruje identifikátor typu lvalue reference na zadaný typ.[1]
Příklady:
int a = 5;
int& r_a = a;
extern int& r_b;
r_a
a r_b
jsou typu „lvalue reference na int
“
int& Foo();
Foo
je funkce, která vrací „lvalue referenci na int
“;
void Bar(int& r_p);
Bar
je funkce, jejímž parametrem je „lvalue reference na int
“;
class MyClass { int& m_b; /* ... */ };
MyClass
je třída s členem m_b
, který je lvalue referencí na int
;
int FuncX() { return 42 ; };
int (&f_func)() = FuncX;
int (&&f_func2)() = FuncX; // v zásadě totéž jako předchozí
FuncX
je funkce, která vrací (nereferenční typ) int
a f_func
je alias pro FuncX
const int& ref = 65;
const int& ref
je lvalue reference na const int
ukazující na paměť obsahující hodnotu 65.
int arr;
int (&arr_lvr) = arr;
int (&&arr_rvr) = std::move(arr);
typedef int arr_t;
int (&&arr_prvl) = arr_t{}; // arr_t{} je prvalue typu pole
int *const & ptr_clv = arr; // totéž jako int *const & ptr_clv = &arr;
int *&& ptr_rv = arr;
// int *&arr_lv = arr; // Chyba: inicializace lvalue reference na nekonstantní typ pomocí rvalue
arr_lvr
je reference na pole. Při inicializaci reference na pole se neprovede konverze pole na ukazatel, provede se však při inicializaci reference na ukazatel. Protože konverze pole na ukazatel vrátí prvalue, jeho výsledkem lze inicializovat pouze lvalue reference na const
nebo rvalue reference. Podobně při inicializaci reference na funkce, se neprovede konverze funkce na ukazatel (viz f_func
výše), ale při inicializaci reference na ukazatel na funkci ano:
int FuncX() { return 42 ; };
int (*const &pf_func)() = FuncX; // totéž jako int (*const &pf_func)() = &FuncX;
int (* &&pf_func2)() = FuncX;
Deklarace tvaru:
typ&& identifikátor
deklaruje identifikátor typu rvalue reference na zadaný typ.
Protože jméno rvalue reference je samo o sobě lvalue, je třeba použít std::move
pro předání rvalue reference na přetížení funkce, jehož parametrem je rvalue reference. Rvalue se odkazuje na cv-nekvalifikovaný parametry šablony typ stejné šablony funkce nebo na auto&&
kromě případu, kdy je odvozena ze seznamu inicializátorů uzavřených ve složených závorkách, nazýváme forwardující referencí (dříve též „univerzální reference“[2]) a může fungovat jako lvalue nebo rvalue reference podle toho, jaký dostanou parametr.[3]
V parametrech funkce se někdy používá std::forward
pro předání argumentu funkce jiné funkci, přičemž se zachovává, zda jde o lvalue nebo rvalue.[4]
Typy, které jsou referencemi na nějaký typ, se někdy nazývají referenční typy. Identifikátory, které jsou referenčního typu se nazývají referenční proměnné, přestože to není příliš vhodné pojmenování, jak uvidíme dále.
Reference nejsou objekty a mohou se odkazovat pouze na objekty nebo funkce. Nemůže existovat pole referencí, protože pole se musí skládat z objektů. Stejně tak nemohou existovat ukazatele na referenci a reference na referenci, protože ukazatel musí ukazovat na objekt a reference se musí odkazovat na objekt. To znamená, že int& i
, int&*i
a int& &i
způsobí chybu překladu (zatímco int(& i)
(reference na pole) a int*&i
(reference na ukazatel) chybou nejsou, pokud jsou inicializované). Také reference na void
je chybná, protože void
není typ, ale vyjádření absence typu; reference na void *
může existovat – je to reference na ukazatel nespecifikovaného typu.
Reference nemohou být const
nebo volatile
(např. int& volatile i
selže, prošlo by to pouze v typedef/decltype, ale pak by const/volatile bylo ignorováno). Pokud při vyvozování argumentu šablony je vyvozen typ reference (což se stane při použití forwardujících referencí a předávání lvalue funkci) nebo pokud typedef
, using
nebo decltype
popisují referenci, pak je možné vytvořit referenci na takový typ. V tomto případě se pro určení typu reference používá pravidlo, které se nazývá kolaps reference a funguje takto: Je-li T
typ a TR
typ reference na T
, pak rvalue reference na TR
bude opět typ TR
, zatímco lvalue reference na TR
bude lvalue reference na T
. Jinými slovy lvalue reference mají přednost před rvalue referencemi a rvalue reference na rvalue reference zůstávají nezměněné.
int i;
typedef int& LRI;
using RRI = int&&;
LRI& r1 = i; // r1 je typu int&
const LRI& r2 = i; // r2 je typu int&
const LRI&& r3 = i; // r3 je typu int&
RRI& r4 = i; // r4 je typu int&
RRI&& r5 = 5; // r5 je typu int&&
decltype(r2)& r6 = i; // r6 je typu int&
decltype(r2)&& r7 = i; // r7 je typu int&
Nestatickou členskou funkci lze deklarovat s kvalifikátorem ref
. Tento kvalifikátor se podílí na výběru varianty přetížené funkce a uplatní se na implicitní parametr objektu jako const
nebo volatile
, ale na rozdíl od nich nemění vlastnosti this
. Jeho významem je, že nařizuje, aby byla funkce volána na lvalue nebo rvalue instanci třídy.
#include <iostream>
struct A
{
A() = default;
void Print()const& { std::cout << "lvalue\n"; }
void Print()const&& { std::cout << "rvalue\n"; }
};
int main()
{
A a;
a.Print(); // vypíše "lvalue"
std::move(a).Print(); // vypíše "rvalue"
A().Print(); // vypíše "rvalue"
A&& b = std::move(a);
b.Print(); // vypíše "lvalue"(!)
}
Reference v C++ se v několika ohledech liší od ukazatelů:
Reference, které jsou samostatnými lokálními nebo globálními proměnnými, musejí být inicializovány ve své definici; a reference, které jsou datovými členy instance třídy, musejí být inicializovány v seznamu inicializátorů konstruktoru třídy. Například:
int& k; // způsobí chybu při překladu: error: `k' declared as reference but not initialized
Mezi ukazateli a referencemi existuje jednoduchý převod: operátor &
použitý na referenci vrací ukazatel na stejný objekt, a naopak pokud se reference inicializuje dereferencí (*
) hodnoty ukazatele, bude se odkazovat na stejný objekt jako tento ukazatel, pokud je možné bez vyvolání nedefinovaného chování. Tato ekvivalence odráží typickou implementaci, která efektivně realizuje reference jako ukazatele, které jsou při každém použití implicitně dereferencovány. I když překladače obvykle realizují reference pomocí ukazatelů, C++ norma to nevyžaduje.
To má za následek, že v mnoha implementacích práce s proměnnými s automatickou nebo statickou životností pomocí referencí může způsobovat skryté operace dereference, které, i když se syntakticky podobají přímému přístupu, jsou nákladné.
Protože operace s referencemi jsou tak omezené, je mnohem snazší rozumět referencím než ukazatelům a použití referencí je také odolnější proti chybám. Zatímco ukazatel je možné zneplatnit mnoha mechanismy, od použití hodnoty null
přes použití aritmetiky ukazatelů k opuštění mezí pole po nepovolené změny typů při jejich vytváření z libovolného celého čísla; dříve platná reference se stane neplatnou pouze ve dvou případech:
První případ lze snadno automaticky detekovat, pokud má reference statický rozsah platnosti, ale představuje to problém, pokud reference je členem dynamicky alokovaného objektu; druhý případ se detekuje obtížněji. To jsou jediné problémy s referencemi, které lze řešit rozumnou strategií přidělování paměti.
Nejobvyklejším použitím referencí je pro (formální) parametry funkcí. Reference umožňují číst i měnit hodnoty argumentů (skutečných parametrů) funkcí, aniž by bylo třeba při každém použití parametru použít operátor dereference *
.
void Square(int x, int& out_result) {
out_result = x * x;
}
Vyvolání této funkce s argumenty 3, y
uloží do proměnné y
druhou mocninu čísla 3:
int y;
Square(3, y);
Pokus vyvolat tuto funkci s celočíselným literálem jako druhým argumentem způsobí chybu překladu, protože parametry, které jsou lvalue referencemi bez const
mohou být vázány pouze na adresovatelné hodnoty:
Square(3, 6);
int& Preinc(int& x) {
return ++x; // "return x++;" je špatně
}
Preinc(y) = 5; // totéž jako ++y, y = 5
const
jsou užitečným způsobem předávání velkých objektů mezi funkcemi, který se této režii vyhýbá:void FSlow(BigObject x) { /* ... */ }
void FFast(const BigObject& x) { /* ... */ }
BigObject y;
FSlow(y); // Pomalé, kopíruje y do parametru x.
FFast(y); // Rychlé, poskytuje přímý přístup k y (pouze pro čtení).
Pokud by funkce FFast
skutečně vyžadovala vlastní kopii x, aby ji mohla měnit, musí si kopii vytvořit explicitně. Stejnou techniku lze použít i s ukazateli, u kterých je však nutné ke všem použitím parametru doplnit operátor (&
) k získání adresy, a stejně obtížné by bylo tuto změnu zrušit, pokud by se objekty později zmenšily.
Při srovnání referencí a ukazatelů (v jazyce C++), mají reference polymorfní funkcionalitu:
#include <iostream>
class A {
public:
A() = default;
virtual void Print() { std::cout << "This is class A\n"; }
};
class B : public A {
public:
B() = default;
virtual void Print() { std::cout << "This is class B\n"; }
};
int main() {
A a;
A& ref_to_a = a;
B b;
A& ref_to_b = b;
ref_to_a.Print();
ref_to_b.Print();
}
Tento program vypíše:
This is class A
This is class B
V tomto článku byl použit překlad textu z článku Reference (C++) na anglické Wikipedii.