Що таке універсальне посилання С




Що таке універсальне посилання С



"Універсальні" посилання в C++11 або T&& не завжди означає "Rvalue Reference"

Нещодавно Скотт Майерс (англ. Scott Meyers) — експерт з мови програмування C++, автор багатьох відомих книг — опублікував статтю, що описує подробиці використання rvalue посилань у C++11.
На Хабре ця тема ще не піднімалася, і, як мені здається, стаття буде цікавою для спільноти.
Оригінал статті: "Universal References in C++11-Scott Meyers"

"Універсальні" посилання в C++11

T&& не завжди означає “Rvalue Reference”

Можливо, найбільш важливим нововведенням C++11 є rvalue посилання. Вони є тим фундаментом, у якому будуються «семантика перенесення (англ. move semantics)» і «perfect forwarding». (Ви можете ознайомитись з основами даних механізмів в огляді Thomas'а Becker'а).

Синтаксично rvalue посилання оголошуються так само, як і «нормальні» посилання (зараз звані lvalue посиланнями), за винятком того, що ви використовуєте два амперсанди замість одного. Таким чином, ця функція приймає параметр типу rvalue-reference-to-Widget:


Враховуючи, що rvalue посилання оголошуються за допомогою "&&", було б розумно припустити, що присутність "&&" в оголошенні типу вказує на rvalue посилання. Але це не так:

Widget&& var1 = someWidget; // тут “&&” означає rvalue посилання auto&& var2 = var1; // тут “&&” НЕ означає rvalue посилання template void f(std::vector&& param); // тут “&&” означає rvalue посилання template void f(T&& param); // тут “&&” НЕ означає rvalue посилання

У цій статті я опишу два значення "&&" в оголошенні типу, роз'ясню як відрізнити їх один від одного і введу нову термінологію, яка дозволить однозначно визначати, яке значення "&&" використовується.Виділення різних значень важливо, тому що якщо Ви думаєте про «rvalue посилання», коли бачите “&&” в оголошенні типу, Ви неправильно зрозумієте велику кількість C++11 коду.

Суть питання в тому, що "&&" в оголошенні типу означає rvalue посилання, але іноді це може означати або rvalue посилання, або lvalue посилання. Отже, деяких випадках “&&” у вихідному коді може мати значення “&”, тобто. синтаксично мати вигляд rvalue посилання (“&&”), а насправді бути lvalue посиланням (“&”).

Посилання є більш гнучким поняттям, ніж lvalue посилання або rvalue посилання. Так rvalue посилання можуть бути пов'язані тільки з rvalue, а lvalue посилання, на додаток до можливості прив'язки до lvalue, можуть бути пов'язані з rvalue за обмежених умов (обмеження на зв'язування lvalue посилань і rvalue полягає в тому, що таке зв'язування допустиме тільки тоді, коли lvalue посилання оголошено як посилання на константу, тобто const T&.) Посилання ж, оголошені з “&&”, які можуть бути або lvalue посиланнями, або rvalue посиланнями, можуть бути пов'язані з чим завгодно. Такі незвично гнучкі посилання заслуговують на свою назву. Я назвав їх «Універсальними» посиланнями.

Подробиці, коли “&&” означає універсальне посилання (тобто коли “&&” у вихідному коді може реально означати “&”) досить складні, тому я відкладу їх опис. А зараз давайте зосередимося на наступному правилі, бо це те, що Ви повинні пам'ятати під час щоденного програмування:

Якщо змінна або параметр оголошено з типом T&& для деякого типу, що виводиться T, така змінна або параметр є універсальним посиланням.

Вимога виведення типу обмежує коло ситуацій, де можуть бути універсальні посилання. Майже всі універсальні посилання - це параметри шаблонів функцій.І оскільки правила виведення типу для змінних, що оголошуються, в основному ті ж, що і для шаблонів, можливі авто-оголошені універсальні посилання. Вони не часто зустрічаються в продакшн коді, але я наведу деякі в цій статті, оскільки вони менш багатослівні приклади шаблонів. У розділі «Дрібні деталі» цієї статті я покажу можливість виникнення універсальних посилань у зв'язку з використанням typedef і decltype, але поки ми не дісталися до «Дрібних деталей», я виходитиму з того, що універсальні посилання стосуються лише шаблонів функцій та авто- оголошеним змінним.

