解析C++中std::ref

本文详细解读了C++11中的std::ref用法,介绍了它是如何通过std::reference_wrapper实现的,特别关注了std::reference_wrapper的工作原理,以及为何std::bind和std::thread需要它。通过实例演示,揭示了std::ref在处理引用传递和函数绑定中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 前言

关于c++中的std::ref,std::ref在c++11引入。本文通过讲解std::ref的常用方式,及剖析下std::ref内部实现,进而再来讲解下std::reference_wrapper,然后我们再进一步分析为什么使用std::ref。

2. std::ref 用法

简单举例来说:

int n1 = 0;
auto n2 = std::ref(n1);

n2++;
n1++;

std::cout << n1 << std::endl;  // 2
std::cout << n2 << std::endl;  // 2

可以看到 是把n1的引用传递给了n2,分别进行加法,可以看到n2是n1的引用,最终得到的值都是2

那么大家可能会想,我都已经有了’int& a = b’的这种引用赋值的语法了,为什么c++11又出现了一个std::ref,我们继续来看例子:

#include <iostream>
#include <thread>

void thread_func(int& n2) { // error, >> int n2
    n2++;
}

int main() {
    int n1 = 0;
    std::thread t1(thread_func, n1);

    t1.join();
    std::cout << n1 << std::endl;
}

我们如果写成这样是编译不过的,除非是去掉引用符号,那么我如果非要传引用怎么办呢?

// snap ...

int main() {
    int n1 = 0;
    std::thread t1(thread_func, std::ref(n1));

    t1.join();
    std::cout << n1 << std::endl; // 1
}

这样可以看到引用传递成功,并且能够达到我们效果,我们再来看个例子:

#include <iostream>
#include <functional>

void func(int& n2) {
    n2++;
}

int main() {
    int n1 = 0;
    auto bind_fn = std::bind(&func, std::ref(n1));

    bind_fn();
    std::cout << n1 << std::endl; // 1
}

这里我们也发现std::bind这样也是需要通过std::ref来实现bind引用。

那么我们其实可以看的出来,std::bind或者std::thread里是做了什么导致我们原来的通过&传递引用的方式失效,或者说std::ref是做了什么才能使得我们使用std::bind和std::thread能够传递引用。

那么我们展开std::ref看看他的真面目,大致内容如下:

template <class _Ty>
reference_wrapper<_Ty> ref(_Ty& _Val) noexcept {
    return reference_wrapper<_Ty>(_Val);
}

这里我们看到std::ref最终只是被包装成reference_wrapper返回,所以关键点还是std::reference_wrapper

3. std::reference_wrapper

关于这个类,我们看下cppreference上的实现形式为:

namespace detail {
template <class T> constexpr T& FUN(T& t) noexcept { return t; }
template <class T> void FUN(T&&) = delete;
}
 
template <class T>
class reference_wrapper {
public:
  // types
  typedef T type;
 
  // construct/copy/destroy
  template <class U, class = decltype(
    detail::FUN<T>(std::declval<U>()),
    std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>()
  )>
  constexpr reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
    : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}
  reference_wrapper(const reference_wrapper&) noexcept = default;
 
  // 赋值
  reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;
 
  // 访问
  constexpr operator T& () const noexcept { return *_ptr; }
  constexpr T& get() const noexcept { return *_ptr; }
 
  template< class... ArgTypes >
  constexpr std::invoke_result_t<T&, ArgTypes...>
    operator() ( ArgTypes&&... args ) const {
    return std::invoke(get(), std::forward<ArgTypes>(args)...);
  }
 
private:
  T* _ptr;
};
 
// deduction guides
template<class T>
reference_wrapper(T&) -> reference_wrapper<T>;

里边有一些语法比较晦涩,我们一点一点的来看

最开始是一个detail的namespace,里边有两个函数,第一个是接收左值引用的,第二个是接收右值引用的,接收右值引用的被delete,不能调用。这里detail是为后边做校验的,大家可能会像,不用右值引用不写就可以了,为啥写了这个函数还要标记为delete。这是因为如果没有第二个函数右值参数是可以传递给第一个函数的,如果写了就会优先匹配到到第二个函数,发现这个函数是delete,不能编译通过,明白了这个我们继续。

