Домівка > Uncategorized > Rvalue посилання: C++0x можливості в VC10

Rvalue посилання: C++0x можливості в VC10

л- та п-значення в C++98/03

Для розуміння правосторонніх посилань (rvalue reference) в C++0x, ви маєте розуміти лівосторонні й правосторонні значення в C++98/03.

Терміни ‘л-значення’ та ‘п-значення’ викликають плутанину, це пов’язано з їхньою історією. Ці поняття прийшли з С і тоді стали частиною C++. Для збереження часу, ми проминемо їхню історію, включно з причинами чому вони називаються ‘л-значення’ та ‘п-значення’ й одразу розглянемо як вони працюють в C++98/03. (Добре, це не велика таємниця, що ‘Л’ означає ‘лівий’, а ‘R’ – ‘правий’. Але самі поняття розвинулись і змінились, і тепер їхні назви не дуже точні. Замість прослуховування уроку з історії їх появи, ви можете уявити, що вони мають довільні назви, приміром, ‘верхня частка’ та ‘нижня частка’, і при цьому ви нічого не втратите.)

C++03 3.10/1 каже: ‘Кожен вираз є або л-значенням або п-значенням.’ Важливо пам’ятати, що л- або п-значеннєвість це властивість виразу, а не об’єкта.

Л-значення живуть довше одного виразу. Наприклад, obj, *ptr, ptr[index] і ++x це л-значення.

Натомість п-значення тимчасові й зникають по обчисленні всього виразу в якому вони живуть (тобто в місці крапки з комою). Наприклад, 1729, x + y, std::string(‘meow’) і x++ це п-значення.

Зазначимо різницю між ++x і x++. Як маємо x = 0;, тоді вираз x це л-значення, ім’я постійного об’єкта. Вираз ++x також л-значення. Тут відбувається зміна постійного об’єкта. Однак, вираз x++ це п-значення. Тут копіюється початкове значення постійного об’єкта, змінюється об’єкт, а повертається копія. Ця копія тимчасова. Обидві прирощення ++x і x++ збільшують x, але ++x повертає сам об’єкт, тоді як x++ повертає тимчасову копію. Ось чому ++x це л-значення, а x++ це п-значення. Значеннєвість не турбується з того, що вираз робить, лиш зважає на те, що він іменує (щось постійне чи щось тимчасове).

якщо ви хочете розвинути інтуїцію щодо цього, можете скористатись іншим способом для визначення чи є л-значенням, для цього треба спитати чи можу я отримати його адресу?. Якщо так, то це л-значення. Як ні, тоді п-значення. Наприклад, &obj, &*ptr, &ptr[index] і &++x всі вірні (хоча деякі з них недолугі), а &1729, &(x + y), &std::string('meow') і &x++ невірні. Чому це працює? Оператор взяття адреси вимагає л-значеннєвості операнда (C++03 5.3.1/2). Чому він цього вимагає? Отримання адреси постійного об’єкта це добре, але отримання адреси тимчасового об’єкта буде дуже небезпечним, бо він незабаром зникне.

Попередній приклад не зважає на перевантаження операторів, яке є зручним методом для виклику функцій. Виклик функції є л-значенням тоді і лише тоді, коли тип результату – посилання. (C++03 5.2.2/10) Затим, vector v(10, 1729);, v[0] це л-значення, бо operator повертає int& (і &v[0] вірно й корисно), а от рядок s(‘foo’); і рядки t(‘bar’);, s + t це п-значення, бо operator+() повертає string (і &(s + t) неправильно).

Як л-значення так і п-значення можуть бути змінюваними (non-const) і незмінюваними (const). Ось приклади:

string one('cute');
const string two('fluffy');
string three() { return 'kittens'; }
const string four() { return 'are an essential part of a healthy diet'; }

one;     // змінюване л-значення
two;     // стале л-значення
three(); // змінюване п-значення
four();  // стале п-значення

Type& прив’язаний до змінюваних л-значень може використовуватись для спостереження за ними і зміни їх. Він не може бути прив’язаним до сталих л-значень, бо це порушить сталість. Він не може бути прив’язаним до змінюваних п-значень, це було б дуже небезпечно. Випадкова змінений тимчасовий об’єкт, втратиться разом із зникненням тимчасового об’єкта, що призвело б до витончених та неприємних помилок, тож С++ прямо забороняє це. (Варто зауважити, що VC має лихе розширення, яке дозволяє це, але при компіляції з /W4, видається попередження, якщо це розширення задіяне. Зазвичай.) І також він не може бути прив’язаним до сталих п-значень, це було б вдвічі гірше. (Уважний читач має звернути увагу, що я тут нічого не кажу про виведення аргументів.)

const Type& прив’язується до будь-чого: змінюваних л-значень, сталих л-значень, змінюваних п-значень, сталих п-значень (і може бути використаним для спостереження за ними).

Посилання це ім’я, тобто посилання прив’язане до п-значення саме є л-значенням (так, Л). (З того, що тільки стале посилання може бути прив’язаним до п-значення, це буде стале л-значення.) Це спантеличує, і буде мати велике значення пізніше, тож я поясню це в подробицях. Дана функція void observe(const string& str), всередині observe(), str це стале л-значення, а його адреса може бути отрмана й використана до повернення з observe(). Це правда, хоча observe() можна викликати з п-значеннями, такими як three() або four() наведеними вище. Також можна викликати observe('purr'), тут буде створений тимчасовий об’єкт, що містить тимчасовий рядок, а str буде прив’язано до нього. Повернуті з three() і four() значення, також праві, але всередині observe(), str це ім’я, тож це л-значення. Як я сказав до цього, ‘л-значеннєвість як і п-значеннєвість це властивість виразів, але не об’єктів’. Звісно, через те, що str може бути прив’язана до тимчасового об’єкта, який незабаром зникне, його адреса має зберігатись лише в самій функції і не може використовуватись після повернення з неї.

Чи зв’язували ви колись п-значення зі сталим посиланням, а потім брали його адресу? Так, ви таке робили! Це саме те, що відбувається за присвоєння копіюванням, Foo& operator=(const Foo& other), з перевіркою на спробу надання свого значення, if (this != &other) { copy stuff; } return *this;, і тут відбувається присвоєння копіюванням з тимчасового об’єкта: Foo make_foo(); Foo f; f = make_foo();.