Форма декларації універсального посилання T&& є більш значною вимогою, ніж це може здатися, але я повернуся до розгляду цього питання пізніше. А поки що, просто майте на увазі цю вимогу.

  • Якщо вираз, яким ініціалізується універсальне посилання є lvalue, то універсальне посилання стає lvalue посиланням.
  • Якщо вираз, яким ініціалізується універсальне посилання є rvalue, то універсальне посилання стає rvalue посиланням.
  • Якщо можна взяти адресу виразу, то це вираз lvalue.
  • Якщо тип виразу є lvalue посиланням (тобто T& або const T&, і т.п.), це вираз lvalue.
  • В іншому випадку вираз є rvalue. Концептуально (і, як правило, насправді), rvalue відповідають тимчасовим об'єктам, таким як повертається з функцій або створеним шляхом неявного перетворення типів. Більшість літералів (наприклад, 10 і 5.3) також rvalue.

Ви можете взяти адресу var1, відповідно var1 – це lvalue. Оголошення типу var2 як auto&& робить var2 універсальним посиланням, і оскільки воно ініціалізується var1 (lvalue), var2 стає lvalue посиланням.

Недбале читання вихідного коду може змусити Вас повірити, що var2 є rvalue посиланням; "&&" в оголошенні, звичайно, наводить на цю думку.Але так як var2 - універсальне посилання, ініціалізоване lvalue, воно є lvalue посиланням. Це ніби var2 була оголошена наступним чином:


Як зазначалося вище, якщо вираз має тип lvalue посилання, це lvalue. Розглянемо такий приклад:
std::vector v; . auto&& val = v[0]; // val стає lvalue посиланням (див. нижче)

val є універсальним посиланням та ініціалізована v[0], тобто. результатом виклику std::vector::operator[]. Ця функція повертає посилання на елемент vector (я ігнорую вихід за межі масиву, що призведе до невизначеної поведінки).

Так як всі посилання lvalue є lvalue, і так як це lvalue використовується для ініціалізації val, val стає lvalue посиланням, хоча оголошення типу val виглядає як rvalue посилання.

Я зазначав, що універсальні посилання найпоширеніші у параметрах шаблонів функцій. Розглянемо ще раз шаблон із початку цієї статті.

template void f(T&& param); // “&&” може означати посилання rvalue
f(10); // 10 є rvalue

param ініціалізований літералом 10, який, тому що не можна взяти його адресу, є rvalue. Це означає, що у викликі f універсальне посилання param ініціалізоване rvalue і, таким чином, стає rvalue посиланням - зокрема int&&.

З іншого боку, якщо f викликається як так:

int x = 10; f(x); // x є lvalue

param ініціалізований змінною x, яка, тому що можна взяти її адресу, є lvalue. Це означає, що в даному виклику f універсальне посилання param ініціалізоване lvalue, і param тому стає lvalue посиланням - int&, якщо бути точним.

Коментар поряд з оголошенням f тепер повинен бути зрозумілий: чи буде тип param lvalue посиланням або rvalue посиланням залежить від того, що було передано в f під час виклику. Іноді param стає lvalue посиланням, а іноді rvalue посиланням. Тобто param справді є універсальним посиланням.

Пам'ятайте, що “&&” означає універсальне посилання лише тоді, коли має місце виведення типу. Там, де немає виведення типу, немає й універсального заслання. У таких випадках “&&” в оголошенні типу завжди означає посилання rvalue. Отже:

template void f(T&& param); // Виведений тип параметра ⇒ виведення типу; && ≡ універсальне посилання template class Widget < . Widget(Widget&rhs); // повністю певний тип параметра ⇒ немає виведення типу; . // && ≡ rvalue посилання >; template class Gadget < . templateGadget(T2& rhs); // Виведений тип параметра ⇒ виведення типу; && ≡ універсальне посилання >; void f(Widget&& param); // повністю певний тип параметра ⇒ немає виведення типу; // && ≡ rvalue посилання

