Идеальная передача и универсальные ссылки в C++

Original author: Eli Bendersky
  • Translation
Недавно на isocpp.org была опубликована ссылка на статью Eli Bendersky «Perfect forwarding and universal references in C++». В этой небольшой статье есть простой ответ на простой вопрос — для решения каких задач и как нужно использовать rvalue-ссылки.

Одно из нововведений C++11, которое нацелено на увеличение эффективности программ – это семейство методов emplace у контейнеров STL. Например, в std::vector появился метод emplace_back (практически аналог метода push_back) и метод emplace (практически аналог метода insert).
Вот небольшой пример, показывающий предназначение этих новых методов:
class MyKlass {
public:
  MyKlass(int ii_, float ff_) {...}
private:
  {...}
};

some function {
  std::vector<MyKlass> v;
  v.push_back(MyKlass(2, 3.14f));
  v.emplace_back(2, 3.14f);
}

Если проследить за вызовами конструкторов и деструкторов класса MyKlass, во время вызова push_back можно увидеть следующее:

  • Сначала выполняется конструктор временного объекта класса MyKlass
  • Затем, для объекта, непосредственно расположенного внутри вектора, вызывается конструктор перемещения (если такой определен в MyClass, если не определен – тогда вызывается конструктор копирования)
  • Деструктор временного объекта

Как видно, выполняется довольно много работы, большее количество которой не очень то и нужно, так как объект, передаваемый в метод push_back, очевидно является rvalue-ссылкой, и уничтожается сразу после выполнения этого выражения. Таким образом, нет никакой причины создавать и уничтожать временный объект. Почему же, в этом случае, не создать объект сразу внутри вектора? Это именно то, что делает метод emplace_back. Для выражения из примера v.emplace_back(2, 3.14f) выполнится только один конструктор, который создает объект внутри вектора. Без использования временных объектов. emplace_back сам вызывает конструктор MyKlass и передает ему нужные аргументы. Это поведение стало возможным благодаря двум нововведениям С++11: шаблоны с переменным количеством аргументов (variadic templates) и идеальная передача (perfect forwarding). В данной статье я хочу пояснить, как работает идеальная передача и как ее использовать.

Проблема идеальной передачи


Допустим, есть некоторая функция func, принимающая параметры типов E1, E2, …, En. Требуется создать функцию wrapper, принимающую такой же набор параметров. Другими словами – определить функцию, которая передаст принимаемые параметры в другую функцию, не создавая временные переменные, то есть выполнит идеальную передачу.
Для того чтобы конкретизировать задачу, рассмотрим метод emplace_back, который был описан выше. vector::emplace_back передает свои параметры конструктору T не зная ничего о том, чем является T.
Следующим шагом рассмотрим несколько примеров, показывающих как можно добиться подобного поведения без использования нововведений С++11. Для упрощения не будем учитывать необходимость использования шаблонов с переменным количеством параметров аргументов, предположим, что требуется передать только два аргумента.
Первый вариант, который приходит на ум:
template <typename T1, typename T2>
void wrapper(T1 e1, T2 e2) {
    func(e1, e2);
}

Но это очевидно не будет работать как нужно, если func принимает параметры по ссылке, так как wrapper принимает параметры по значению. В этом случае, если func изменяет получаемые по ссылке параметры, это не отразится на параметрах, переданных во wrapper (будут изменены копии, созданные внутри wrapper).
Хорошо, тогда мы можем переделать wrapper, чтобы он принимал параметры по ссылке. Это не будет помехой, если func будет принимать не по ссылке, а по значению, так как func внутри wrapper сделает себе необходимые копии.
template <typename T1, typename T2>
void wrapper(T1& e1, T2& e2) {
    func(e1, e2);
}

Здесь другая проблема. Rvalue не может быть передано в функцию в качестве ссылки. Таким образом вполне тривиальный вызов не скомпилируется:
wrapper(42, 3.14f);                  	// ошибка: инициализация неконстантной ссылки rvalue-значением
wrapper(i, foo_returning_float());   // та же ошибка

И сразу нет, если пришла мысль сделать эти ссылки константными – это тоже не решит проблему. Потому что func может требовать в качестве параметров неконстантные ссылки.
Остается только грубый подход, используемый в некоторых библиотеках: перегрузить функцию для константных не неконстантных ссылок:
template <typename T1, typename T2>
void wrapper(T1& e1, T2& e2)                { func(e1, e2); }