接着我们看到reference_wrapper,首先是一个模板,看到很长的一个构造函数,我们拆开来看,template <class U, class = xxx>这种写法,后边那个class=也是在编译期做校验使用,SFINEA的一种实现形式吧,如果class=后边那个编译不过,那么你就不可以使用这个构造函数。

class=后边这段很长的代码:

template <class U, class = decltype(
    detail::FUN<T>(std::declval<U>()),
    std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>()
  )>

首先是一个decltype关键字,得到的是一个类型。decltype内部是使用逗号表达式连接两部分,逗号左边部分调用detail的FUN来校验,std::declval是不用调用构造函数便可以使用类的成员函数,不过只能用于不求值语境。获取U的对象看下是否是右值,上边也说到如果右值则编译不过。如果是左值的话看逗号右边的部分,std::enable_if_t<>, 这里<>中的第一个参数是条件,如果条件满足返回第二个参数,第二个参数是类型, 这里没有第二个参数,默认是void,即如果满足条件可以编译通过,否则编译不通过。条件是std::is_same_v取反,std::is_same_v<>是如果两个模板参数相同类型则是true,否则false。所以reference_wrapper和std::remove_cvref_t<U>不相同则可以通过编译,std::remove_cvref_t这个模板又是去掉U这个类型的const,volatile和引用的属性,单纯两个类型比较。

上边总结就是在调用构造函数时,首先进行校验,传入参数时右值和reference_wrapper类型就不能编译通过。

然后是构造函数的正文:

constexpr reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
    : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}

这里先看下“noexcept(noexcept(detail::FUN(std::forward<U>(u))))”这段代码,不了解noexcept我这里大概讲解下。

语法上来说noexcept分为修饰符和操作符两种分类吧。

修饰符写法是noexcept(expression),expression是常量表达式,expression这个值返回true则编译器认为修饰的函数不抛出异常,这时如果该函数再抛出异常则调用std::abort终止程序,如果值返回false则认为该函数可能会抛出异常。而我们常看到函数声明后边只写一个noexcept,其实也是相当于noexcept(true)。

操作符大都用于模板中,写法就是我们这里缩写的那样noexcept(noexcept(T())),那么这里T()决定该函数是否抛出异常,如果T()会抛出异常那么第二个noexcept就会返回false,否则返回true。

那么这里构造函数就是说如果执行“detail::FUN(std::forward<U>(u))”不会抛出异常,那么就不会抛出异常,这样也是更好的告知编译器一个条件吧。

继续的就是_ptr存放的是传进来参数的地址,这里也是比较关键,相当于是reference_wrapper的实现就是通过保存传进来参数的地址来达到引用的包装(ref wrapper)效果。

构造函数终于讲完了,拷贝构造函数和赋值运算符应该不用讲了

再然后就是看下如何访问了

constexpr operator T& () const noexcept { return *_ptr; }
constexpr T& get() const noexcept { return *_ptr; }

这两个也比较简单,提供了一个get函数和()的重载,实现就是获取_ptr存放地址所指向的值。

template< class... ArgTypes >
  constexpr std::invoke_result_t<T&, ArgTypes...>
    operator() ( ArgTypes&&... args ) const {
    return std::invoke(get(), std::forward<ArgTypes>(args)...);
  }

还有一个实现是给存放的参数是函数类型使用的,也就是重载"()()",可以调用这个函数并传参过去。

最后就是C++17引入的推导指引,顾名思义就是帮助模板类型推导使用的

//// 推导指引
template<class T>
reference_wrapper(T&) -> reference_wrapper<T>;

如果没有这句话,我们构造reference_wraper时,需要这么写reference_wraper<int>(n1),那么有了这句推导指引,我们可以写成这样reference_wraper(n1),方便很多,不用写模板参数类型。