Немає нічого дивного у цих прикладах. У будь-якому випадку, якщо Ви бачите T&& (де T — це параметр шаблону), є виведення типу, тому Ви дивитеся на універсальне посилання. А якщо Ви бачите “&&” після певного імені типу (наприклад, Widget&&), Ви дивитеся на rvalue посилання.

Я заявив, що форма декларації посилання має бути «T&&» для того, щоб посилання було універсальним. Це важливий аспект. Подивіться ще раз на декларацію із початку цієї статті:

template void f(std::vector&& param); // “&&” означає rvalue посилання


Тут у нас є і виведення типу та “&&”-описаний параметр функції, але форма декларації параметра не “T&&”, а “std::vector&&”. В результаті параметр є нормальним посиланням rvalue, а не універсальним посиланням. Оголошення універсального посилання може бути лише у формі T&&! Навіть простого додавання const специфікатора достатньо, щоб не інтерпретувати “&&” як універсальне посилання.

template void f(const T&& param); // “&&” означає rvalue посилання


"T&&" є просто необхідною формою для оголошення універсальних посилань. Це не означає, що потрібно використовувати ім'я T для параметрів шаблону.

template void f(MyTemplateParamType&& param); // "&&" означає універсальне посилання


Іноді ви можете побачити T&& у декларації функції шаблону, де T є параметром шаблону, але поки що немає виведення типу. Розглянемо функцію push_back в std::vector (показана тільки цікава для нас версія

std::vector::push_back): template > class vector < public: . void push_back(T&& x); // повністю певний тип параметра ⇒ немає виведення типу; . // && ≡ rvalue посилання >;

Тут T є параметр шаблону, і push_back приймає T&&. Тим не менш, параметр не є універсальним посиланням! Як це може бути?

Відповідь стає очевидною, якщо ми подивимося на те, як push_back буде оголошено поза класом. Я робитиму вигляд, що параметр Allocator відсутній, щоб не захаращувати код. З огляду на це нижче наводиться декларація цієї версії
std::vector::push_back:

template void vector::push_back(T&& x);

push_back не може існувати без класу std::vector, який містить його. Але якщо у нас є клас std::vector, то ми вже знаємо, чим є T, і таким чином, немає необхідності виводити цей тип.

Подивимося приклад. Якщо я напишу,

Widget makeWidget(); // Фабрична функція Widget std::vector vw; . Widget w; vw.push_back(makeWidget()); // створює Widget і додає до vw


то моє використання push_pack скаже компілятор інстанціювати цю функцію для класу std::vector. Її декларація поза класом виглядатиме так:

void std::vector::push_back(Widget&& x);


Розумієте? Як тільки ми знаємо, що клас це std::vector, тип параметра push_back повністю визначений. Виведення типу не провадиться.
Порівняйте це з методом emplace_back std::vector'а, яка оголошена так:

template > class vector < public: . template void emplace_back(Args&&. args); // Виведений тип параметра ⇒ виведення типу; . // && ≡ універсальне посилання >;


Не звертайте увагу, що emplace_back приймає змінну кількість аргументів (як зазначено в декларації Args і args). Тут важливо те, що типи для кожного з аргументів мають бути виведені. Параметр шаблону функції Args не залежить від параметра шаблону класу T, отже, навіть якщо клас повністю відомий, скажімо std::vector, це нічого не говорить про тип (типи) аргументів emplace_back. Оголошення emplace_back поза класом для std::vector це явно показує (я продовжую ігнорувати існування параметра Allocator):

template void std::vector::emplace_back(Args&&. args);

Вочевидь, знання те, що клас — це std::vector< Widget >, не усуває необхідності виведення типу (типів), переданих в emplace_back. В результаті параметри std::vector::emplace_back є універсальними посиланнями на відміну від параметра версії std::vector::push_back, який, як ми побачили, є посиланням rvalue.

