std::ref和std::cref的使用和原理分析

目录

1.用法

2.std::reference_wrapper介绍

3.std::ref原理分析

4.std::cref原理分析

5.总结


1.用法

它的定义如下:

std::ref:用于包装按引用传递的值。

std::cref:用户包装按const引用传递的值。

C++本身就有引用(&),那为什么C++11又引入了std::ref(或者std::cref)呢?

这主要是考虑函数式编程(如std::bind或std::thread)在使用时,是对参数直接拷贝,而不是引用。这一点在讲解std::bind是也说的很清楚,bind **函数的所有实参(含第1个实参)都是按值传递的。**如果有不清楚的地方可参考其定义或下面的博客:

C++中的std::bind深入剖析-CSDN博客

std::ref 和 std::cref 只是尝试模拟引用传递,并不能真正变成引用,在非模板情况下,std::ref根本没法实现引用传递,只有模板自动推到类型时,ref能包装类型reference_wrapper来代替原本会被识别的值类型,而reference_wrapper能隐式转换为被引用的值的引用类型,但是并不能被用作 & 类型。

下面举个例子来说明一下它的用法:

cpp 复制代码
#include <functional>
#include <iostream>
 
void f(int& n1, int& n2, const int& n3)
{
    std::cout << "函数中: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    ++n1; // 增加存储于函数对象的 n1 副本
    ++n2; // 增加 main() 的 n2
    // ++n3; // 编译错误
}
 
int main()
{
    int n1 = 1, n2 = 2, n3 = 3;
    std::function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    n1 = 10;
    n2 = 11;
    n3 = 12;
    std::cout << "函数前: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    bound_f();
    std::cout << "函数后: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
}

输出:

cpp 复制代码
函数前: 10 11 12
函数中: 1 11 12
函数后: 10 12 12

从上面的例子中可以看到,执行完f,n1的值仍然是1,n2的值已经改变,这说明std::bind使用的是参数的拷贝而不是引用,这也就是为什么C++11要引入std::ref和std::cref的原因了,接下来分析std::ref的实现(std::cref不作分析,因为std::cref和std::ref唯一的差别只是引用变成了const而已)。

2.std::reference_wrapper介绍

std::reference_wrapper是一个模板类,用于包装引用,使其能够在容器中存储或以引用的形式传递。它提供类似引用的语法,并且可以与标准容器一起使用,因为容器无法直接存储引用。

其实使用std::ref时,后台真正起作用的关键类是std::reference_wrapper,它才是真正包装引用的类。它的实现如下:

cpp 复制代码
template <class _Ty>
class reference_wrapper
#if !_HAS_CXX20
    : public _Weak_types<_Ty>
#endif // !_HAS_CXX20
{
public:
    static_assert(is_object_v<_Ty> || is_function_v<_Ty>,
        "reference_wrapper<T> requires T to be an object type or a function type.");

    using type = _Ty;

    template <class _Uty, enable_if_t<conjunction_v<negation<is_same<_Remove_cvref_t<_Uty>, reference_wrapper>>,
                                          _Refwrap_has_ctor_from<_Ty, _Uty>>,
                              int> = 0>
    _CONSTEXPR20 reference_wrapper(_Uty&& _Val) noexcept(noexcept(_Refwrap_ctor_fun<_Ty>(_STD declval<_Uty>()))) {
        _Ty& _Ref = static_cast<_Uty&&>(_Val);
        _Ptr      = _STD addressof(_Ref);
    }

    _CONSTEXPR20 operator _Ty&() const noexcept {
        return *_Ptr;
    }

    _NODISCARD _CONSTEXPR20 _Ty& get() const noexcept {
        return *_Ptr;
    }

private:
    _Ty* _Ptr{};

public:
    template <class... _Types>
    _CONSTEXPR20 auto operator()(_Types&&... _Args) const
        noexcept(noexcept(_STD invoke(*_Ptr, static_cast<_Types&&>(_Args)...))) // strengthened
        -> decltype(_STD invoke(*_Ptr, static_cast<_Types&&>(_Args)...)) {
        return _STD invoke(*_Ptr, static_cast<_Types&&>(_Args)...);
    }
};