Зараз ви можете спитати, Тож в чому відмінність між змінюваними та сталими п-значеннями? Я не можу прив’язати Type& до змінюваних п-значень, я не можу присвоїти їм значення, отже чи справді я можу змінювати їх? Це дуже добре питання! В C++98/03, несталі функції члени можуть бути викликані на змінюваних п-значеннях. C++ не хоче, щоб ви могли випадково змінити тимчасовий об’єкт, але прямий виклик несталої функції члени над змінюваним п-значенням дозволено. В C++0x, відповідь дуже змінюється через використання семантики переносу.

Поздоровляю! Тепер ви маєте те, що я називає ‘бачення л- і п-значень’, вміння поглянути на вираз і визначити де л-, а де п-значення. Поєднане з вашим ‘баченням сталості’, ви можете обґрунтувати, що void mutate(string& ref) використане так mutate(one) вірно, а так mutate(two), mutate(three()), mutate(four()), і mutate('purr') невірно, всі observe(one), observe(two), observe(three()), observe(four()), і observe('purr') вірні. Якщо ви розробник на C++98/03, то вже нутром відчували, що вірне, а що ні, інакше вам це казав компілятор. Чи це корисно? Для мовних адвокатів можливо, для нормальних програмістів не дуже. Порівняно з C++98/03, C++0x має набагато більш потужне бачення л- і п-значень (особливо, можливість поглянути на вираз, визначити змінюване це чи стале л-/п-значення, і зробити з ним щось). Щоб ефективно використовувати C++0x, ви також повинні мати це бачення. І тепер воно в вас є, тож ми можемо продовжувати!

проблема копіювання

C++98/03 поєднує шалено потужне абстрагування з надзвичайно ефективним виконанням, але його проблемою є те, що він занадто любить копіювання. Об’єкти зі значеннєвою семантикою поводяться як int, тож копіювання об’єкта не змінює джерело, і отримані копії незалежні. Семантика значень це чудово, за винятком того, що вона тяжіє до необов’язкового копіювання важких об’єктів як то рядки, вектори і т.д. (Під важкими розуміємо дорогих для копіювання; вектор з мільйоном елементів важкий.) Поліпшення повернення значення (Return Value Optimization, RVO) і Поліпшення повернення іменованого значення (Named Return Value Optimization, NRVO), де вдається уникнути використання конструктора копіювання в певних ситуаціях, полегшує цю проблему, але не скасовує всі необов’язкові копії.

Найнеобов’язковіші копіювання це ті, де джерело має бути знищено. Чи будете ви робити фотокопію аркуша паперу й одразу потому знищувати первинний аркуш, прискаємо, що оригінал і копія тотожні? Це буде марнотратством; ви мали б залишити первинний аркуш і не займатись копіюванням. Наведемо один приклад, який я називаю вбивчим прикладом, отриманий він з одного прикладів Комітету Стандартизації (в N1377). Уявімо, що ви маєте набір рядків, як оцей:

string s0('my mother told me that');
string s1('cute');
string s2('fluffy');
string s3('kittens');
string s4('are an essential part of a healthy diet');

І тоді ви з’єднуєте їх так:

string dest = s0 + ' ' + s1 + ' ' + s2 + ' ' + s3 + ' ' + s4;

Наскільки це дієво? (Нас не бентежить цей приклад, що виконується за мікросекунди; нас турбує узагальнення, яке зустрічається в мові повсюдно.)

Кожен виклик operator+() повертає тимчасовий рядок. Тут 8 викликів, отже 8 тимчасових рядків. Кожен з них, під час конструювання, здійснює динамічне виділення пам’яті і копіює всі всі символи, що були з’єднані, а потім, під час руйнування, виконує динамічне вивільнення пам’яті. (Якщо ви чули про оптимізацію маленьких рядків (Small String Optimization), яку здійснює VC для уникнення виділення та вивільнення пам’яті для коротких рядків, вона тут не застосовується через зумисне достатньо довгий рядок s0, і навіть якщо вона застосовується, копіювання відбувається все одно. Якщо ви чули про оптимізацію Копіювання-По-Запису (Copy-On-Write), забудьте про неї – вона не застосовується тут, і це деоптимізація при багатопотоковості, тож Стандартна бібліотека більше не здійснює її.)

Через те, що кожна конкатенація копіює всі сиволи, що були об’єднані до нього, тут присутня квадратична складність за числом конкатенацій. Отакої! Це дуже неощадливо, і має особливе значення для C++. Чому так відбувається, і що ми можемо з цим подіяти?

Проблема полягає в невмінні operator+(), що отримує два const string& або const string& і const char * (присутні й інші перевантаження, які ми не використовуємо тут), визначити отримав він л- чи п-значення, тож він завжди створює тимчасовий рядок. Чому це має значення?

При обчисленні s0 + ' ', безперечно необхідно створити новий тимчасовий рядок. s0 це л-значення, що іменує постійний об’єкт, отже ми не можемо змінювати його. (Дехто може зауважити!) Але дехто може зауважити, що коли обчислюємо (s0 + ' ') + s1, ми можеме просто добавити вміст s1 в наш тимчасовий рядок, замість створення другого тимчасового і відкидання геть першого. Це й є ключовий погляд вглиб семантики переносу: бо s0 + ' ' це п-значення, тобто виражає тимчасовий об’єкт, ніхто інший в цій програмі не може дізнатись його значення. Якби ми могли визначити, що вираз є змінюваним п-значенням, ми могли б змінювати цей об’єкт як завгодно, і ніхто б цього не помітив. operator+() не передбачає зміни своїх параметрів, але якщо це змінювані п-значення, то всім байдуже. Таким чином, кожний виклик operator+() може додавати символи до одного тимчасового рядка. Це повністю виключає потребу в динамічному керуванні пам’яттю та непотрібному копіюванні, залишивши нас з лінійною складністю. Далебі так!

Технічною мовою, в C++0x, кожен виклик operator+() все ще повертає окремий тимчасовий рядок. Однак, другий тимчасовий рядок (з обчислення (s0 + ' ') + s1) будується через крадіжку пам’яті, що належить першому тимчасовому рядку (з обчислення s0 + ' ') і потому додає вміст s1 в цю пам’ять (що може викликати звичайний геометричний перерозподіл). Викрадання складається з обміну вказівниками: другий тимчасовий об’єкт копіює і тоді обнуляє внутрішній вказівник першого тимчасового об’єкта. Коли перший тимчасовий рядок зрештою знищується (‘на крапці з комою’), його вказівник нульовий, отже деструктор не робить нічого.