Слід мати на увазі те, що вираз lvalue або rvalue не залежить від його типу. Розглянемо тип int. Є lvalue типу int (наприклад, змінні, оголошені int) і є rvalue типу int (наприклад, літерали, наприклад, 10). Це справедливо і для типів користувача, як Widget. Об'єкт Widget може бути lvalue (наприклад, змінна Widget) або rvalue (наприклад, функція-фабрика повернула створений об'єкт Widget). Тип виразу не скаже Вам, чи це lvalue або rvalue.

Widget makeWidget(); // Фабрична функція для Widget Widget&&var1 = makeWidget(); // var1 є lvalue, але тип // var1 – це rvalue посилання (Widget) Widget var2 = static_cast< Widget&& >(var1); // cast вираз дає rvalue, але // його тип – це rvalue посилання (на Widget)


Загальноприйнятим способом перетворити lvalue (наприклад var1) на rvalue є використання std::move, тому var2 може бути визначена наступним чином:

Widget var2 = std::move(var1); // еквівалентно коду вище

Я спочатку навів код з використанням static_cast тільки для того, щоб явно показати, що тип виразу є rvalue посилання (Widget&&).

Іменовані змінні та параметри типу rvalue посилання є lvalue. (Ви можете отримати їхню адресу.) Ще раз розглянемо Widget і Gadget шаблони:

template class Widget < . Widget(Widget&rhs); // Тип rhs - rvalue посилання, . // але rhs є lvalue >; template class Gadget < . template Gadget(T2&rhs);// rhs є універсальним посиланням чий тип . // зрештою стане rvalue посиланням або >; // lvalue посиланням, але rhs є lvalue

У конструкторі Widget rhs є rvalue посиланням, так що ми знаємо, що воно пов'язане з rvalue (тобто було передано rvalue), але саме rhs є lvalue, тому ми повинні перетворити його назад на rvalue, якщо ми хочемо отримати переваги від того що rhs пов'язано з rvalue. Наше бажання, як правило, обумовлене вимогою використовувати rhs як джерело перенесення, тому для перетворення lvalue на rvalue застосовується std::move. Подібним чином rhs в конструкторі Gadget є універсальним посиланням, і отже воно може бути пов'язане з lvalue або rvalue, але в будь-якому випадку саме rhs є lvalue. Якщо воно пов'язане з rvalue і ми хочемо отримати переваги від цього, ми повинні перетворити rhs назад на rvalue. Однак, якщо воно пов'язане з lvalue, ми звичайно не хочемо трактувати його як rvalue. Така залежність від того, з чим пов'язане універсальне посилання, спричиняє використання std::forward: взяти універсальне посилання і перетворити його на rvalue тільки в тому випадку, якщо воно пов'язане з rvalue виразом. Назва функції (“forward”) підтверджує наші очікування, що вона виконає пересилання на іншу функцію, завжди зберігаючи тип посилання аргументу (lvalue чи rvalue).

Але std::move та std::forward не є предметом цієї статті.Стаття розповідає про те, що "&&" в оголошення типу може або не може описувати rvalue посилання. Щоб не відволікатися, я надсилаю Вас до посилань у розділі «Додаткова інформація» для детального опису std::move та std::forward.

Дрібні деталі


Суть питання в тому, що деякі конструкції C++11 породжують посилання на посилання, а посилання на посилання не допускаються в C++. Якщо вихідний код явно містить посилання на посилання, код не вірний:

Widget w1; . Widget& & w2 = w1; // Помилка! Немає такого поняття як "посилання на посилання"

Однак є випадки, де посилання на посилання виникають в результаті маніпуляцій з типами, що відбуваються під час компіляції, і в таких випадках відкинути цей код буде проблематично. Ми знаємо це з досвіду початкового стандарту для C++, тобто, C++98/C++03.

Під час виведення типу параметрів шаблону, який є універсальним посиланням, lvalue і rvalue одного типу виводяться в кілька різних типів. Зокрема, lvalue типу T виводяться як тип T& (тобто lvalue посилання на T), а rvalue типу T виводяться просто як тип T. (Зверніть увагу, що lvalue виводиться як lvalue посилання, rvalue не виводяться як rvalue посилання!) Розглянемо що відбувається при виклику шаблонної функції, що приймає універсальне посилання, з rvalue та lvalue:

template void f(T&& param); . int x; . f(10); // викликається f з rvalue f(x); // викликається f з lvalue
void f(int&& param); // f інстанційна з rvalue


Це добре. Однак у викликі f з lvalue x, T виводиться як int&, і інтстування f містить посилання на посилання:

void f(int&&& param); // початкове інстанціювання f з lvalue

Через посилання на посилання цей код екземпляра виглядає здавалося б невірним, але вихідний код "f(x)" - це цілком розумно.Щоб не відкидати цей код, C++ виконує “згортання посилань”, коли виникає посилання на посилання в контекстах, таких як інстанція шаблону.

  • Rvalue посилання на rvalue посилання стає (“згортається у”) rvalue посиланням.
  • Всі інші посилання на посилання (тобто всі комбінації за участю lvalue посилання) згортаються в lvalue посилання.
void f(int&param); // інстанціювання f з lvalue після згортання посилань

Це дає точний механізм, яким універсальна посилання може (після виведення типу та згортання посилань) бути перетворена на lvalue посилання. Насправді універсальне посилання це просто rvalue посилання в контексті згортання посилань.

Особлива ситуація коли виводиться тип для змінних, які є посиланнями. У такому випадку частина типу, що означає посилання, ігнорується. Наприклад, якщо

int x; . int&& r1 = 10; // тип r1 - int&& int& r2 = x; // тип r2 - int&


тип як для r1, так і для r2 при виклику шаблону f вважається int. Така поведінка відкидання посилань не залежить від правил виведення типу для універсальних типів, lvalue виводяться як тип T&, а rvalue як тип T, і таким чином у цих викликах,

виведений тип як r1, так r2 буде int&. Чому? По-перше, посилальна частина типів r1 і r2 відкидається (виходить int в обох випадках), потім, оскільки це lvalue, обидва розглядаються як int& під час виведення типу для параметра-універсального посилання у викликі f.

Згортання посилань відбувається, як я зазначив, у “контекстах, таких як інстанція шаблону”. Другий такий контекст – це визначення “auto” змінних. Виведення типу для auto змінних, які є універсальними посиланнями, по суті ідентичне виведенню типу для параметрів шаблонів функцій, які є універсальними посиланнями, так lvalue типу T виводиться як тип T&, а rvalue типу T виводиться як має тип T&. Розглянемо ще раз приклад із початку статті:

Widget&& var1 = someWidget; // var1 має тип Widget&& (auto не використовується) auto&& var2 = var1; // var2 має тип Widget& (див. нижче)


Тип var1 - Widget&&, але його "посилальна частина" ігнорується під час виведення типу при ініціалізації var2; він вважається типом Widget. Оскільки це lvalue, яке використовується для ініціалізації універсального посилання (var2), виведений тип буде Widget&. Підставляючи Widget& замість auto у визначенні var2, отримаємо наступний невірний код,

Widget&&& var2 = var1; // Зверніть увагу на посилання на посилання
Widget&var2 = var1; // var2 має тип Widget&


Третій контекст згортання посилань – це формування та використання typedef. Враховуючи цей шаблон класу

template class Widget < typedef T& LvalueRefType; . >;

Якщо ми потім будемо використовувати цей typedef в контексті з використанням посилань на нього, наприклад,
void f(Widget::LvalueRefType&& param);

Останній контекст, де застосовується згортання посилань – це використання decltype. Як і у випадках з шаблонами і auto, decltype виконує виведення типу виразу, який дає типи T або T&, і decltype потім застосовує правила згортання посилань C++11.

