Разница между std :: reference_wrapper и простым указателем?

100

Зачем это нужно std::reference_wrapper? Где его использовать? Чем он отличается от простого указателя? Как его производительность по сравнению с простым указателем?

Лауринас Лазаускас
источник
4
По сути, это указатель, который вы используете .вместо->
MM
5
@MM Нет, использование .не работает так, как вы предлагаете (если в какой-то момент предложение точки оператора не будет принято и интегрировано :))
Коломбо,
3
Именно такие вопросы меня расстраивают, когда мне приходится работать с новым C ++.
Nils
Чтобы следовать за Columbo, std :: reference_wrapper используется с его get()функцией-членом или с его неявным преобразованием обратно в базовый тип.
Макс Барраклаф

Ответы:

88

std::reference_wrapperполезно в сочетании с шаблонами. Он обертывает объект, сохраняя указатель на него, позволяя переназначать и копировать, имитируя его обычную семантику. Он также указывает некоторым шаблонам библиотеки хранить ссылки вместо объектов.

Рассмотрим алгоритмы в STL, которые копируют функторы: вы можете избежать этой копии, просто передав ссылочную оболочку, ссылающуюся на функтор, а не на сам функтор:

unsigned arr[10];
std::mt19937 myEngine;
std::generate_n( arr, 10, std::ref(myEngine) ); // Modifies myEngine's state

Это работает, потому что…

  • reference_wrapperS перегрузка,operator() чтобы их можно было вызывать точно так же, как объекты функций, на которые они ссылаются:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • … (Не) как обычные ссылки, копирование (и присвоение) reference_wrappersпросто присваивает указатель.

    int i, j;
    auto r = std::ref(i); // r refers to i
    r = std::ref(j); // Okay; r refers to j
    r = std::cref(j); // Error: Cannot bind reference_wrapper<int> to <const int>

Копирование ссылочной оболочки практически эквивалентно копированию указателя, которое настолько дешево, насколько это возможно. Все вызовы функций, присущие его использованию (например, те, которые используются operator()), должны быть просто встроены, поскольку они являются однострочными.

reference_wrappers создаются через std::refиstd::cref :

int i;
auto r = std::ref(i); // r is of type std::reference_wrapper<int>
auto r2 = std::cref(i); // r is of type std::reference_wrapper<const int>

Аргумент шаблона определяет тип и квалификацию объекта, на который имеется ссылка; r2ссылается на a const intи дает только ссылку на const int. Вызовы ссылочных оболочек с constфункторами в них будут вызывать только constфункции-члены operator().

Инициализаторы Rvalue запрещены, поскольку их разрешение принесет больше вреда, чем пользы. Поскольку rvalues ​​все равно будут перемещены (и с гарантированным исключением копирования, даже если этого частично избежать), мы не улучшаем семантику; мы можем ввести висячие указатели, поскольку обертка ссылок не продлевает время жизни указателя.

Библиотечное взаимодействие

Как упоминалось ранее, можно make_tupleуказать сохранить ссылку в результате tuple, передав соответствующий аргумент через reference_wrapper:

int i;
auto t1 = std::make_tuple(i); // Copies i. Type of t1 is tuple<int>
auto t2 = std::make_tuple(std::ref(i)); // Saves a reference to i.
                                        // Type of t2 is tuple<int&>

Обратите внимание, что это немного отличается от forward_as_tuple: Здесь rvalues ​​в качестве аргументов не допускаются.

std::bindпоказывает то же поведение: он не копирует аргумент, но сохраняет ссылку, если это reference_wrapper. Полезно, если этот аргумент (или функтор!) Не нужно копировать, но остается в области видимости, пока используется bind-функтор.

Отличие от обычных указателей

  • Дополнительного уровня синтаксической косвенности нет. Указатели должны быть разыменованы, чтобы получить l-значение для объекта, на который они ссылаются; reference_wrappers имеют неявный оператор преобразования и могут вызываться как объект, который они обертывают.

    int i;
    int& ref = std::ref(i); // Okay
  • reference_wrappers, в отличие от указателей, не имеет нулевого состояния. Они должны быть инициализированы ссылкой или другойreference_wrapper .

    std::reference_wrapper<int> r; // Invalid
  • Сходство reference_wrapperзаключается в семантике неглубокого копирования: указатели и s можно переназначать.

Коломбо
источник
В чем-то std::make_tuple(std::ref(i));превосходит std::make_tuple(&i);?
Лауринас Лазаускас
6
@LaurynasLazauskas Это другое. Последний, который вы показали, сохраняет указатель i, а не ссылку на него.
Columbo
Хм ... Думаю, я до сих пор не могу различить этих двоих так хорошо, как хотелось бы ... Что ж, спасибо.
Лауринас Лазаускас
@Columbo Как возможен массив ссылочных оболочек, если у них нет нулевого состояния? Разве массивы обычно не начинаются со всех элементов, установленных в нулевое состояние?
анатолий 06
2
@anatolyg Что мешает вам инициализировать этот массив?
Columbo
27