Загалом, вміння визначати змінювані п-значення дозволяє задіяти перетягування ресурсів. Якщо на об’єкти посилаються через змінювані п-значення відповідають за бідь-які ресурси (як то пам’ять), ви можете вкрасти їхні ресурси замість копіювання, бо вони зникнуть все одно. Побудова з або присвоєння із змінюваних п-значень, говорячи про ресурси, якими вони володіють зазвичай згадується як переніс, і переносимі об’єкти мають семантику переносу.

Це дуже корисно в багатьох випадках, таких як перерозподіл вектора. Коли вектор збільшує місткість (наприклад, під час push_back()) і відбувається перерозподіл, тут потрібне скопіювати елементи зі старого блоку пам’яті до нового. Виклик коснтрукторів копіювання може бути дорогим. (У випадку з vector, потрібно скопіювати кожний рядок, що потребує динаміного виділення пам’яті.) Але стоп! Елементи в старому блоці буде знищено. Отже можемо перенести елементи замість копіювати їх. В такому разі, елементи в старому блоці знаходяться в області постійного зберігання і вирази використовні для посилання на них, такі як old_ptr[index], є л-значеннями. Під час перерозподілу, ми хочемо посилатись на елементи в старому блоці пам’яті за допомогою виразів, які є змінюваними п-значеннями. Вдавання, що вони є змінюваними п-значеннями допоможе нам перенести їх, уникнувши викликів конструкторів копіювання. (Вислів Я хочу вдати, що це л-значення є змінюваним п-значенням тотожний до Я знаю, що це л-значення, що посилається на постійний об’єкт, але мені байдуже, що станеться з цим л-значенням після цього. Я збираюсь знищити його, надати йому значення абощо. Тож, якщо ви можете вкрасти ресурси з нього, вперед.)

Посилання п-значення в C++0x уможливлюють семантику переносу, надаючи нам можливість виявити змінювані посилання п-значення й обікрасти їх. Також вони надають нам можливість задіяти семантику переносу за нашим бажанням трактуючи л-значення як змінювані р-значення. Тепер, давайте поглянемо як посилання п-значення працюють!

посилання п-значення: ініціалізація

C++0x вводить новий тип посилань, посилання п-значення, з таким синтаксисом Type&& і const Type&&. Поточний папір з C++0x, N3242 8.3.2/2, каже: Тип посилань оголошений за допомогою & називається посилання л-значення, а тип посилань оголошений через && називається посиланням п-значення. Посилання л- і п-значень це різні типи. Окрім місць де явно зазначено, вони семантично тотожні та загалом згадані як посилання. Це значить, що ваша інтуїція стосовно посилань в C++98/03 (тепер відомих як посилання п-значень) перекладається на посилання п-значення; все, що ви маєте зробити – вивчити відмінності.

В чому ж відмінність? Порівняно з посиланнями л-значеннями, посилання п-значення поводяться інакше під час ініціалізації і розв’язання перевантажень. Вони відмінні тим до чого вони згодні прив’язатись (тобто ініціалізуватись) і тим, що воліло б прив’язатись до них (тобто як розв’язуються перевантаження). Спершу поглянемо на ініціалізацію:

  • Ми вже бачили як змінювані посилання л-значення, Type&, можуть прив’язатись до змінюваних л-значень, але ні до чого іншого(сталих л-значень, змінюваних п-значень, сталих п-значень).
  • Також ми бачили, що сталі посилання л-значення, const Type&, готові прив’язатись до будь-чого.
  • Змінювані посилання п-значення, Type&&, здатні прив’язатись до змінюваних л-значень і змінюваних п-значень, але не сталих л- або п-значень (це буде порушувати константну відповідність).
  • Сталі п-значення, const Type&&, можуть прив’язуватись до будь-чого.

Ці правила можуть виглядати таємниче, але вони отримані з двох простих правил:

  • Покори відповідності константності через попередження прив’язування змінюваних посилань до сталих об’єктів.
  • Уникнення випадкової зміни тимчасових об’єктів через заборону прив’язування змінюваних посилань л-значень до змінюваних п-значень.

Якщо вам більше подобається читати повідомлення про помилки замість читання українською, маєте показ:

using namespace std;

string modifiable_rvalue() {
    return 'cute';
}

const string const_rvalue() {
    return 'fluffy';
}

int main() {
    string modifiable_lvalue('kittens');
    const string const_lvalue('hungry hungry zombies');

    string& a = modifiable_lvalue;          // Line 16
    string& b = const_lvalue;               // Line 17 - ERROR
    string& c = modifiable_rvalue();        // Line 18 - ERROR
    string& d = const_rvalue();             // Line 19 - ERROR

    const string& e = modifiable_lvalue;    // Line 21
    const string& f = const_lvalue;         // Line 22
    const string& g = modifiable_rvalue();  // Line 23
    const string& h = const_rvalue();       // Line 24

    string&& i = modifiable_lvalue;         // Line 26
    string&& j = const_lvalue;              // Line 27 - ERROR
    string&& k = modifiable_rvalue();       // Line 28
    string&& l = const_rvalue();            // Line 29 - ERROR

    const string&& m = modifiable_lvalue;   // Line 31
    const string&& n = const_lvalue;        // Line 32
    const string&& o = modifiable_rvalue(); // Line 33
    const string&& p = const_rvalue();      // Line 34
}

initialization.cpp
initialization.cpp(17) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &'
    Conversion loses qualifiers
initialization.cpp(18) : warning C4239: nonstandard extension used : 'initializing' : conversion from 'std::string' to 'std::string &'
    A non-const reference may only be bound to an lvalue
initialization.cpp(19) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &'
    Conversion loses qualifiers
initialization.cpp(27) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&'
    Conversion loses qualifiers
initialization.cpp(29) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&'
    Conversion loses qualifiers

Для змінюваних посилань п-значень допустимо прив’язатись до змінюваних п-значень; справа в тому, що вони можуть використовуватись для зміни тимчасових об’єктів.

Хоча посилання л-значення та посилання п-значення поводяться схоже під час ініціалізації (тільки рядки 18 і 28 різняться), вони більше розходяться при розв’язання перевантажень.

посилання п-значення: розв’язання перевантажень