На жаль, правила згортання посилань, які застосовуються decltype не ті, що використовуються при виведенні типу для шаблону або auto типу. Подробиці занадто складні для обговорення тут (у розділі «Додаткова інформація» наводяться посилання для деталей), але помітна різниця в тому, що decltype для іменованої змінної не типу посилань виводить тип T (тобто не тип посилання), коли за деяких умов шаблони та auto-типи виводять тип T&. Інша важлива відмінність у тому, що виведення типу decltype залежить тільки від decltype виразу; тип ініціалізує вирази (якщо воно є) ігнорується. Отже:

Widget w1, w2; auto&& v1 = w1; // v1 є універсальним посиланням, // ініціалізоване lvalue, відповідно v1 // є lvalue посиланням на w1. decltype(w1)&& v2 = w2; // v2 є універсальним посиланням, і decltype(w1) це Widget, // таким чином v2 є rvalue посиланням. // w2 це lvalue, і неприпустимо ініціалізувати // rvalue посилання lvalue, таким чином код не буде компілюватися.
Висновок

В описі типу “&&” означає або rvalue посилання, або універсальне посилання – посилання, яке бути або lvalue посиланням або rvalue посиланням. Універсальні посилання мають форму T&& для деякого виведеного типу T.

Згортання посилань – це механізм приведення універсальних посилань (які є просто rvalue посиланнями в ситуаціях, коли застосовується згортання посилань) іноді до lvalue посилань, а іноді до rvalue посилань. Він використовується у спеціальних контекстах, у яких в результаті компіляції можуть з'явитися посилання посилання. Це контексти виведення типу шаблону, виведення auto-типу, формування та використання типовогоефекту та виразу decltype.

Подяки

Чорнові версії цієї статті рецензувалися Cassio Neri, Michal Mocny, Howard Hinnant, Andrei Alexandrescu, Stephan T. Lavavej, Roger Orr, Chris Oldwood, Jonathan Wakely та Anthony Williams. Їхні зауваження сприяли суттєвим покращенням статті, а також її презентації.

Посилання (C++)

У посиланні, як і в покажчику, зберігається адреса об'єкта, розташованого в іншій області пам'яті. На відміну від покажчика, посилання після ініціалізації не можна зробити, щоб посилатися на інший об'єкт або задати значення NULL. Існує два типи посилань: посилання lvalue , які посилаються на іменовану змінну та посилання rvalue , що посилаються на тимчасовий об'єкт. Оператор & позначає посилання lvalue, а оператор && позначає або посилання rvalue, або універсальне посилання (rvalue або lvalue) залежно від контексту.

Посилання можуть оголошуватися за допомогою наступного синтаксису.

[описувач класу сховища] [cv-qualifiers] type-specifiers [ms-modifier] декларатор [ = вираз] ;

Можна використовувати будь-який допустимий декларатор, який задає посилання. Наступний спрощений синтаксис застосовується завжди, крім випадків коли посилання є посиланням на функцію або тип масиву.

Посилання оголошуються за допомогою наступної послідовності.

  1. Специфікатори оголошення:
    • Необов'язковий специфікатор класу схову.
    • Необов'язкові const та/або volatile кваліфікатори.
    • Специфікатор типу: Назва типу.
  2. Декларатор:
    • Необов'язковий модифікатор, який використовується у системах Microsoft. Для отримання додаткових відомостей див. у розділі "Модифікатори для конкретного Майкрософт".
    • Оператор & або && оператор.
    • Необов'язкові const та(або) volatile кваліфіки.
    • Ідентифікатор.
  3. Необов'язковий ініціалізатор.

Більш складні форми декларатора для покажчиків на масиви та функції також застосовуються до посилань на масиви та функції. Додаткові відомості див. у покажчиках.

Кілька деклараторів та ініціалізаторів можуть відображатися в розділеному комі після окремого специфікатора оголошення. Наприклад:

Посилання, покажчики та об'єкти можуть бути оголошені разом:

Посилання містить адресу об'єкта, проте з синтаксичної точки зору поводиться як об'єкт.

У наступній програмі зверніть увагу, що ім'я об'єкта s та посилання на об'єкт, SRef , можуть використовуватися ідентично в наступних програмах.

приклад