那么接下来我们调用试试看(因为cppreference中实现有些语法用到了C++17或者更高,使用编译器要更高版本或者替换一些语法即可):

void func(int& n2) {
    n2++;
}

int main() {
    int n1 = 0;
    auto bind_fn = std::bind(&func, reference_wrapper(n1));

    bind_fn();
    std::cout << n1 << std::endl;  // 1
}

完美!可以通过, 所以reference_wrapper本质是把对象的地址保存, 访问是取出地址的值。

这里我们借助的是cppreference中实现来讲解的,大家也可以参考自己本地编译器的实现。

4. 为什么使用

我们看下为什么std::bind或者std::thread为什么要使用reference_wrapper,我们以std::bind为例子吧,我们大致去跟踪下std::bind,跟踪的目的是看传递bound参数(即我们传给bind函数的参数)的生命周期,以vs2019的实现为例:

template <class _Fx, class... _Types>
_NODISCARD _CONSTEXPR20 _Binder<_Unforced, _Fx, _Types...> bind(_Fx&& _Func, _Types&&... _Args) {
    return _Binder<_Unforced, _Fx, _Types...>(_STD forward<_Fx>(_Func), _STD forward<_Types>(_Args)...);
}

看到是构造了一个_Binder的对象返回,bound参数作为构造函数的参数传入,

using _Second = tuple<decay_t<_Types>...>; //std::decay_t会移除掉引用属性
_Compressed_pair<_First, _Second> _Mypair;

constexpr explicit _Binder(_Fx&& _Func, _Types&&... _Args)
        : _Mypair(_One_then_variadic_args_t{}, _STD forward<_Fx>(_Func), _STD forward<_Types>(_Args)...) {}

也可以看到构造函数中,参数传递给_Mypair成员。到这里结束。

我们再看下调用时:

#define _CALL_BINDER                                                                  \
    _Call_binder(_Invoker_ret<_Ret>{}, _Seq{}, _Mypair._Get_first(), _Mypair._Myval2, \
        _STD forward_as_tuple(_STD forward<_Unbound>(_Unbargs)...))

template <class... _Unbound>
    _CONSTEXPR20 auto operator()(_Unbound&&... _Unbargs) noexcept(noexcept(_CALL_BINDER)) -> decltype(_CALL_BINDER) {
    return _CALL_BINDER;
}

看到调用时会用到_CALL_BINDER宏,这里调用_Call_binder函数,并把_Mypair传入,再接下来就会调用到我们的函数并传入bound的参数了。

总结下就是std::bind首先将传入的参数存放起来,等到要调用bind的函数就将参数传入,而这里没有保存传入参数的引用,只能保存一份参数的拷贝,如果使用我们上边说的“int& a = b”语法,_Binder类中无法保存b的引用,自然调用时传入的就不是b的引用,所以借助reference_wrapper将传入参数的地址保存,使用是通过地址取出来值进而调用函数。

5. 总结

我来给总结下,首先我们讲解了std::ref的一些用法,然后我们讲解std::ref是通过std::reference_wrapper实现,然后我们借助了cppreference上的实现来给大家剖析了他本质就是存放了对象的地址(类似指针的用法😁),还讲解了noexcept等语法,最后我们讲解了下std::bind为什么要使用到reference_wrapper.

6. 参考

  • https://en.cppreference.com
  • 《深入理解C++11》