template <typename T1, typename T2>
void wrapper(const T1& e1, T2& e2)          { func(e1, e2); }

template <typename T1, typename T2>
void wrapper(T1& e1, const T2& e2)          { func(e1, e2); }

template <typename T1, typename T2>
void wrapper(const T1& e1, const T2& e2)    { func(e1, e2); }

Экспоненциальный рост. Можно представить, сколько веселья это доставит, когда потребуется обработать какое-то разумное количество параметров реальных функций. Чтобы ухудшить ситуацию С++11 добавляет rvalue ссылки, которые тоже нужно учесть в функции wrapper, и это точно не является расширяемым решением.

Сжатие ссылок и особый вывод типа для rvalue-ссылок


Для объяснения того, как в С++11 реализуется идеальная передача, нужно сначала понять два новых правила, которые были добавлены в этот язык программирования.
Начнем с простого – сжатия ссылок (reference collapsing). Как известно, взятие ссылки на ссылку в С++ не допускается, но это иногда может происходить при реализации шаблонов:
template <typename T>
void baz(T t) {
  T& k = t;
}

Что случится, если вызвать эту функцию следующим образом:
int ii = 4;
baz<int&>(ii);

При инстанцировании шаблона T установится равным int&. Какой же тип будет у переменной k внутри функции? Компилятор «увидит» int& & — а так как это запрещенная конструкция, компилятор просто преобразует это в обычную ссылку. Фактически, до С++11 такое поведение не было стандартизированным, но многие компиляторы принимали и преобразовывали такой код, так как он часто встречается в метапрограммировании. После того, как в С++11 были добавлены rvalue-ссылки, стало важным определить поведение при совмещении различных типов ссылок (например, что значит int&& & ?).
Так появилось правило сжатия ссылок. Это правило очень простое – одиночный амперсанд (&) всегда побеждает. Таким образом – (& и &) это (&), также как и (&& и &), и (& и &&). Единственный случай, при котором в результате сжатия получается (&&) — это (&& и &&). Это правило можно сравнить с результатом выполнения логического ИЛИ, в котором & это 1, а && это 0.
Другое дополнение С++, имеющее прямое отношение к рассматриваемой теме – это правила особого вывода типа (special type deduction rules) для rvalue-ссылок в различных случаях [1]. Рассмотрим пример шаблонной функции:
template <class T>
void func(T&& t) {
}

Не позволяйте двойному амперсанду обмануть Вас – t здесь не является rvalue-ссылкой [2]. При появлении в данной ситуации (когда необходим особый вывод типа), T&& принимает особое значение – когда func инстанцируется, T изменяется в зависимости от переданного типа. Если была передана lvalue типа U, то Т становится U&. Если же U это rvalue, то Т становится просто U. Пример:
func(4);            // 4 это rvalue: T становится int

double d = 3.14;
func(d);            // d это lvalue; T становится double&

float f() {...}
func(f());          // f() это rvalue; T становится float

int bar(int i) {
  func(i);          // i это lvalue; T становится int&
}


Это правило может показаться необычным и даже странным. Оно такое и есть. Но, тем не менее, это правило становится вполне очевидным, когда приходит понимание что это правило помогает решить проблему идеальной передачи.

Реализация идеальной передачи с использованием std::forward


Теперь давайте вернемся к нашей описанной выше шаблонной функции wrapper. Вот как она должна быть реализована с использованием С++11:
template <typename T1, typename T2>
void wrapper(T1&& e1, T2&& e2) {
    func(forward<T1>(e1), forward<T2>(e2));
}

А вот как реализован forward [3]:
template<class T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
  return static_cast<T&&>(t);
}

Рассмотрим следующий вызов:
int ii ...;
float ff ...;
wrapper(ii, ff);

Рассмотрим первый аргумент (второй аналогичен): ii является lvalue, таким образом T1 становится int& в соответствии с правилом особого вывода типа. Получается вызов func(forward<int&>(e1), …). Таким образом, шаблон forward инстанцирован типом int& и получаем следующую версию этой функции:
int& && forward(int& t) noexcept {
    return static_cast<int& &&>(t);
}