// references.cpp #include struct S < short i; >; int main() < S; // Declare the object. S&SRef = s; // Declare and initialize the reference. s.i = 3; printf_s("%d\n", s.i); printf_s("%d\n", SRef.i); SRef.i = 4; printf_s("%d\n", s.i); printf_s("%d\n", SRef.i); >

Заглиблюємося в C++: move семантика та rvalue

У цій статті розібрано основні переваги та нюанси move семантики в C++11 і старше.Все описане у цій статті було перевірено на компіляторі Clang 6.0.1 із бібліотекою libc++ на x86, GNU/Linux.

Введення у move

Move семантика дозволяє перемістити об'єкт замість копіювання для збільшення продуктивності. Найпростіше зрозуміти семантику переміщення з прикладу. Як цей приклад використовуватиметься клас String :

class String < public: explicit String(const char * const c_string) < std::cout c_string = new char[size]; strcpy(this->c_string, c_string); > String(const String& other) < std::cout ~String() noexcept < std::cout private: char *c_string; size_t size; >;

При передачі об'єкта цього класу у функцію, що приймає його за значенням, назвемо її by_value () відбудеться наступне:

auto string = String("Hello, C++11"); by_value(string); // копіювання string by_value() stdout: String(const char *const c_string) // new[] String(const String& other) // new[] ~String() // delete[] ~String() // delete []

Виходить 4 звернення до аллокатора, що є досить накладно. Але якщо об'єкт String більше не знадобиться, а функцію by_value() міняти не можна, можна перемістити об'єкт, а не копіювати. Для цього необхідно написати конструктор переміщення для класу String:

String(String &&other) noexcept

Параметр конструктора переміщення other, по-перше, неконстантний, т.к. конструктор його змінює; по-друге, є rvalue -посиланням ( && ), а не lvalue -посиланням ( & ). Про їхні відмінності буде сказано далі. Сам конструктор переносить Сі-рядок з other на this , роблячи other порожнім.