<think>我们正在处理关于C++中`std::bind`和`std::forward`的使用问题。参考了提供的引用内容,特别是引用[1]和引用[3],它们分别讨论了`std::bind_front`(C++20)和`std::bind`以及`std::function`的区别。 用户的问题集中在`std::bind`和`std::forward`的使用上。注意,`std::forward`用于完美转发,而`std::bind`用于绑定参数生成新的可调用对象。在C++20中引入了`std::bind_front`和`std::bind_back`(C++23)来改进绑定机制,并支持完美转发。 根据引用[1]:`std::bind_front`和`std::bind_back`可以实现参数的完美转发。这意味着它们能够保留原始参数的值类别(左值或右值)。 引用[3]详细说明了`std::bind`和`std::function`的区别。但用户的问题涉及`std::forward`,因此我们需要解释如何将完美转发与`std::bind`结合使用,并注意在传统`std::bind`中使用完美转发的限制。 核心问题:如何在`std::bind`中实现完美转发?或者为什么需要结合`std::forward`? 然而,需要注意的是,传统的`std::bind`(在C++11中引入)并不直接支持完美转发绑定参数。在绑定参数时,参数会被拷贝或移动(取决于传入的值类别),然后在调用时以左值传递。这意味着,即使我们传入一个右值,绑定后的可调用对象在调用时也会以左值传递该参数。 因此,如果我们希望绑定后的可调用对象能够保留参数的值类别(以便进行完美转发),我们需要使用C++20的`std::bind_front`(或`std::bind_back`)或者使用lambda表达式。 但是,用户可能在使用传统`std::bind`时遇到问题,因此我们需要解释: 1. `std::bind`如何工作,以及它在参数传递上的局限性(不保留值类别)。 2. 如何结合`std::forward`在绑定的可调用对象内部进行转发(但注意,这需要绑定的可调用对象本身是一个转发调用)。 3. 替代方案:使用lambda表达式(更推荐)或C++20的`std::bind_front`。 由于引用[1]提到了`std::bind_front`支持完美转发,我们可以对比说明。 回答结构: 1. 解释`std::bind`的基本用法和局限性(关于完美转发)。 2. 解释`std::forward`的作用(完美转发)。 3. 分析为什么在`std::bind`中直接使用`std::forward`是困难的(因为绑定参数在调用时都是左值)。 4. 提供替代方法:使用lambda表达式实现完美转发,或者使用C++20的`std::bind_front`(如果可用)。 5. 示例代码演示。 注意:用户可能没有指定C++版本,但我们可以同时给出传统C++11和C++20的解决方案。 示例: 传统`std::bind`无法完美转发绑定参数: ```cpp #include <functional> #include <iostream> void foo(int& x) { std::cout << "lvalue: " << x << std::endl; } void foo(int&& x) { std::cout << "rvalue: " << x << std::endl; } int main() { int a = 42; auto bound_foo = std::bind(foo, a); // 绑定左值,内部保存一个拷贝(或引用?实际上,默认是拷贝,除非用std::ref) bound_foo(); // 调用foo(int&) 因为a的拷贝是左值 auto bound_foo2 = std::bind(foo, std::move(a)); // 这里会移动构造一个副本,但调用时仍然是左值 bound_foo2(); // 还是调用foo(int&) // 尝试绑定右值引用 auto bound_foo3 = std::bind(foo, 42); // 绑定一个右值,但42被拷贝(或移动)到绑定对象内部,调用时以左值传递 bound_foo3(); // 调用foo(int&) } ``` 如何解决?使用lambda表达式: ```cpp int main() { int a = 42; auto lambda_foo = [&a]() { foo(a); }; // 捕获左值引用,调用foo(int&) lambda_foo(); auto lambda_foo2 = [a = std::move(a)]() mutable { foo(std::move(a)); }; // 移动捕获,然后转发 lambda_foo2(); // 调用foo(int&&) auto lambda_foo3 = [](auto&& arg) { foo(std::forward<decltype(arg)>(arg)); }; // 泛型lambda,可以完美转发 lambda_foo3(42); // 调用foo(int&&) lambda_foo3(a); // 调用foo(int&) } ``` 或者使用C++20的`std::bind_front`(支持完美转发): ```cpp #if __cplusplus >= 202002L #include <functional> #include <iostream> void foo(int& x) { std::cout << "lvalue: " << x << std::endl; } void foo(int&& x) { std::cout << "rvalue: " << x << std::endl; } int main() { int a = 42; auto bound_foo = std::bind_front(foo, a); // 绑定左值,调用时传递左值 bound_foo(); // 调用foo(int&) auto bound_foo2 = std::bind_front(foo, std::move(a)); // 绑定右值,但注意:这里会移动构造一个副本,但std::bind_front会保留值类别吗? // 实际上,std::bind_front会完美转发绑定的参数。但是,绑定时参数已经被移动了(所以a的值可能已被移走),但调用时,绑定对象内部存储的是右值,所以会以右值传递。 bound_foo2(); // 调用foo(int&&) auto bound_foo3 = std::bind_front(foo, 42); // 绑定右值,调用时传递右值 bound_foo3(); // 调用foo(int&&) } #endif ``` 注意:`std::bind_front`在调用时,会将存储的参数按照其存储的值类别(左值或右值)转发给被调用函数。因此,它实现了完美转发。 总结: - 在C++11/14中,`std::bind`不支持完美转发绑定参数(调用时都是左值),因此如果需要完美转发,建议使用lambda表达式。 - 在C++20中,可以使用`std::bind_front`(或C++23的`std::bind_back`)来实现绑定参数的完美转发。 另外,关于`std::forward`,它通常用在模板函数内部,将参数以原始值类别转发。在绑定场景中,如果我们绑定的可调用对象本身是一个转发调用(例如,绑定一个泛型lambda),那么我们可以使用`std::forward`。 最后,生成相关问题。</think>### C++ 中 `std::bind` 与 `std::forward` 的使用解析 #### 1. **`std::bind` 的核心功能** `std::bind` 用于**绑定参数生成新的可调用对象**,支持: - **参数重排**:改变函数参数的顺序 - **部分绑定**:固定部分参数,延迟传递其他参数 - **成员函数绑定**:绑定类成员函数(需传递对象指针) ```cpp #include <functional> #include <iostream> void func(int a, double b) { std::cout << a << ", " << b << std::endl; } int main() { auto bound = std::bind(func, 42, std::placeholders::_1); bound(3.14); // 输出:42, 3.14 } ``` #### 2. **`std::forward` 的作用(完美转发)** `std::forward` 用于**保留参数的原始值类别**(左值/右值),确保: - 左值参数转发后仍是左值 - 右值参数转发后仍是右值 ```cpp template <typename T> void wrapper(T&& arg) { target(std::forward<T>(arg)); // 完美转发 } ``` #### 3. **`std::bind` 与 `std::forward` 的协作问题** ⚠️ **关键限制**:`std::bind` **默认不保留绑定参数的值类别**。 例如,绑定右值时实际发生**拷贝/移动**,调用时以**左值形式传递**: ```cpp void process(int&) { std::cout << "lvalue\n"; } void process(int&&) { std::cout << "rvalue\n"; } int main() { int x = 10; auto bound = std::bind(process, 20); // 20是右值,但绑定后被存储为对象成员 bound(); // 输出 "lvalue"(非预期!) } ``` #### 4. **解决方案:结合 `std::ref` 或 C++20 新特性** **(1) 使用 `std::ref` 传递引用** 保留左值引用语义: ```cpp auto bound = std::bind(process, std::ref(x)); // 传递左值引用 bound(); // 输出 "lvalue" ``` **(2) C++20 的 `std::bind_front`(推荐)** 支持参数的**完美转发**: ```cpp #if __cplusplus >= 202002L auto bound = std::bind_front(process, 20); // 完美转发绑定 bound(); // 输出 "rvalue"(符合预期) #endif ``` [^1] **(3) 使用 Lambda 表达式替代** 更灵活地控制值类别: ```cpp auto lambda = [arg=20]() mutable { process(std::move(arg)); // 显式转为右值 }; lambda(); // 输出 "rvalue" ``` #### 5. **应用场景对比** | **场景** | **推荐工具** | |------------------------|-----------------------| | 简单参数绑定 | `std::bind` | | 完美转发需求 | `std::bind_front` (C++20) | | 成员函数绑定 | `std::bind` 或 Lambda | | 复杂值类别控制 | Lambda + `std::forward` | > **最佳实践**: > - C++11/14 中优先用 **Lambda + `std::forward`** 实现完美转发 > - C++20+ 直接使用 **`std::bind_front`** 简化代码[^1]
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值