Время применить правило сжатия ссылок:
int& forward(int& t) noexcept {
    return static_cast<int&>(t);
}

Другими словами, аргумент передан по ссылке в func, как и требуется для lvalue.
Следующий пример:
wrapper(42, 3.14f);

Здесь аргументы являются rvalue, таким образом T1 становится int. Получаем вызов func(forward(e1), ...). Таким образом, шаблонная функция forward инстанцирована типом int и получаем следующую версию функции:
int&& forward(int& t) noexcept {
    return static_cast<int&&>(t);
}

Аргумент, полученный по ссылке, приводится к rvalue-ссылке, которую и требуется получить от forward.
Шаблонную функцию forward можно рассматривать как некоторую обертку над static_cast<T&&>(t), когда T может принять значение U& или U&&, в зависимости от типа входного аргумента (lvalue или rvalue). Теперь wrapper является одним шаблоном, который обрабатывает любые сочетания типов аргументов.
Шаблонная функция forward реализована в С++11, в заголовочном файле «utility», в пространстве имен std.

Еще один момент, который нужно отметить: использование std::remove_reference. На самом деле forward может быть реализован и без использования этой функции. Сжатие ссылок выполнит всю работу, таким образом, применение std::remove_reference для этого избыточно. Однако, эта функция позволяет вывести T& t в ситуации, когда этот тип не может быть выведен (согласно стандарту С++, 14.8.2.5), поэтому необходимо явно указывать параметры шаблона при вызове std::forward.

Универсальные ссылки


В своих выступлениях, постах в блоге и книгах, Скотт Майерс дает наименование «универсальные ссылки» (universal reference) для rvalue-ссылок, которые в контексте вывода типов. Удачное это наименование или нет, сложно сказать. Что касается меня, когда я первый раз прочитал относящуюся к данной теме главу из новой книги «Effective C++», я почувствовал, что запутался. Более-менее стало все понятно позже, когда я разобрался с лежащими в основе этого механизмами (сжатия ссылок и правил особого вывода типов).
Ловушка заключается в том, что фраза «универсальные ссылки» [4] конечно более кратка и красива, чем «rvalue-ссылки в контексте вывода типов». Но если есть желание на самом деле понять некоторый код, не получится избежать полного описания.

Примеры использования идеальной передачи


Идеальная передача довольно полезна, потому что делает возможным программирование на более высоком уровне. Функции высшего порядка – это функции, которые могут принять другие функции в качестве аргументов или возвращать их. Без идеальной передачи, применение функций высшего порядка довольно обременительно, так как нет удобного способа передать аргументы в функцию внутри функции-обертки. Под термином «функция» я здесь кроме самих функций также имею в виду и классы, конструкторы которых фактически тоже являются функциями.
В начале данной статьи я описывал метод контейнеров emplace_back. Другой хороший пример – это стандартная шаблонная функция make_unique, которую я описывал в предыдущей статье:
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args)
{
    return unique_ptr<T>(new T(std::forward<Args>(args)...));
}

Признаюсь честно, что в той статье я просто игнорировал странный двойной амперсанд и фокусировался на переменном количестве аргументов шаблона. Но сейчас совершенно несложно полностью понять код. Само собой разумеется, что идеальная передача и шаблоны с переменным количеством аргументов очень часто используются вместе, потому что, в большинстве случаев неизвестно, какое количество аргументов принимают функция или конструктор, которым мы передаем эти аргументы.
В качестве примера со значительно более сложным использованием идеальной передачи Вы можете посмотреть реализацию std::bind.

Ссылки на источники


Вот некоторые источники, которые мне очень помогли при подготовке материала:
  1. The 4th edition of «The C++ Programming Language» by Bjarne Stroustrup
  2. The new «Effective Modern C++» by Scott Myers. В этой книге широко обсуждаются «универсальные ссылки». Фактически, данной теме посвящено более пятой части этой книги.
  3. Technical paper n1385: «The forwarding problem: Arguments».
  4. Thomas Becker C++ Rvalue references explained – отлично написанная и очень полезная статья