Есть как минимум две мотивирующие цели std::reference_wrapper<T>:

  1. Он должен дать ссылочную семантику объектам, передаваемым в качестве параметра значения в шаблоны функций. Например, у вас может быть большой функциональный объект, который вы хотите передать, std::for_each()который принимает параметр своего функционального объекта по значению. Чтобы избежать копирования объекта, вы можете использовать

    std::for_each(begin, end, std::ref(fun));

    Передача аргументов в std::reference_wrapper<T>к std::bind()выражению довольно часто , чтобы связать аргументы по ссылке , а не по значению.

  2. При использовании std::reference_wrapper<T>с std::make_tuple()соответствующим элементом кортежа становится а, T&а не а T:

    T object;
    f(std::make_tuple(1, std::ref(object)));
Дитмар Кюль
источник
Не могли бы вы привести пример кода для первого случая?
user1708860 05
1
@ user1708860: ты имеешь ввиду не тот, что дан ...?
Дитмар Кюль
Я имею в виду фактический код, который идет с std :: ref (fun), потому что я не понимаю, как он используется (если fun не является объектом, а не функцией ...)
user1708860
2
@ user1708860: да, скорее всего fun, это объект функции (т. е. объект класса с оператором вызова функции), а не функция: если funэто фактическая функция, не std::ref(fun)иметь цели и потенциально замедлять выполнение кода.
Дитмар Кюль
23

Еще одно отличие, с точки зрения самодокументируемого кода, заключается в том, что использование reference_wrapperобъекта фактически отменяет право собственности на объект. Напротив, a unique_ptrзаявляет о владении, в то время как пустой указатель может принадлежать или не принадлежать (это невозможно узнать, не глядя на множество связанного кода):

vector<int*> a;                    // the int values might or might not be owned
vector<unique_ptr<int>> b;         // the int values are definitely owned
vector<reference_wrapper<int>> c;  // the int values are definitely not owned
Эдвард Лопер
источник
3
Если это не код до C ++ 11, первый пример должен подразумевать необязательные, не имеющие владельца значения, например, для поиска в кеше на основе индекса. Было бы неплохо, если бы std предоставил нам что-то стандартное для представления ненулевого, принадлежащего значения (уникальные и общие варианты)
Bwmat
Возможно, это не так важно в C ++ 11, где голые указатели почти всегда будут заимствованными значениями.
Эллинг,
reference_wrapperпревосходит необработанные указатели не только потому, что ясно, что они не принадлежат, но и потому, что это не может быть nullptr(без махинаций), и, следовательно, пользователи знают, что они не могут пройти nullptr(без махинаций), и вы знаете, что вам не нужно проверьте это.
underscore_d
19

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

std::vector<std::reference_wrapper<T>> vec; // OK - does what you want
std::vector<T&> vec2; // Nope! Will not compile

По сути, это CopyAssignableверсия T&. Каждый раз, когда вам нужна ссылка, но она должна быть назначаемой, использовать std::reference_wrapper<T>или ее вспомогательную функцию std::ref(). Или используйте указатель.


Другие причуды sizeof::

sizeof(std::reference_wrapper<T>) == sizeof(T*) // so 8 on a 64-bit box
sizeof(T&) == sizeof(T) // so, e.g., sizeof(vector<int>&) == 24

И сравнение:

int i = 42;
assert(std::ref(i) == std::ref(i)); // ok

std::string s = "hello";
assert(std::ref(s) == std::ref(s)); // compile error
Барри
источник
1
@LaurynasLazauskas Можно напрямую вызывать объекты функций, содержащиеся в оболочке. Это также объясняется в моем ответе.
Columbo
2
Поскольку эталонная реализация - это просто указатель внутри, я не могу понять, почему обертки добавляют какое-либо косвенное обращение или штраф за производительность
Рига
4
Когда дело доходит до кода выпуска, это не должно быть более косвенным, чем простая ссылка
Рига,
3
Я ожидаю, что компилятор встроит тривиальный reference_wrapperкод, сделав его идентичным коду, который использует указатель или ссылку.
Дэвид Стоун
4
@LaurynasLazauskas: std::reference_wrapperимеет гарантию, что объект никогда не будет нулевым. Рассмотрим члена класса std::vector<T *>. Вы должны изучить весь код класса, чтобы увидеть, может ли этот объект когда-либо хранить a nullptrв векторе, тогда как с std::reference_wrapper<T>, у вас гарантированно будут действительные объекты.
Дэвид Стоун