Ви вже знайомі з тим як можуть бути перевантажені функції за допомогою змінюваних і сталих посилань л-значень параметрів. В C++0x, функції також можуть бути перевантажені із змінюваними і сталими посиланнями п-значеннями. Ви не маєте дивуватись, що кожне з наведених перевантажень одномісної функції віддає перевагу прив’язуванню до відповідного посилання:

#include <iostream>
#include <ostream>
#include <string>

using namespace std;

void meow(string& s) {
    cout << 'meow(string&): ' << s << endl;
}

void meow(const string& s) {
    cout << 'meow(const string&): ' << s << endl;
}

void meow(string&& s) {
    cout << 'meow(string&&): ' << s << endl;
}

void meow(const string&& s) {
    cout << 'meow(const string&&): ' << s << endl;
}

string strange() {
    return 'strange()';
}

const string charm() {
    return 'charm()';
}

int main() {
    string up('up');
    const string down('down');

    meow(up);
    meow(down);
    meow(strange());
    meow(charm());
}

meow(string&): up
meow(const string&): down
meow(string&&): strange()
meow(const string&&): charm()

На практиці, перевантаження на Type&, const Type&, Type&& і const Type&& не дуже корисне. Цікавіше буде const Type& і Type&& :

#include <iostream>
#include <ostream>
#include <string>

using namespace std;

void purr(const string& s) {
    cout << 'purr(const string&): ' << s << endl;
}

void purr(string&& s) {
    cout << 'purr(string&&): ' << s << endl;
}

string strange() {
    return 'strange()';
}

const string charm() {
    return 'charm()';
}

int main() {
    string up('up');
    const string down('down');

    purr(up);
    purr(down);
    purr(strange());
    purr(charm());
}

purr(const string&): up
purr(const string&): down
purr(string&&): strange()
purr(const string&): charm()

Як це працює? Ось правила:

  • Правило ініціалізації має силу вето.
  • Л-значення сильно схильні до прив’язування до посилань л-значень, а п-значення до посилань п-значень.
  • Змінювані вирази слабко схильні до прив’язування до змінюваних посилань.

(Під вето, мається на увазі, що потенційна функція, яка потребує забороненої прив’язки виразу до посилання, негайно зараховується до нежиттєздатних і видаляється з подальшого розгляду.) Давайте проглянемо процес застосування правил.

  • Для purr(up), правила ініціалізації не забороняють ні purr(const string&) ні purr(string&&). up це л-значення, тож воно сильно схильне до прив’язування до посилнання л-значення purr(const string&). up змінюване, отже воно слабко схильне до прив’язування до змінюваного посилання purr(string&&). Сильна схильність до purr(const string&) перемагає.
  • Для purr(down), правила ініціалізаціїх забороняють purr(string&&) через невідповідність сталості, тож purr(const string&) виграє одразу.
  • Для purr(strange()), правило ініціалізації не забороняє ані purr(const string&) ані purr(string&&). strange() це п-значення, отже воно сильно схильне до purr(string&&). strange() змінюване, отже воно слабко схильне до прив’язування до purr(string&&). Двічі обране purr(string&&) виграє.
  • Для purr(charm()), правила ініціалізації забороняють purr(string&&) через невідповідність сталості, отже purr(const string&) виграє одразу.

Важливо запам’ятати, що у випадку перевантаження із const Type& і Type&&, змінювані п-значення прив’язуються до Type&&, все інше до const Type&. Затим, це й є набір перевантажень для семантики переносу.

Важливе зауваження: функції, що вертають результат за значенням, мають повертати Type (подібно до strange()), а не const Type (подібно до charm()). Другий варіант майже нічого не дає (заборону на виклик несталих функцій членів) і перешкоджає оптимізації через семантику переносу.

семантика переносу: шаблон

Ось простий клас, remote_integer, що зберігає вказівник на динамічно розташований int. (Це 'віддалена власність (remote ownership)'.) Його конструктор за невказанням іншого, одномісний конструктор, коструктор копіювання, оператор присвоєння копіюванням і деструктор мають бути дуже знайомими вам. Додатково наявні конструктор переносу і оператор присвоєння переносом. Вони оточені #ifdef MOVABLE отже ми зможемо побачити, що відбудеться з ними і без них; справжній код не буде робити цього.

#include <stddef.h>
#include <iostream>
#include <ostream>

using namespace std;

class remote_integer {
public:
    remote_integer() {
        cout << 'Default constructor.' << endl;
        m_p = NULL;
    }

    explicit remote_integer(const int n) {
        cout << 'Unary constructor.' << endl;
        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << 'Copy constructor.' << endl;

        if (other.m_p)
            m_p = new int(*other.m_p);
        else
            m_p = NULL;
    }

#ifdef MOVABLE
    remote_integer(remote_integer&& other) {
        cout << 'MOVE CONSTRUCTOR.' << endl;

        m_p = other.m_p;
        other.m_p = NULL;
    }
#endif // #ifdef MOVABLE

    remote_integer& operator=(const remote_integer& other) {
        cout << 'Copy assignment operator.' << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p)
                m_p = new int(*other.m_p);
            else
                m_p = NULL;
        }
        return *this;
    }

#ifdef MOVABLE
    remote_integer& operator=(remote_integer&& other) {
        cout << 'MOVE ASSIGNMENT OPERATOR.' << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }
        return *this;
    }