从源代码中可以看出以下几点信息:

1)有一个类成员_Ptr,类型为所引用类型的指针,用于存储实际对象的地址

2)std::reference_wrapper的构造函数中的限制条件:

cpp 复制代码
conjunction_v<negation<is_same<_Remove_cvref_t<_Uty>, reference_wrapper>>,
                                          _Refwrap_has_ctor_from<_Ty, _Uty>>

_Remove_cvref_t : 把类型中的引用、const、volatile去掉

is_same: 判断两个数据类型是否一样

negation: 逻辑非

conjunction_v : 逻辑与

_Refwrap_has_ctor_from定义如下:

cpp 复制代码
// CLASS TEMPLATE reference_wrapper
template <class _Ty>
void _Refwrap_ctor_fun(_Identity_t<_Ty&>) noexcept;
template <class _Ty>
void _Refwrap_ctor_fun(_Identity_t<_Ty&&>) = delete;

template <class _Ty, class _Uty, class = void>
struct _Refwrap_has_ctor_from : false_type {};

template <class _Ty, class _Uty>
struct _Refwrap_has_ctor_from<_Ty, _Uty, void_t<decltype(_Refwrap_ctor_fun<_Ty>(_STD declval<_Uty>()))>> : true_type {};

从template <class _Ty> void _Refwrap_ctor_fun(_Identity_t<_Ty&&>) = delete;这行代码可以看出_Refwrap_has_ctor_from是拒绝右值的,所以我们可以看出std::reference_wrapper的构造是拒绝右值引用的

接下来在std::reference_wrapper构造函数里面,形参对应的是被持有对象的左值引用类型,其接受ref函数接受的左值引用变量_Ref。通过addressof(_Ref)函数取出该变量的地址,将这个地址信息存入成员变量_Ptr中。

3)重载类型转换标识符 operator _Ty&()和_Ty& get(),这个操作符提供了该对象类型到被包装类型的左值引用类型的类型转换。从而可以让这个对象像未包装前的类型一样去使用。

4)重载函数调用操作符 operator() ,这对应的被包装对象是可调用对象如lambda的数据类型的版本。基本原理和对值类型变量的原理一致

5)std::reference_wrapper与普通引用最大的不同是:该引用可以拷贝或赋值

从上面的代码中,可以看出来。为了保证引用类型在经过函数模板或者类模板中的值传递过程中可以保持引用信息。这里面采用将传入变量包装成另外一个新的对象,在这个新的对象中持有被包装对象的地址信息。在函数模板和类模板的值传递过程中,对这个新的对象进行值传递,其内部的被包装的对象地址信息可以得到保存。在函数模板或者内模板内部使用这个新的对象的时候,可以通过重载的类型转换函数将被包装变量的地址信息转换还原成相应的引用,对这个引用进行操作。从而达到操作外部变量的作用。

下面演示将 std::reference_wrapper 作为引用的容器使用,这令使用多重索引访问同一容器称为可能。

cpp 复制代码
#include <algorithm>
#include <functional>
#include <iostream>
#include <list>
#include <numeric>
#include <random>
#include <vector>
 
void println(auto const rem, std::ranges::range auto const& v)
{
    for (std::cout << rem; auto const& e : v)
        std::cout << e << ' ';
    std::cout << '\n';
}
 
int main()
{
    std::list<int> l(10);
    std::iota(l.begin(), l.end(), -4);
 
    // 不能在 list 上用 shuffle(要求随机访问),但能在 vector 上使用它
    std::vector<std::reference_wrapper<int>> v(l.begin(), l.end());
 
    std::ranges::shuffle(v, std::mt19937{std::random_device{}()});
 
    println("list 的内容: ", l);
    println("list 的内容,通过经混洗的 vector 所见: ", v);
 
    std::cout << "倍增初始化式列表中的值...\n";
    std::ranges::for_each(l, [](int& i) { i *= 2; });
 
    println("list 的内容,通过经混洗的 vector 所见: ", v);
}

输出:

cpp 复制代码
list 的内容: -4 -3 -2 -1 0 1 2 3 4 5
list 的内容,通过经混洗的 vector 所见: -1 2 -2 1 5 0 3 -3 -4 4
倍增初始化式列表中的值...
list 的内容,通过经混洗的 vector 所见: -2 4 -4 2 10 0 6 -6 -8 8

3.std::ref原理分析

cpp 复制代码
// FUNCTION TEMPLATES ref AND cref
template <class _Ty>
_NODISCARD _CONSTEXPR20 reference_wrapper<_Ty> ref(_Ty& _Val) noexcept {
    return reference_wrapper<_Ty>(_Val);
}

template <class _Ty>
void ref(const _Ty&&) = delete;

template <class _Ty>
_NODISCARD _CONSTEXPR20 reference_wrapper<_Ty> ref(reference_wrapper<_Ty> _Val) noexcept {
    return _STD ref(_Val.get());
}

有了之前的std::reference_wrapper介绍,std::ref的理解就简单很多了。从源代码中可以看出以下几点信息:

1)std::ref是一个模板函数,返回值是模板类std::reference_wrapper

2)从第二个函数可以看到,std::ref不允许传递右值引用参数,即无法包装右值引用传递的值
std::ref的传入参数可以是一个普通的引用,也可以是另外一个std::reference_wrapper对象

示例如下:

cpp 复制代码
#include <iostream>
#include <functional>

void func(int& value) {
    value *= 2;
}

int main() {
    int number = 42;
    auto refNumber = std::ref(number);

    func(refNumber);  // 使用可修改的引用作为参数

    std::cout << "func Value: " << number << std::endl;

    return 0;
}

调用std::ref返回一个类型为std::reference_wrapper的refNumber, 调用func前隐式转换成这样int&类型的参数, 然后就能顺利的改变 refNumber 的值了。

4.std::cref原理分析

cpp 复制代码
template <class _Ty>
_NODISCARD _CONSTEXPR20 reference_wrapper<const _Ty> cref(const _Ty& _Val) noexcept {
    return reference_wrapper<const _Ty>(_Val);
}

template <class _Ty>
void cref(const _Ty&&) = delete;

template <class _Ty>
_NODISCARD _CONSTEXPR20 reference_wrapper<const _Ty> cref(reference_wrapper<_Ty> _Val) noexcept {
    return _STD cref(_Val.get());
}

std::cref只是在std::ref基础上加了一个const,表示它返回的这个引用不能修改值。其它都一样。可以在需要引用的地方使用。这在函数参数传递中特别有用,因为它允许我们在不进行拷贝的情况下传递常量对象,同时保持引用的语义。

示例如下:

cpp 复制代码
#include <iostream>
#include <functional>

void printValue(const int& value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    int number = 42;
    auto crefNumber = std::cref(number);

    printValue(crefNumber);  // 使用常量引用传递参数

    return 0;
}

5.总结

总的来说,std::refstd::cref 是用于创建引用包装器的有用工具,但它们通常需要与其他技术(如 std::bind 或 lambda 表达式)结合使用,让它展现出和普通引用类似的效果,以便与 C++ 的算法库等接口兼容。

std::reference_wrapper - cppreference.com

std::ref, std::cref - cppreference.com

相关推荐
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
CYBEREXP20084 小时前
MacOS M3源代码编译Qt6.8.1
c++·qt·macos
yuanbenshidiaos4 小时前
c++------------------函数
开发语言·c++
yuanbenshidiaos4 小时前
C++----------函数的调用机制
java·c++·算法
tianmu_sama4 小时前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
羚羊角uou5 小时前
【C++】优先级队列以及仿函数
开发语言·c++
姚先生975 小时前
LeetCode 54. 螺旋矩阵 (C++实现)
c++·leetcode·矩阵
FeboReigns5 小时前
C++简明教程(文章要求学过一点C语言)(1)
c语言·开发语言·c++
FeboReigns5 小时前
C++简明教程(文章要求学过一点C语言)(2)
c语言·开发语言·c++
264玫瑰资源库5 小时前
从零开始C++棋牌游戏开发之第二篇:初识 C++ 游戏开发的基本架构
开发语言·c++·架构