Примечания:
[1] Также может применяться auto и decltype, здесь я описываю только случай использования шаблона.
[2] Я считаю неудачным решение комитета стандартизации С++ по выбору обозначения для rvalue-ссылок (перегрузка &&). Скотт Майерс признался в своем выступлении (и немного комментировал в своем блоге), что после 3 лет этот материал до сих пор непрост в изучении. И Бьерн Страуструп в The 4th edition of «The C++ Programming Language» при описании std::forward забыл явное указание аргумента шаблона. Можно сделать вывод, что это действительно довольно непростая область.
[3] Это упрощенная версия std::forward из STL C++11. Там еще есть дополнительная версия, перегруженная явно для rvalue аргументов. Я до сих пор стараюсь разобраться, зачем она нужна. Дайте знать, если есть какая-либо идея.
[4] Передающиеся ссылки (forwarding references) – еще одно обозначение, которое я встречал

От переводчика: на CppCon2014 многими (в том числе Мейерсом, Страуструпом, Саффером) было принято решение использовать термин forwarding references вместо universal references.

Пара статей на хабре по данной теме:
Краткое введение в rvalue-ссылки
«Универсальные» ссылки в C++11 или T&& не всегда означает «Rvalue Reference»
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 27

    +3
    У emplace в текущем стандарте неприятная особенность — нельзя делать emplace для aggregate types. Еще довольно сранный недосмотр — невозможность инициализировать ссылки через braced direct-initialization (e.g. int& ref{other_int}).
      0
      Вот блин. Хорошо что сказали, а то я уже раскатал губу… Подождем C++ 17 или какого там, когда он появится.
        0
        Еще довольно сранный недосмотр — невозможность инициализировать ссылки через braced direct-initialization (e.g. int& ref{other_int}).

        Можно, это был баг какой-то версии gcc.
          +1
          Занятно, у меня на убунте 4.8.2, и не собирается. Регресс, что ли?
          Я знаю про ещё один неприятный баг в gcc, связанный с C++11, причём судя по всему его так и не исправили в gcc 4.9 и выше, но я не могу зарепортить его, потому что их трекер закрыт для новых регистраций.
            +2
            Если поменять int на что-нибудь посложнее, то компилироваться перестает. В clang и GCC 4.9 начинает работать, но вообще это видимо баг стандарта, потому что про list-initialization там написано следующее:
            List-initialization of an object or reference of type T is defined as follows:
            • If the initializer list has no elements and T is a class type with a default constructor...
            • Otherwise, if T is an aggregate...
            • Otherwise, if T is a specialization of std::initializer_list<E>...
            • Otherwise, if T is a class type...
            • Otherwise, if T is a reference type, a prvalue temporary of the type referenced by T is list-initialized, and the reference is bound to that temporary. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type. —end note ]
            • Otherwise, if the initializer list has a single element, the object or reference is initialized from that element;...
            • ...
            Тут явно какая-то ошибка, потому что до нижнего пункта в случае ссылок мы никогда не дойдем, а предпоследний пункт говорит нам выдавать compilation error на неконстантные lvalue ссылки, а все остальные инициализировать путем обязательного создания временной переменной и привязки её к ссылке, что тоже весьма странно.

            В С++14 это дело подправили и теперь оно звучит так:
            • ...
            • Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element; if
              a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed.
            • Otherwise, if T is a reference type, a prvalue temporary of the type referenced by T is copy-list-initialized or direct-list-initialized, depending on the kind of initialization for the reference, and the reference is bound to that temporary. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type. —end note ]
            • ...
              +1
              Да, точно, я исследовал этот момент и находил ошибку в списке про list-initialization.
          +1
          К слову, насчет первой хотелки, в теории её нетрудно реализовать, надо просто на нижнем уровне, где конструируется объект, заменить в его инициализации круглые скобочки фигурными. Тогда, если наша структура выглядит как-то так:

          struct A {
              int x;
              double y;
              const char* z;
          };
          

          то после вызова emplace_back(10, 20.0, "abcd"), она будет проинициализирована списком {10, 20.0, "abcd"}. Конечно, в таком подходе есть свои недостатки: для произвольных типов такая инициализация может скрывать конструкторы (скажем, vector<int>(10, 20) — это не то же самое, что vector<int>{10, 20}), для аггрегатов у нас отвалится инициализация копированием (нельзя будет вызвать emplace_back(A{}), например), кроме того мы все равно никак не сможем передать вложенные списки (типа { {1, 2, 3}, {}, {4, 5} }. В целом, если делать так только для аггрегатных типов и починить им инициализацию копированием, перегрузив для неё отдельно emplace_back, то первые две проблемы разрешимы.

          Более того, в теории мы уже сейчас можем заставить стандартные контейнеры себя так вести (правда функцию проверки is_aggregate нам не завезли), стандарт говорит нам про них:
          For the components affected by this subclause that declare an allocator_type, objects stored in these components shall be constructed using the allocator_traits<allocator_type>::construct function and destroyed using the allocator_traits<allocator_type>::destroy function (20.6.8.2). These functions are called only for the container’s element type, not for internal types used by the container. [ Note: This means, for example, that a node-based container might need to construct nodes containing aligned buffers and call construct to place the element into the buffer. —end note]
          а allocator_traits<allocator_type>::construct в свою очередь вызывает метод construct переданного ей аллокатора, если этот метод определен. То есть нам всего лишь нужно создать свой аллокатор с методом construct, который будет делать инициализацию нужным нам способом. На практике все сложнее, путем небольшим плясок с бубном мне удалось завести это для вектора:
          Код
          #include <memory>
          #include <vector>
          #include <string>
          #include <iostream>
          #include <cmath>
          
          template<typename T>
          struct init_list_allocator : public std::allocator<T> {
              template<typename... Args>
              void construct(T* p, Args&&... args)
                  { ::new((void *)p) T{std::forward<Args>(args)...}; }
          
              // Чиним инициализацию копированием
              void construct(T* p, T& copy_construct_arg)
                  { std::allocator<T>::construct(p, copy_construct_arg); }
          
              void construct(T* p, const T& copy_construct_arg)
                  { std::allocator<T>::construct(p, copy_construct_arg); }
          
              void construct(T* p, const T&& copy_construct_arg)
                  { std::allocator<T>::construct(p, copy_construct_arg); }
          
              void construct(T *p, T&& move_construct_arg)
                  { std::allocator<T>::construct(p, move_construct_arg); }
          
              // Без этого не работает, потому что вместо переданного типа аллокатора
              // реализация зачем-то использует Allocator::rebind<T>::other
              template<typename U>
              struct rebind {
                  using other = init_list_allocator<U>;
              };
          
          };
          
          template<class T>
          using improved_vector = std::vector<T, init_list_allocator<T>>;
          
          struct A {
              int x;
              double y;
              const char* z;
          };
          
          int main()
          {
              using namespace std;
              vector<string> strings;
              improved_vector<A> v;
              for (int i = 0; i < 21; ++i) {
                  strings.emplace_back(to_string(i*i));
                  v.emplace_back(i, sqrt(i), strings.back().c_str());
              };
              for (const auto& elem : v)
                  cout << elem.x << ' ' << elem.y << ' ' << elem.z << '\n';
          }
          
          а вот для std::list уже ничего не выходит, потому что в глубинах его реализации объект инициализируется в конструкторе элемента списка (той штуки, которая помимо объекта содержит ещё указатели на соседей), и там в списке инициализации конструктора стоят круглые скобочки. Причем таким образом происходит и в gcc, и в clang. Почему они так поступают, когда стандарт вроде как говорит иное, я не знаю.
            0
            Там, кстати, баг в коде: в вариантах construct, которые принимают rvalue ссылку в качестве аргумента, надо её смувить или сфорвардить перед передачей дальше, в общем, из lvalue преобразовать в xvalue.
          +1
          The new «Effective Modern C++» by Scott Myers
          ох! вот и подоспело, надо заказать…
            +1
            А вот и спасибо за перевод. Многое не знал…
            Вообще говоря, что бы разобраться «не про» с ново-старым стандартом надо, даже, не пара часов. Страшно представить, что будет с 14-м.
              +2
              Рад, что перевод оказался полезен. Да, С++ очень быстро развивается, тоже жду выхода нового стандарта, думаю будет много интересного.
              Например — концепты и аксиомы, описанные в блоге Страуструпа. Хотя, их, скорее всего, увидим только в С++17
                0
                Я совершенно не успеваю следить за всеми новшествами, которые вводит комитет… надо и про аксиомы почитать!
                +1
                Как раз в 14-ом ничего особо принципиального нет, скорее доработка идей 11-го, в котором действительно множество важнейших инноваций. А вот в 17-ом снова ожидаются принципиальные изменения.
                0
                К слову, по стандарту у std::forward есть две версии, которые возвращают одно и то же:

                template<typename _Tp>
                constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept
                {
                    return static_cast<_Tp&&>(__t);
                }
                
                template<typename _Tp>
                constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
                {
                    static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
                            " substituting _Tp is an lvalue reference type");
                    return static_cast<_Tp&&>(__t);
                }
                

                Нетрудно увидеть, что при описанном в статье способе использования нам нужна только первая перегрузка. Поэтому интересно, а для чего нужен второй вариант, да ещё и с такой проверкой (которая также требуется по стандарту)? Видимо есть какие-то другие разумные способы использования std::forward, для которых она нужна?

                На stackoverflow есть такой вопрос, там отвечают, что это уберегает от ошибок. Но при данном способе использования единственные 2 ошибки, которые можно допустить на мой взгляд — это забыть написать после forward тип в угловых скобочках, и тогда компилятор и при одной версии ругается, что не может его вывести, или забыть написать "&&" в списке аргументов функции-обертки (или написать один амперсанд вместо двух), но тогда компилятор нас за руку не ловит и никаких ошибок не выдает.
                  +2
                  Проверка добавлена, чтобы отсечь случай forward rvalue as lvalue, который не имеет никакго смысла и может привести к неопределенному поведению. Вывод типов для forward не может работать, потому что к моменту передачи аргумента в forward реальный тип объекта может быть утерян, так что вариант с forward без указания шаблонного параметра-типа не работал бы.
                    0
                    А в каком контексте forward rvalue имеет смысл? Потому что, если использовать только подобные конструкции:

                    template< class... Args >
                    void some_func( Args&&... args )
                    {
                        T some_object(std::forward<Args>(args)...);
                    }
                    

                    то в качестве аргумента в forward всегда будет подаваться lvalue и, соотвественно, выбираться первая перегрузка.
                      0
                      Наверное только для искусственных случаев с кодом типа:
                      std::forward<some_object>(some_object{})

                      Но какой в этом смысле — не знаю :-)
                        +1
                        Комитет любит полноту, вполне возможно что вторую перегрузку добавили и из таких соображений.
                        К примеру, они могли бы просто проигнорировать случай с make_unique<T[]> (попытка аллокации runtime-sized массива), но вместо этого добавили соответствующую перегрузку и пометили как deleted.
                          +1
                          скорее всего, чтобы не путали forward и move, поскольку они очень похожи.
                      +1
                      Всем доброго дня!

                      Всем интересующимся, в дополнение, советую также прочесть интереснейшую статью Efficient argument passing in C++11, Part1 (Part2, Part3). В ней, вопрос эффективной передачи аргуменов в С++11, охватывается чуть более глобально, а также предлагается подход к реализации runtime perfect forwarding'а (напомню в С++11, perfect forwarding доступен только для шаблонов, т.е. этот механизм нам доступен только в compile time).
                        +1
                        Кто-нибудь видел перевод «perfect-forwarding» на русский как-то иначе, чем «идеальная передача»? Может, есть какие-то другие варианты перевода?
                          0
                          Тоже считаю что «идеальная передача» не является идеальным переводом. Но на просторах рунета (и на хабре) нашел только этот вариант.
                            0
                            Предлагаю вместо буквального перевода — смысловой: «Непосредственная передача» или «Непосредственное размещение».
                              0
                              Звучит разумно. Рассматривал этот вариант и еще «чистая передача». Посетила мысль, что можно было бы сделать опрос в конце статьи на вариант перевода, чтобы следующий переводчик знал, какой вариант более предпочтителен. Но эта статья уже ушла далеко от первых позиций новостной ленты, поэтому боюсь, что голосующих будет не так много, как хотелось бы, кворум может не набраться.
                            +1
                            Мне больше нравится «точная передача», но ссылки на авторитетный источник, использующий этот термин, я дать не могу.
                              0
                              Если речь идет о какой-то «точности», в чем тогда состоит «промах»? Параметры ведь в любом случае попадают по своим местам, просто в традиционном варианте — через посредство временной переменной.
                                +1
                                С «точностью» до всех квалификаторов (const, volatile, ref).

                          Only users with full accounts can post comments. Log in, please.