Конструктор переміщення в загальному випадку не повільніше, а найчастіше навіть швидше за конструктора копіювання, але нічого не заважає програмісту помістити sleep(10'000) в конструктор переміщення.

Для виклику конструктора переміщення замість конструктора можна використовувати std::move() . Тепер приклад виглядає так:

auto string = String("Hello, C++11"); by_value(std::move(string)); // переміщення string в by_value(), string тепер порожня stdout: String(const char *const c_string) // new[] String(String&& other) // завдяки заміні на конструктор переміщення, пропав new[] ~String() // delete[] ~String() // delete[] на nullptr

Кількість звернень до алокатора зменшилася вдвічі!

rvalue та lvalue

Основна відмінність rvalue від lvalue в тому, що об'єкти rvalue можуть бути переміщені, тоді як об'єкти lvalue завжди копіюються.

Це «можуть бути» найкраще демонструють такі два приклади:

class TextView < public: explicit TextView(const String string) : text(std::move(string)) // stdout: String(const String& other) <>private: String text; >;

Цей код працює не так, як від нього очікується. std::move() все ще конвертує lvalue в rvalue , але конвертація зберігає всі модифікатори, у тому числі і const . Потім компілятор вибирає серед двох конструкторів класу String найкращий. Через те, що компілятор не може відправити const rlvaue туди, де очікується non-const rvalue , він вибирає конструктор копіювання, і const rvalue конвертується назад у const lvalue . Висновок: стежте модифікаторами об'єктів, т.к. вони враховуються при виборі однієї з навантажень функції.

Другий приклад демонструє правило: аргументи та результат функції можуть бути як rvalue, так і lvalue, але параметри функцій можуть бути лише lvalue. Аргумент – це те, що передається у функцію. Він ініціалізує параметр, доступний безпосередньо всередині функції.

void f(String&& string) < g(string); // Clang: невідома конверсія від 'String' до 'String &&' for 1st argument >void g(String&& string) <>

Хоча параметр string функції f() і має тип rvalue -посилання, він є lvalue і вимагає явної конвертації rvalue перед передачею в функцію g() . Цим займається std::move() .

Універсальні посилання

Універсальні посилання можуть бути як rvalue-, так і lvalue-посиланням залежно від аргументів або результату функції. Використовуються в шаблонах та в auto&& :

template void template_func(T&& string) < by_value(std::forward(string)); > // template_func() приймає будь-яку Xvalue template_func(string); // String(const String& other) template_func(std::move(string)); String(String&& other)

Компілятор на основі цього шаблону генерує 2 функції, одна з яких приймає lvalue, інша - rvalue, якщо вони використовуватимуться. Якщо програміст хоче використовувати переміщення для rvalue -посилання і просте копіювання для lvalue -посилання, він може використовувати std::forward() , який наводить свій аргумент до rvalue тільки тоді, коли його тип є rvalue -посиланням. std::forward() вимагає явної вказівки шаблонного параметра навіть з автоматичним виведенням шаблонів С++17.

Універсальна посилання має бути шаблонним параметром (звідси таке дивне визначення шаблону функції у прикладі) у форматі T&& . Наприклад, std::vector&& вже не універсальна, а rvalue -посилання.

Згортання посилань

Насправді різниці між rvalue -посиланням та універсальним посиланням немає. Універсальне посилання - лише зручна абстракція над rvalue -посиланням, яким багато хто користується. Але як тоді rvalue -посилання перетворюється на lvalue -посилання при lvalue аргументі? Вся справа в згортанні посилань.

При виклику template_func(string) компілятор генерує наступний заголовок функції:

І виходить посилання на посилання! Вручну так не можна зробити, але шаблони можуть. Далі компілятор згортає посилання посилання. Згортання проводиться за таким правилом: результатом згортання буде rvalue -посилання тільки тоді, коли обидва посилання є rvalue -посиланнями.

Саме через це неподобство простіше використовувати абстракцію універсальних посилань.

Докладніше про move семантику та rvalue можна дізнатися з книги С. Мейєрса «Ефективний та сучасний С++: 42 рекомендації щодо використання С++ 11 та С++14» ISBN: 978-5-8459-2000-3.

Дивовижні можливості сучасного C++, які корисно знати кожному розробнику

Copy/move elision

Copy/move elision – це оптимізація, при якій компілятор може прибрати деякі виклики конструктора копіювання та деструктора, але в даний момент тільки при поверненні об'єкта з функції, і тільки якщо тип об'єкта, що повертається, повністю збігається з типом функції.

Тому при поверненні з функції використання std::move() може знизити продуктивність, обмеживши компілятор Copy elision оптимізації, адже відсутність конструктора швидше, ніж конструктор переміщення.

String non_copy_elision()

Тут std::move() тільки уповільнює код, додаючи зайвий виклик String(String&& other) та ~String() .

C++20 copy/move elision може бути розширений, завдяки чому в деяких випадках використання std::move() також знизить продуктивність. Докладніше про розширення copy/move elision можна дізнатися з відеозапису із зустрічі робочої групи зі стандартизації С++ у московському офісі Яндекса.

Схожі статті

  • Що таке Ютм посилання
  • Що таке адреса посилання
  • Що таке Ципру
  • Що таке режим Atti для дронів Drones Cameras
  • Що таке Адамове яблуко у чоловіків
  • Що таке добори на міжкімнатні
  • Що таке дека на газонокосарці
  • Що таке код помилки 601
  • Недавні статті

  • Як бродить зернова брага
  • Що робити якщо не засмагаєш на сонці чому засмага погано лягає на шкіру або перестає прилипати
  • Як швидко зняти гель лак без апарату
  • Як робиться Каті голови
  • Яка гребінець краще для об'єму
  • Чим роблять м'яку покрівлю
  • Чи можна залишати крем для обличчя на ніч
  • Де знаходиться датчик селектора
  • географія нашої діяльності
    вулиця Драгоманова, 27
    вул. Курчатова 1Б
    вул. Міцкевича 130
    вул. Лабунського, 1
    вул. Макарова-Пржевальського
    вул. Толстого 10
    вул. Грушевського 28
    вул. Перший промінь (Черняхівського)
    напишіть нам

    сообщение успешно отправлено
    x