#endif // #ifdef MOVABLE

    ~remote_integer() {
        cout << 'Destructor.' << endl;
        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

remote_integer square(const remote_integer& r) {
    const int i = r.get();
    return remote_integer(i * i);
}

int main() {
    remote_integer a(8);

    cout << a.get() << endl;

    remote_integer b(10);

    cout << b.get() << endl;

    b = square(a);

    cout << b.get() << endl;
}
#define MOVEABLE
Unary constructor.
8
Unary constructor.
10
Unary constructor.
Copy assignment operator.
Destructor.
64
Destructor.
Destructor.
Unary constructor.
8
Unary constructor.
10
Unary constructor.
MOVE ASSIGNMENT OPERATOR.
Destructor.
64
Destructor.
Destructor.

Зазначимо декілька моментів.

  • Конструктори копіювання та переносу перевантажені, оператор присвоєння копіюванням і переносом теж перевантажені. Ми вже бачили, що відбувається з функцією перевантаженою на const Type& і Type&&. Саме це уможливлює код b = square(a); автоматично вибирати оператор присвоєння переносом, коли такий присутній.
  • Замість динамічного виділення пам'яті, конструктор переносу і оператор присвоєння копіюванням просто цуплять її в other. При поцупленні, ми копіюємо вказівник other і обнуляємо його. Коли other знищується, його деструктор нічого не робить.
  • Оператори присвоєння копіюванням і переносом потребують перевірку на самоприсвоєння. Добре відомо чому оператор присвоєння копіюванням потребує перевірки на самоприсвоєння. Через те, що типи такі як int можуть бути присвоєні самі собі без якоїсь шкоди (тобто x = x;), отже й типи даних визначені користувачем мають поводитись так само. Самоприсвоєння ніколи не відбудеться в коді написаному вручну, але легко може статись в середині алгоритмів на кшталт std::sort(). В C++0x, алгоритми подібні до std::sort() можуть переносити аргументи замість копіювання. Тут присутній такий самий ризик самоприсвоєння.

Тут ви можете здивуватись як це взаємодіє з автоматично створеними ('неявно оголошеними') конструкторами й операторами присвоєння.

<

ul>

  • Конструктори переносу й оператори присвоєння переносом ніколи не оголошуються неявно.
  • Неявно оголошений конструктор по замовчанню забороняється будь-яким конструктором визначеним користувачем, як конструктором копіювання так і конструктором переносу.
  • Неявно оголошений конструктор копіювання переважується користувацьким конструктором копіювання, але не користувацьким конструктором переносу.
  • Неявне оголошення оператора присвоєння копіювання переважується користувацьким оператором присвоєнням копіюванням, але не користувацьким оператором присвоєння копіюванням.

    По суті, автоматично створені правила не перетинаються з семантикою переносу, за винятком конструктора переносу, як і в випадку будь-якого іншого користувацького конструктора, визначення якого забороняє конструктор по замовчанню.

    семантика переносу: перенесення з л-значень

    Тепер, що якщо ви полюбляєте писати свої конструктори копіювання через оператори присвоєння копіювання? Ви можете спробувати написати свій конструктор переносу в термінах свого оператору присвоєння переносу. Це можливо, але ви маєте бути обережним. Ось хибний шлях для цього:

    #include <stddef.h>
    #include <iostream>
    #include <ostream>
    
    using namespace std;
    
    class remote_integer {
    
    public:
        remote_integer() {
            cout << 'Default constructor.' << endl;
            m_p = NULL;
        }
    
        explicit remote_integer(const int n) {
            cout << 'Unary constructor.' << endl;
            m_p = new int(n);
        }
    
        remote_integer(const remote_integer& other) {
            cout << 'Copy constructor.' << endl;
     
            m_p = NULL;
            *this = other;
        }
    
    #ifdef MOVABLE
        remote_integer(remote_integer&& other) {
            cout << 'MOVE CONSTRUCTOR.' << endl;
     
            m_p = NULL;
            *this = other; // WRONG
        }
    
    #endif // #ifdef MOVABLE
        remote_integer& operator=(const remote_integer& other) {
            cout << 'Copy assignment operator.' << endl;
    
            if (this != &other) {
                delete m_p;
    
                if (other.m_p)
                    m_p = new int(*other.m_p);
                else
                    m_p = NULL;
            }
    
            return *this;
        }
    
    #ifdef MOVABLE
        remote_integer& operator=(remote_integer&& other) {
            cout << 'MOVE ASSIGNMENT OPERATOR.' << endl;
    
            if (this != &other) {
                delete m_p;
     
                m_p = other.m_p;
                other.m_p = NULL;
            }
            return *this;
        }
    
    #endif // #ifdef MOVABLE
        ~remote_integer() {
            cout << 'Destructor.' << endl;
            delete m_p;
        }
    
        int get() const {
            return m_p ? *m_p : 0;
        }
    
    private:
        int * m_p;
    };
    
    remote_integer frumple(const int n) {
        if (n == 1729) {
            return remote_integer(1729);
        }
    
        remote_integer ret(n * n);
    
        return ret;
    }
    
    int main() {
        remote_integer x = frumple(5);
    
        cout << x.get() << endl;
    
        remote_integer y = frumple(1729);
    
        cout << y.get() << endl;
    }
    
    #define MOVEABLE
    
    Unary constructor.
    Copy constructor.
    Copy assignment operator.
    Destructor.
    25
    Unary constructor.
    1729
    Destructor.
    Destructor.
    Unary constructor.
    MOVE CONSTRUCTOR.
    Copy assignment operator.
    Destructor.
    25
    Unary constructor.
    1729
    Destructor.
    Destructor.

    (Компілятор тут здійснює RVO, але не NRVO. Як зауважено раніше, деякі виклики коснтрукторів копіювання усуваються компілятором завдяки RVO і NRVO, але компілятор не завжди в змозі застосувати їх. Конструктор переносу оптимізує те, що не зміг компілятор.)

    Рядок всередині конструктора переносу з позначкою WRONG викликає оператор присвоєння копіюванням! Це компілюється та запускається, але втрачається ціль конструктора переносу..

    Що ж cталось? Згадайте з C++98/03, що іменовані посилання л-значення це л-значення (якщо ви пишете int& r = *p; тоді r це л-значення) і неіменовані посилання л-значення це теж л-значення (нехай дано vector v(10, 1729), у разі виклику v[0] повертається int&, неіменоване посилання л-значення адресу якого ми можемо видобути). П-значення поводяться інакше:

    • Іменовані посилання п-значення це л-значення.
    • Неіменовані посилання п-значення це п-значення.

    Іменоване посилання п-значення це л-значення, бо воно може багато разів згадуватись в тексті програми, і з ним можна виконати багато дій. Якби воно було п-значенням, тоді перша ліпша операція над ним могла б обікрасти його, що відбилось би на наступних діях. Крадіжка має бути непомітною, тож такий підхід заборонено. З іншого боку, неіменоване посилання п-значення не може згадуватись більш як один раз, тож тут п-значеннєвість залишається.

    Якщо ви дійсно маєте намір виконати свій конструктор копіювання через оператор присвоєння переносом, ви маєте бути спроможні переносити з л-значень трактуючи їх як п-значення. Для цього існує std::move() з C++0x , реалізуємо цю функції наново:

    #include <stddef.h>
    #include <iostream>
    #include <ostream>
    
    using namespace std;
    
    template <typename T> struct RemoveReference {
         typedef T type;
    };
    
    template <typename T> struct RemoveReference<T&> {
         typedef T type;
    };
    
    template <typename T> struct RemoveReference<T&&> {
         typedef T type;
    };
    
    template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {
        return t;
    }
    
    class remote_integer {
    public:
        remote_integer() {
            cout << 'Default constructor.' << endl;
            m_p = NULL;
        }
    
        explicit remote_integer(const int n) {
            cout << 'Unary constructor.' << endl;
            m_p = new int(n);
        }
    
        remote_integer(const remote_integer& other) {
            cout << 'Copy constructor.' << endl;
    
            m_p = NULL;
            *this = other;
        }
    
    #ifdef MOVABLE
        remote_integer(remote_integer&& other) {
            cout << 'MOVE CONSTRUCTOR.' << endl;
    
            m_p = NULL;
            *this = Move(other); // RIGHT
        }
    #endif // #ifdef MOVABLE
    
        remote_integer& operator=(const remote_integer& other) {
            cout << 'Copy assignment operator.' << endl;
    
            if (this != &other) {
                delete m_p;
    
                if (other.m_p)
                    m_p = new int(*other.m_p);
                else
                    m_p = NULL;
            }
            return *this;
        }
    
    #ifdef MOVABLE
        remote_integer& operator=(remote_integer&& other) {
            cout << 'MOVE ASSIGNMENT OPERATOR.' << endl;
     
            if (this != &other) {
                delete m_p;
    
                m_p = other.m_p;
                other.m_p = NULL;
            }
            return *this;
        }
    #endif // #ifdef MOVABLE
    
        ~remote_integer() {
            cout << 'Destructor.' << endl;
            delete m_p;
        }
    
        int get() const {
            return m_p ? *m_p : 0;
        }
    
    private:
        int * m_p;
    };
    
    remote_integer frumple(const int n) {
        if (n == 1729)
            return remote_integer(1729);
      
        remote_integer ret(n * n);
    
        return ret;
    }
    
    int main() {
        remote_integer x = frumple(5);
    
        cout << x.get() << endl;
    
        remote_integer y = frumple(1729);
    
        cout << y.get() << endl;
    }
    

    Unary constructor.
    MOVE CONSTRUCTOR.
    MOVE ASSIGNMENT OPERATOR.
    Destructor.
    25
    Unary constructor.
    1729
    Destructor.
    Destructor.

    (std::move() і Move() взаємозамінні, бо втілені однаково.) Як працює std::move()? Зараз я збираюсь сказати, що все діло в магії. (Тут подано повне пояснення; воно не складне, але використовує виведення аргументів шаблону і прибирання посилань, яке ми зустрінемо при розгляді досконалого перенаправлення.) Я можу оминути магію через простий приклад. Дано л-значення типу string, як up у вищенаведенному прикладі з розв'язання перевантаження, std::move(up) викликає string&& std::move(string&). Поветається неіменоване посилання п-значення, яке є п-значенням. У випадку із strange() з того ж прикладу, std::move(strange()) викликає string&& std::move(string&&). І знов, повертається неіменоване посилання п-значення, яке є п-значенням.

    std::move() корисне і в інших місцях, окрім втілення конструкторів переносу за допомогою операторів присвоєння копіювання. Де б ви не мали л-значення, яке надалі не потрібне, ви можете написати std::move(ваш вираз л-значення) для задіяння семантики переносу.

    семантика переносу: переносимі члени

    Класи зі cтандарту C++0x (наприклад vector, string, regex) мають конструктори переносу й оператори присвоєння переносом, і ми бачили як реалізувати їх в наших класах, які вручну керують ресурсами (наприклад remote_integer). Але як бути з класами, що містять переносимі члени даних (наприклад vector, string, regex, remote_integer)? Компілятор не створює автоматично конструктори переносу й оператори присвоєння переносом для нас. Отже, ми маємо написати їх самостійно. На щастя, з std::move(), це дуже просто:

    #include <stddef.h>
    #include <iostream>
    #include <ostream>
    #include <utility>
    
    using namespace std;
    
    class remote_integer {
    public:
        remote_integer() {
            cout << 'Default constructor.' << endl;
            m_p = NULL;
        }
    
        explicit remote_integer(const int n) {
            cout << 'Unary constructor.' << endl;
            m_p = new int(n);
        }
    
        remote_integer(const remote_integer& other) {
            cout << 'Copy constructor.' << endl;
    
            if (other.m_p) 
                m_p = new int(*other.m_p);
            else
                m_p = NULL;
        }
    
        remote_integer(remote_integer&& other) {
            cout << 'MOVE CONSTRUCTOR.' << endl; 
            m_p = other.m_p;
            other.m_p = NULL;
        }
    
        remote_integer& operator=(const remote_integer& other) {
            cout << 'Copy assignment operator.' << endl;
    
            if (this != &other) {
                delete m_p;
    
                if (other.m_p) 
                    m_p = new int(*other.m_p);
                else
                    m_p = NULL;
            }
    
            return *this;
        }
    
        remote_integer& operator=(remote_integer&& other) {
            cout << 'MOVE ASSIGNMENT OPERATOR.' << endl;
    
            if (this != &other) {
                delete m_p;
    
                m_p = other.m_p;
                other.m_p = NULL;
            }
    
            return *this;
        }
    
        ~remote_integer() {
            cout << 'Destructor.' << endl;
            delete m_p;
        }
    
        int get() const {
            return m_p ? *m_p : 0;
        }
    
    private:
        int * m_p;
    };
    
    class remote_point {
    public:
        remote_point(const int x_arg, const int y_arg)
            : m_x(x_arg), m_y(y_arg) { }
    
        remote_point(remote_point&& other)
            : m_x(move(other.m_x)),
              m_y(move(other.m_y)) { }
    
        remote_point& operator=(remote_point&& other) {
            m_x = move(other.m_x);
            m_y = move(other.m_y);
            return *this;
        }
    
        int x() const { return m_x.get(); }
        int y() const { return m_y.get(); }
    
    private:
        remote_integer m_x;
        remote_integer m_y;
    };
    
    remote_point five_by_five() {
        return remote_point(5, 5);
    }
    
    remote_point taxicab(const int n) {
        if (n == 0)
            return remote_point(1, 1728);
    
        remote_point ret(729, 1000);
    
        return ret;
    }
    
    int main() {
        remote_point p = taxicab(43112609);
    
        cout << '(' << p.x() << ', ' << p.y() << ')' << endl;
    
        p = five_by_five();
    
        cout << '(' << p.x() << ', ' << p.y() << ')' << endl;
    }
    

    Unary constructor.
    Unary constructor.
    MOVE CONSTRUCTOR.
    MOVE CONSTRUCTOR.
    Destructor.
    Destructor.
    (729, 1000)
    Unary constructor.
    Unary constructor.
    MOVE ASSIGNMENT OPERATOR.
    MOVE ASSIGNMENT OPERATOR.
    Destructor.
    Destructor.
    (5, 5)
    Destructor.
    Destructor.

    Як видно, почленне копіювання здійснюється легко. Зауважте, що оператор присвоєння переносом remote_point не зобов'язаний робити перевірки на самоприсвоєння, бо її робить remote_integer. Також зверніть увагу, що визначені за невказанням іншого конструктор копіювання, оператор присвоєння копіюванням і деструктор роблять все правильно.

    Тепер, ви маєте вичерпні дані про семантику переносу. (Але сподіваюсь, що не вичерпані!) Для перевірки щойно отриманої неймовірної сили, розв'яжіть приклад з частини проблема копіювання з operator+() (застосований до remote_integer замість string). Ця вправа залишається читачеві.

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

    проблема перенаправлення (forwarding)

    Правила C++98/03 для л-значень, п-значень, посилань і шаблонів здаються милими доки програміст не спробує написати дуже узагальнений код. Припустимо ви створюєте повністю узагальнену функцію outer() ціллю якої є отримання довільної кількості параметрів і передання (перенаправлення (forward) їх) довільній функції inner(). Цьому існує багато прикладів. Функція фабрика make_shared(args) перенаправляє args конструктору T і повертає shared_ptr. (Так уможливлюється зберігання об'єкта T і блоку керування його лічильником посилань в одній динамічно виділенній ділянці пам'яті, цим досягається швидкодія рівна інтрузивному підрахунку посилань, коли лічильник зберігається в самому об'єкті.) Обгортки на кшталт function перенаправляють аргументи до функторів, які вони зберігають, і т.д. У цій статті нас цікавить лиш перенаправляння аргументів від outer() до inner(). Визначення типу значення, що повертається з outer() це окрема задача (іноді досить легка, наприклад, make_shared(args) завжди повертає shared_ptr, але постає потреба в іншій особливості C++0x - decltype для здійснення повністю узагальненної версії).

    Безаргументний випадок розв'язується сам собою, перейдемо одразу до одного аргументу. Спробуймо написати outer():

    template <typename T> void outer(T& t) {
        inner(t);
    }
    

    Проблемою цього outer() в тому, що його не можна викликати зі змінюваними п-значеннями. Якщо inner() отримує const int&, тоді inner(5) скомпілюється. А от outer(5) ні; T буде виведено як int, і int& не прив'яжеться до 5.

    Добре, спробуймо так:

    template <typename T> void outer(const T& t) {
        inner(t);
    }
    

    Якщо inner() отримує int&, тоді порушується відповідність константності, отже знов не зкомпілюється.

    Зараз, ви можете перевантажити outer() для T& і для const T&, і це спрацює. Тоді ви зможете викликати outer() начебто це inner().

    На жаль, це не переносимо на випадок з багатьма аргументами. Потрібно буде перевантажувати T1& і const T1&, T2& і const T2& і т.д. для кожного аргументу, отримуючи експоненційну кількість перевантажень. (VC9 SP1 tr1::bind() це відчайдух, що зробив це до 5 аргументів, залучив 63 перевантаження. Складно було б пояснити користувачам чому вони не можуть викликати зв'язаний функтор з п-значеннями на кшталт 1729 інакше як через все це пояснення. Викорінення цих перевантажень потребує огидної препроцесорної машинерії.)

    Проблема перевантаження важка та принципово нерозв'язна в C++98/03 (якщо не вдаватись до згадуваної машинерії препроцесора, яка значно сповільнить компіляцію і зробить код насправді складним для читання). Однак, посилання п-значення розв'язують проблему перевантаження вишуканим способом.

    (Правила ініціалізації та розв'язання перевантажень були пояснені до показування шаблону семантики переносу, але зараз ми розглянемо шаблон досконалого перенаправлення до пояснення правил виведення аргументів шаблону та прибирання посилань. Так це матиме більше сенсу.)

    шаблон досконалого перенаправлення (perfect forwarding)

    Досконале перенаправлення дозволяє вам написати єдину шаблонну функцію, що приймає N довільних аргументів і прозоро перенаправляє їх довільній функції. Їхня змінювана/стала л-/п-значеннєва природа буде збережена, що дозволить outer() використовуватись як inner() і як додаткова винагорода взаємодіяти із семантикою переносу. Це просто, хоч і виглядає спочатку як магія:

    #include <iostream>
    #include <ostream>
    
    using namespace std;
    
    void inner(int&, int&) {
        cout << "inner(int&, int&)" << endl;
    }
    
    void inner(int&, const int&) {
        cout << "inner(int&, const int&)" << endl;
    }
    
    void inner(const int&, int&) {
        cout << "inner(const int&, int&)" << endl;
    }
    
    void inner(const int&, const int&) {
        cout << "inner(const int&, const int&)" << endl;
    }
    
    template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {
        inner(forward<T1>(t1), forward<T2>(t2));
    }
    
    int main() {
        int a = 1;
        const int b = 2;
    
        cout << "Directly calling inner()." << endl;
    
        inner(a, a);
        inner(b, b);
        inner(3, 3);
    
        inner(a, b);
        inner(b, a);
    
        inner(a, 3);
        inner(3, a);
    
        inner(b, 3);
        inner(3, b);
    
        cout << endl << "Calling outer()." << endl;
    
        outer(a, a);
        outer(b, b);
        outer(3, 3);
    
        outer(a, b);
        outer(b, a);
    
        outer(a, 3);
        outer(3, a);
    
        outer(b, 3);
        outer(3, b);
    }
    

    Directly calling inner().
    inner(int&, int&)
    inner(const int&, const int&)
    inner(const int&, const int&)
    inner(int&, const int&)
    inner(const int&, int&)
    inner(int&, const int&)
    inner(const int&, int&)
    inner(const int&, const int&)
    inner(const int&, const int&)

    Calling outer().
    inner(int&, int&)
    inner(const int&, const int&)
    inner(const int&, const int&)
    inner(int&, const int&)
    inner(const int&, int&)
    inner(int&, const int&)
    inner(const int&, int&)
    inner(const int&, const int&)
    inner(const int&, const int&)

    Два рядки! Досконале перенаправлення потребує всього два рядки! Це чудово.

    Це показує, що outer() перенаправляє t1 і t2 в inner() прозоро; inner() зважаючи на л-/п-значеннєвість і сталість.

    Подібно std::move(), std::identity і std::forward() визначені в модулі C++0x ; Я показую як вони реалізовані.

    template <typename T> struct Identity {
        typedef T type;
    };
    
    template <typename T> T&& Forward(typename Identity<T>::type&& t) {
        return t;
    }
    

    Використання identity призводить до необхідності явного вказання T під час виклику forward.

    forward(t);     // помилка часу компіляції
    forward<T>(t);  // добре
    

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

    посилання п-значення: виведення аргументів шаблони і прибирання посилань

    Посилання п-значення та шаблони взаємодіють особливом чином. Ось як це відбувається:

    #include <iostream>
    #include <ostream>
    #include <string>
    
    using namespace std;
    
    template <typename T> struct Name;
    
    template <> struct Name<string> {
        static const char * get() {
            return "string";
        }
    };
     
    template <> struct Name<const string> {
        static const char * get() {
            return "const string";
        }
    };
     
    template <> struct Name<string&> {
        static const char * get() {
            return "string&";
        }
    };
     
    template <> struct Name<const string&> {
        static const char * get() {
            return "const string&";
        }
    };
     
    template <> struct Name<string&&> {
        static const char * get() {
            return "string&&";
        }
    };
     
    template <> struct Name<const string&&> {
        static const char * get() {
            return "const string&&";
        }
    };
     
    template <typename T> void quark(T&& t) {
        cout << "t: " << t << endl;
        cout << "T: " << Name<T>::get() << endl;
        cout << "T&&: " << Name<T&&>::get() << endl;
        cout << endl;
    }
     
    string strange() {
        return "strange()";
    }
     
    const string charm() {
        return "charm()";
    }
     
    int main() {
        string up("up");
        const string down("down");
     
        quark(up);
        quark(down);
        quark(strange());
        quark(charm());
    }
    

    t: up
    T: string&
    T&&: string&

    t: down
    T: const string&
    T&&: const string&

    t: strange()
    T: string
    T&&: string&&

    t: charm()
    T: const string
    T&&: const string&&

    Явна спеціалізація Name уможливлює роздрук типів.

    Коли ми викликаємо quark(up), виведення аргументів шаблону виконане. quark() шаблонна функція з шаблонним параметром T, але ми не надали явний аргумент шаблону (який мав би вигляд quark(up)). Натомість, шаблонний аргумент може бути виведений через порівняння типу параметра функції T&& з типом аргументу функції (л-значенням типу string).

    C++0x перетворює тип параметра функції і тип аргументу функції перед ти як їх співставляти.

    Спочатку перетворюється тип аргументу функції. Особливе правило задіюється (N2798 14.8.2.1 [temp.deduct.call]/3): коли тип параметра функції T&&, де T - це параметр шаблону, і аргумент функції - це л-значення типу A, використовується тип A& для виведення аргументу шаблону. (Це особливе правило не застосовується ні до типів параметру функції в вигляді T& або const T&, які поводяться як і раніше в C++98/03, ні до const T&&.) У випадку з quark(up), це значить, що ми перетворюємо string на string&.

    Під час перетворення типу параметра функції і C++98/03 і C++0x відкидає посилання (C++0x відкидає як посилання л- так і п-значення). В усіх чотирьох випадках ми переходимо від T&& до T.

    Отже, ми отримуємо T як наслідок перетворення типу аргументу функції. От чому quark(up) друкує 'T: string&' і quark(down) друкує 'T: const string&'; up і down це л-значення, тому вони задіюють особливе правило. strange() і charm() це п-значення, вони обробляються за звичайними правилами, які призводять до того, що quark(strange()) друкує 'T: string' і quark(charm()) друкує 'T: const string'.

    Заміна відбувається після виведення аргументу шаблону. Кожне входження параметра шаблону T замінюється на виведений аргумент шаблону. В quark(strange()), T це string, отже T&& це string&&. Схожим чином, в quark(charm()), T це const string, тому T&& це const string&&. Однак, quark(up) і quark(down) задіюють інше особливе правило.

    В quark(up), T це string&. Заміна його на T&& створює string& &&. Посилання на посилання в C++0x прибираються, і правило прибирання таке, що посилання л-значення заразливі. X& &, X& && і X&& & прибираються в X&. Тільки X&& && скорочується до X&&. Затім, string& && переходить string&. У шаблонах, речі, які виглядають наче посилання п-значення, необов'язково є такими. quark(up) інстанціює quark<string&>(). За цієї інстанціації T&& це string&. Ми бачимо це через Name::get(). Подібно, коли quark(down) інстанціює quark<string&>(), T&& це const string&. В C++98/03, ви можливо використовували приховану константність параметрів шаблону (шаблон функції, що приймає T& можна викликати з const Foo; що може бути приведений до T&, тоді він перетвориться на const Foo&). В C++0x, л-значеннєвість може ховатись в параметрах шаблону.

    Добре, що ж ці два правила дають нам? Всередині quark(), тип T&& має ту саму л-/п-значеннєвість і сталість, що й аргумент функції quark(). Отак п-значення зберігають л-/п-значеннєвість і сталість для досконалого перенаправлення.

    Вихідна стаття

    Advertisements
  • Категорії:Uncategorized Позначки:,
    1. Коментарів ще немає.
    1. No trackbacks yet.

    Залишити відповідь

    Заповніть поля нижче або авторизуйтесь клікнувши по іконці

    Лого WordPress.com

    Ви коментуєте, використовуючи свій обліковий запис WordPress.com. Log Out / Змінити )

    Twitter picture

    Ви коментуєте, використовуючи свій обліковий запис Twitter. Log Out / Змінити )

    Facebook photo

    Ви коментуєте, використовуючи свій обліковий запис Facebook. Log Out / Змінити )

    Google+ photo

    Ви коментуєте, використовуючи свій обліковий запис Google+. Log Out / Змінити )

    З’єднання з %s