C++中的std::bind深入剖析

news2024/12/25 0:43:06

目录

1.概要

2.原理

3.源码分析

3.1._Binder分析

3.2._CALL_BINDER的实现

4.总结


1.概要

        std::bind是C++11 中的一个函数模板,用于创建一个可调用对象(函数对象或者函数指针)的绑定副本,其中一部分参数被固定为指定值,从而生成一个新的可调用对象。

        std::bind其实就是一个函数适配器,它把一个非标准的函数转换为一个标准的函数,并完成对其的调用,返回结果。

        std::bind将可调用对象(包括函数,仿函数,lambda表达式,函数指针等)与配套参数打包混合在一起,形成一个对象,对这个对象进行调用时,能自动识别可调用对象调用方法与配套参数解析传入,常用于延时调用与部分参数“间接消除”。

       std::bind能很好的工作,它是怎么做到的呢?本文主要从以下几个方面来理解:

        1)  对可调用对象有无返回值的处理?

        2)形参怎么保存的?参数如何绑定的?形参如果需要返回,该怎么办?

        3)如果有占位符时,占位符到底是什么 ?占位符的类型与实际参数类型不相同,又是如何替换的?占位符顺序如何调整的?

2.原理

        std::bind的实现原理如下图所示:

模拟实现如下:

#include <iostream>
#include <tuple>

using namespace std;

//1. 占位符定义
template<size_t idx>
struct placeholder{}; 

template<size_t idx>
using ph = placeholder<idx>; 

constexpr ph<0> _0{}; //定义一个constexpr类型的占位符对象(_0),并用大括号初始化。
constexpr ph<1> _1{}; //定义一个constexpr类型的占位符对象(_1)
constexpr ph<2> _2;   //定义一个constexpr类型的占位符对象(_2)
constexpr ph<3> _3;   //定义占位符对象(_3)

//2. 参数选择:do_select_param会根据是否为占位符来选择合适的实参。
//2.2 泛化版本:arg不是占位符
template<class Args, class Params>
struct do_select_param
{
    decltype(auto) operator()(Args& arg, Params&&) //arg不是占位符,说明arg本身就是一个真正的实参,直接返回。
    {
        return arg;  //由于arg是个引用,decltype(auto)结果也是arg的引用
    }
};

//2.3 特化:arg为占位符情况。
template<size_t idx, class Params>
struct do_select_param<placeholder<idx>, Params> //注意Params为bind返回的绑定对象被调用时,传入其中的参数包
{
    decltype(auto) operator()(placeholder<idx>, Params&& params) //这里的params是个tuple对象。
    {
        return std::get<idx>(std::forward<Params>(params));//根据占位符取出params参数包中的实参。
    }
};

//2.1 根据占位符选择合适的实参
template<class Args, class Params>
decltype(auto) select_param(Args& arg, Params&& params)
{
    //注意,其中的arg是bind绑定时传入的实参,可能是实参或占位符。而params是bind返回的可调用对象被执行时传入的实参。
    //如果绑定时是占位符,会do_select_param会分派到其特化的版本,否则分派到其泛化版本。
    return do_select_param<Args, Params>{}(arg, std::move(params)); //params是副本,统一用move而不用forwared!
}

//3. binder类及辅助函数
//3.3 绑定对象(obj)的调用: 其中args表示传入bind函数的参数,params表示传入obj可调用对象的参数。
template<size_t... idx, class Callable, class Args, class Params>
decltype(auto) bind_invoke(std::index_sequence<idx...>, Callable& obj, Args& args, Params&& params)
{

    //根据args是否是占位符来选择合适的实参,并传给可调用对象obj。
    //注意:为了提高效率,参数是以move的形式(右值)被传递给obj可调动对象的。
    return std::invoke(obj, select_param(std::get<idx>(args), std::move(params))...);//C++17, invoke(func, 参数1, 参数2, ...)
}

//3.2 binder类(核心类)
template<class Callable, class... Args>
class binder
{
    using Seq = std::index_sequence_for<Args...>;   //等价于std::make_index_sequence<sizeof...(Args)>
                                                    //会创建类似一个index_sequence<0,1,2...>的类
    //保存bind函数的所有实参(即可调用对象及其实参)
    using args_t = std::tuple<std::decay_t<Args>...>; //注意,decay_t去掉其引用、const\volatile等特性)
    using callable_t = std::decay_t<Callable>; //可调用对象的类型(注意,decay_t去掉其引用、cv等特性)

    callable_t mObj; //以副本的形式保存可调用对象
    args_t mArgs; //以副本的形式保存可调用对象的所有实参。(打包放在tuple中)
public:

    //注意,不管是可调用对象(callableObj),还是它的实参(args)均会根据其左右值特性,被复制或移动到Binder类中,以副本的形式保存起来。
    explicit binder(Callable&& callableObj, Args&& ... args)
                 :mObj(std::forward<Callable>(callableObj)),mArgs(std::forward<Args>(args)...)
    {
    }

    //Binder仿函数的调用
    template<class... Params>
    decltype(auto) operator()(Params&& ... params) //可调用对象被调用,并传入参数。
    {
        //第1个参数:升序的整数序列。第4个参数将传入的params实参转化为tuple对象。
        //注意:std::forward_as_tuple被定义成tuple<_Types&&...>(_STD forward<_Types>(_Args)...);
        //      这说明params将以引用的形式被保存在tuple中!!!
        return bind_invoke(Seq{}, mObj, mArgs, std::forward_as_tuple(std::forward<Params>(params)...));
    }
};

//3.1 bind辅助函数
template <class Callable, class... Args>
decltype(auto) bind(Callable&& callableObj, Args&& ... args)
{
    return binder<Callable, Args...>(std::forward<Callable>(callableObj), std::forward<Args>(args)...);
}

可调用对象的执行流程如下

 (1) 将tuple展开为变参:绑定可调用对象时,是将可调用对象的形参(可能含占位符)保存在tuple中。到了调用阶段,就必须反过来将tuple展开为可变参数。因为这个可变参数才是可调用对象的形参。这里借助一个整型序列来将tuple变为可变参数,在展开tuple的过程中还需要根据占位符来选择合适实参,即占位符要替换为调用实参。

  (2) 根据占位符来选择合适的实参(如select_param函数)

      ①tuple中可能含有占位符,如果发现某个元素类型为占位符,则从调用时生成的实参tuple(如params)中取出相应的元素,用来作为变参的一个参数。如上面的select_param(ph<0>,{4,5,6}),ph<0>是个占位符,表示该处的实参是其后的{4,5,6}这个tuple中的0位置元素,即4。(具体的实现见do_select_param特化版本)

       ②如果某个类型不为占位符时,则直接从绑定时生成的形参tuple(如mArgs)中出取参数,用来作为变参的一个参数。如select_param(1,{4,5,6}),由于第1个实参为1,不是占位符,因此直接将1这个实参取出,传入invoke函数(具体实现见do_select_param泛化版本)

下面再来看看vs2019环境下std::bind的具体实现吧。

3.源码分析

本文以Visual Studio 2019下,选C++17为编译标准,bind为C++的模板函数,比较简单,先看原型,有两种形式:

// FUNCTION TEMPLATE bind (implicit return type)
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)...);
}

// FUNCTION TEMPLATE bind (explicit return type)
template <class _Ret, class _Fx, class... _Types>
_NODISCARD _CONSTEXPR20 _Binder<_Ret, _Fx, _Types...> bind(_Fx&& _Func, _Types&&... _Args) {
    return _Binder<_Ret, _Fx, _Types...>(_STD forward<_Fx>(_Func), _STD forward<_Types>(_Args)...);
}

1)第一个是隐式返回值的场景,_Unforced是空类,起占位的作用,后来用_Unforced来特化的一个标记而已。一般不显示指定返回值的std::bind都会调用这个函数。

2)第二个是显式返回值的场景,_Ret实际是返回类型,特别的显示指出。

3.1._Binder分析

本文的重点会转移到这里,毕竞bind只是一个空壳,真正发挥作用的,还是要看_Binder这个模板类

template <class _Ret, class _Fx, class... _Types>
class _Binder : public _Binder_result_type<_Ret, _Fx>::type { // wrap bound callable object and arguments
private:
    using _Seq    = index_sequence_for<_Types...>;
    using _First  = decay_t<_Fx>;
    using _Second = tuple<decay_t<_Types>...>;

    _Compressed_pair<_First, _Second> _Mypair;

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

#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;
    }

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

#undef _CALL_BINDER
};

这个看起来复杂,实际STL代码看多了,也比较简单,主要分这几个功能模块:

1)类型提取:在元编程中,模板的参数可不仅是类型传入设定,还是代表输入的意思,经常会对输入的模板类型进行处理,处理后一般做输出,这里就用到了。
        a) using _Seq :将输入的可变参数个数转成序列,index_sequence_for的定义如下:

  template <class... _Types>
  using index_sequence_for = make_index_sequence<sizeof...(_Types)>;

其实就是根据可变参数的总大小生成一个从小到大的序列{0,1,2,3,...,N},如果还有不明白的地方,可参考:

C++14之std::index_sequence和std::make_index_sequence-CSDN博客

        b)  using _First:对可调用对象进行一定的退化处理,作pair的第一个模板参数
        c)using _Second:对参数类型每一个进行一定的退化处理,然后同样的展开,作为tuple的参数输入,得到的tuple作pair的第二个模板参数,所以bind函数的所有实参(含第1个实参)都是按值传递的,即它们均通过复制或移动的方式以副本的形式保存起来的。可以通过对实参实施std::ref的方式来达到按引用存储的效果

2)数据保存:_Compressed_pair<_First, _Second> _Mypair,这个pair类型是xmemory文件中的内容,实现的也比较巧妙,注意它还有第三个隐藏的bool型模板参数,表示是否是空类且可再派生,不再细提了。所有的数据都存到这个pair中了,可调用对象为pair第一个,所有参数为pair第二个。
3)构造函数:_Binder是一个显式的构造函数,只是将数据完美转发到pair中
4)重载()调用:有两个形式,一个带const,一个不带const的,小细节也可以看出写库的人非常细心,毕竞const对象调用不了非const函数。函数参数类型..._Unbound是非绑定的参数,可以间接减少参数的直接传入,返回值为auto,用了decltype推断,其是对外延迟调用的实现重点,重点要看_CALL_BINDER的实现,怎么实现了参数补全与占位符的替换。

小结:

调用std::bind时,会调用_Binder的构造函数,将调用对象_Func与参数_Args...保存到类的pair中,然后通过重载()对外提供调用方法,调用()传入是非绑定对象,非绑定对象个数可以小于_Func要求的参数,并且可以用std::placeholders::_n调整参数顺序。

3.2._CALL_BINDER的实现

我们先看一下_CALL_BINDER的实现,上文_Binder代码已经有了,也就是调用_Call_binder模板函数,给其传了5个参数,分别是_Invoker_ret<_Ret>{}, _Seq{}, _Mypair._Get_first(), _Mypair._Myval2, _STD forward_as_tuple(_STD forward<_Unbound>(_Unbargs)...),一会要一一分析,先看一下原型。

_Call_binder原型分析

// FUNCTION TEMPLATE _Call_binder
template <class _Ret, size_t... _Ix, class _Cv_FD, class _Cv_tuple_TiD, class _Untuple>
_CONSTEXPR20 auto _Call_binder(_Invoker_ret<_Ret>, index_sequence<_Ix...>, _Cv_FD& _Obj, _Cv_tuple_TiD& _Tpl,
    _Untuple&& _Ut) noexcept(noexcept(_Invoker_ret<_Ret>::_Call(_Obj,
    _Fix_arg(_STD get<_Ix>(_Tpl), _STD move(_Ut))...)))
    -> decltype(_Invoker_ret<_Ret>::_Call(_Obj, _Fix_arg(_STD get<_Ix>(_Tpl), _STD move(_Ut))...)) {
    // bind() and bind<R>() invocation
    return _Invoker_ret<_Ret>::_Call(_Obj, _Fix_arg(_STD get<_Ix>(_Tpl), _STD move(_Ut))...);
}

不算复杂,但也值得讲解一下:

1)第一个参数:_Invoker_ret<_Ret>{},实际就是临时构造一个对象传目,其目的非常简单,用这个对象推断出模板的参数的类型_Ret,然后用这个类型作_Invoker_ret<_Ret>::_Call写法上的调用,算是用实参推模板参,再用模板参数作变形,元编程常用手法

2)第二个参数:_Seq{},这是一个非常典型的模板手法,基本同参数一用法,用一index_sequence<_Ix...>类型的构造临时序列对象,推断出序列的具体数值,以便后面再用模板参数size_t... _Ix进行处理,一般都是展开形式,模板中序列的紧缩与展开是一种非常常用的手段。

3)第三个参数:_Mypair._Get_first(),这个比较简单,从pair中获取第一个参数,也就是我们传入的可调用对象。

4)第四个参数:_Mypair._Myval2,这个比较简单,从pair中获取第二个参数,然后得到绑定时传入的参数构成的tuple。

5)第五个参数:forward_as_tuple(_STD forward<_Unbound>(_Unbargs)...),这个是tuple中一个模板函数forward_as_tuple,传入的可变参数(_Types&&... _Args) ,用这些可变参数构成的新的tuple对象tuple<_Types&&...>(_STD forward<_Types>(_Args)...);并返回。关于forward_as_tuple的介绍可参考std::tuple的用法:

C++之std::tuple(一) : 使用精讲(全)-CSDN博客

由此可见std::bind的返回值(称之为“绑定对象”)的所有实参都是按引用传递的,因为这些参数被打包成std::forward_as_tuple,而这里存的都是引用。

到这时我们还没有真正得到我们的问题答案,看来还需要往下看。

_Invoker_ret的实现

// STRUCT TEMPLATE _Invoker_ret
// helper to give INVOKE an explicit return type; avoids undesirable Expression SFINAE
template <class _Rx, bool = is_void_v<_Rx>>
struct _Invoker_ret { // selected for _Rx being cv void
    template <class _Fx, class... _Valtys>
    static _CONSTEXPR20 void _Call(_Fx&& _Func, _Valtys&&... _Vals) noexcept(_Select_invoke_traits<_Fx,
        _Valtys...>::_Is_nothrow_invocable::value) { // INVOKE, "implicitly" converted to void
        _STD invoke(static_cast<_Fx&&>(_Func), static_cast<_Valtys&&>(_Vals)...);
    }
};

template <class _Rx>
struct _Invoker_ret<_Rx, false> { // selected for all _Rx other than cv void and _Unforced
    template <class _Fx, class... _Valtys>
    static _CONSTEXPR20 _Rx _Call(_Fx&& _Func, _Valtys&&... _Vals) noexcept(_Select_invoke_traits<_Fx,
        _Valtys...>::template _Is_nothrow_invocable_r<_Rx>::value) { // INVOKE, implicitly converted to _Rx
        return _STD invoke(static_cast<_Fx&&>(_Func), static_cast<_Valtys&&>(_Vals)...);
    }
};

template <>
struct _Invoker_ret<_Unforced, false> { // selected for _Rx being _Unforced
    template <class _Fx, class... _Valtys>
    static _CONSTEXPR20 auto _Call(_Fx&& _Func, _Valtys&&... _Vals) noexcept(
        _Select_invoke_traits<_Fx, _Valtys...>::_Is_nothrow_invocable::value)
        -> decltype(_STD invoke(static_cast<_Fx&&>(_Func), static_cast<_Valtys&&>(_Vals)...)) { // INVOKE, unchanged
        return _STD invoke(static_cast<_Fx&&>(_Func), static_cast<_Valtys&&>(_Vals)...);
    }
};

这个实际比较简单,还有两个特化版本分别是Rx的特化,以及bool参数的特化,只是显式的返回值不同。这个模板类里面只有一个静态成员函数函数_Call来调用,默认是void返回,有Rx与_Unforced推断两个特化版,实际都是调用std::invoke模板函数来真正实现,这个函数实现是在type_traits文件中,具体就不贴了,意思就是能针对不同类型的可调用体自己区分选择全适的调用方法,比如_Func可能 是普通函数或者仿函数,可能成员函数,可能成员函数函数指针,可能是智能指针warp过的,可能是引用,总之写库的人非常严谨。

C++17之std::invoke: 使用和原理探究(全)-CSDN博客

这里说明一下,_Vals是已经处理过的参数,我们只需知道它只是一个真正调用,具体处理不在这里,其传入的第一个传数obj没什么好说的,第二个参数_Fix_arg(_STD get<_Ix>(_Tpl), _STD move(_Ut))...)是我们分析的重头戏,注意其是可变参数,会按照这个_Fix_arg形式展开。

_Fix_arg实现

看这个模板函数具体实现前,先看其传入的两个参数,理解透。

1) get<_Ix>(_Tpl)写法就很妙,从std::bind绑定的tuple中获取第_Ix个对象,可以仔细回味,实参到形参类型推算,再到模板参数的传入传出妙用。

2) _STD move(_Ut),对延迟调用时非绑定传入的参数组成的tuple,右值转换传入。

template <class _Cv_TiD, class _Untuple>
constexpr auto _Fix_arg(_Cv_TiD& _Tid, _Untuple&& _Ut) noexcept(
    noexcept(_Select_fixer<_Cv_TiD>::_Fix(_Tid, _STD move(_Ut))))
    -> decltype(_Select_fixer<_Cv_TiD>::_Fix(_Tid, _STD move(_Ut))) { // translate an argument for bind
    return _Select_fixer<_Cv_TiD>::_Fix(_Tid, _STD move(_Ut));
}

分析起来也简单,主要是第一个参数的_Cv_TiD的提取,方法前面说过了,用tid推出_Cv_TiD类型,再传入到_Select_fixer中作模板输入,这个函数就是一个中转的helper型函数,在元编程手法中也是非常常用的,所以真正还要看_Select_fixer

巧秒的_Select_fixer

这个实现太秒了,值得看一下源码

// FUNCTION TEMPLATE _Fix_arg
template <class _Cv_TiD, bool = _Is_specialization_v<remove_cv_t<_Cv_TiD>, reference_wrapper>,
    bool = is_bind_expression_v<_Cv_TiD>, int = is_placeholder_v<_Cv_TiD>>
struct _Select_fixer;

template <class _Cv_TiD>
struct _Select_fixer<_Cv_TiD, true, false, 0> { // reference_wrapper fixer
    template <class _Untuple>
    static constexpr auto _Fix(_Cv_TiD& _Tid, _Untuple&&) noexcept -> typename _Cv_TiD::type& {
        // unwrap a reference_wrapper
        return _Tid.get();
    }
};

template <class _Cv_TiD>
struct _Select_fixer<_Cv_TiD, false, true, 0> { // nested bind fixer
    template <class _Untuple, size_t... _Jx>
    static constexpr auto _Apply(_Cv_TiD& _Tid, _Untuple&& _Ut, index_sequence<_Jx...>) noexcept(
        noexcept(_Tid(_STD get<_Jx>(_STD move(_Ut))...))) -> decltype(_Tid(_STD get<_Jx>(_STD move(_Ut))...)) {
        // call a nested bind expression
        return _Tid(_STD get<_Jx>(_STD move(_Ut))...);
    }

    template <class _Untuple>
    static constexpr auto _Fix(_Cv_TiD& _Tid, _Untuple&& _Ut) noexcept(
        noexcept(_Apply(_Tid, _STD move(_Ut), make_index_sequence<tuple_size_v<_Untuple>>{})))
        -> decltype(_Apply(_Tid, _STD move(_Ut), make_index_sequence<tuple_size_v<_Untuple>>{})) {
        // call a nested bind expression
        return _Apply(_Tid, _STD move(_Ut), make_index_sequence<tuple_size_v<_Untuple>>{});
    }
};

template <class _Cv_TiD>
struct _Select_fixer<_Cv_TiD, false, false, 0> { // identity fixer
    template <class _Untuple>
    static constexpr _Cv_TiD& _Fix(_Cv_TiD& _Tid, _Untuple&&) noexcept {
        // pass a bound argument as an lvalue (important!)
        return _Tid;
    }
};

template <class _Cv_TiD, int _Jx>
struct _Select_fixer<_Cv_TiD, false, false, _Jx> { // placeholder fixer
    static_assert(_Jx > 0, "invalid is_placeholder value");

    template <class _Untuple>
    static constexpr auto _Fix(_Cv_TiD&, _Untuple&& _Ut) noexcept
        -> decltype(_STD get<_Jx - 1>(_STD move(_Ut))) { // choose the Jth unbound argument (1-based indexing)
        return _STD get<_Jx - 1>(_STD move(_Ut));
    }
};

_Select_fixer实际有4个模板参数,但一般只传入第一个,后面三个可以直接由第一个推断出,也是一种常用写法,第一个表示参数类型,第二表示引用的偏特化,智能指针的那种,第三个是否是bind的表达式,具体细节不再贴了,如果是_Binder类型就为真,也就是说参数还可以是bind对象,好恐怖,其第二种特化就是支持参数里面还有binder嵌套调用太复杂太抽象,我们先不分析了,只能说写库的大佬太强了,考虑太周全了。第四个参数,判断是否是占位符,也是我们分析的重点。里面有个静态模板函数_Fix,用来转换参数的类型与值,也是常用的元编程手法。

  • 特化一 _Select_fixer<_Cv_TiD, true, false, 0>:如果绑定的参数,通过_Is_specialization_v推断是一个wrap引用,用std::ref包装的引用(std::ref返回的是std::reference_wrapper),就返回其get方法,换句话说,这里可以知,这参数必然不是占位符,可以直接处理,其返回类型用了auto与decltype推导。
  • 特化二 _Select_fixer<_Cv_TiD, false, true, 0>:刚说过,支持内嵌_Binder,能递规,太复杂了,只能说大佬真强。
  • 特化三 _Select_fixer<_Cv_TiD, false, false, 0>:最常用的,就是绑定的是一个普通类型,直接返绑定的参数。
  • 特化四 _Select_fixer<_Cv_TiD, false, false, _Jx>:常用的,就是占位符类型,要取非绑定参数,占位符一会再说,先说结论第n个占位符jx就为n,分析实质,_Jx是int的模板输入参数,直接对非绑定的tuple进行get<_Jx - 1>取值,然后直正返回。

什么是占位符

// PLACEHOLDERS
template <int _Nx>
struct _Ph { // placeholder
    static_assert(_Nx > 0, "invalid placeholder index");
};

namespace placeholders {
    _INLINE_VAR constexpr _Ph<1> _1{};
    _INLINE_VAR constexpr _Ph<2> _2{};
    _INLINE_VAR constexpr _Ph<3> _3{};
    _INLINE_VAR constexpr _Ph<4> _4{};
    _INLINE_VAR constexpr _Ph<5> _5{};
    _INLINE_VAR constexpr _Ph<6> _6{};
    _INLINE_VAR constexpr _Ph<7> _7{};
    _INLINE_VAR constexpr _Ph<8> _8{};
    _INLINE_VAR constexpr _Ph<9> _9{};
    _INLINE_VAR constexpr _Ph<10> _10{};
    _INLINE_VAR constexpr _Ph<11> _11{};
    _INLINE_VAR constexpr _Ph<12> _12{};
    _INLINE_VAR constexpr _Ph<13> _13{};
    _INLINE_VAR constexpr _Ph<14> _14{};
    _INLINE_VAR constexpr _Ph<15> _15{};
    _INLINE_VAR constexpr _Ph<16> _16{};
    _INLINE_VAR constexpr _Ph<17> _17{};
    _INLINE_VAR constexpr _Ph<18> _18{};
    _INLINE_VAR constexpr _Ph<19> _19{};
    _INLINE_VAR constexpr _Ph<20> _20{};
} // namespace placeholders

有了代码就明了,不过std命名空间下的placeholders命名空间下的一些_Ph<N>编译器常量罢了,搞的神神秘秘的,而且_Ph非常简单,没有任何内容,只是不同的int进行特化而已,是不是很简单。

占位符顺序如何调整的

由止面特化四可知,get的模板参数为_Jx - 1,也就是说这里才是真正决定顺序的,假如std::bind(fun, std::placeholders::_2, 5.0f,std::placeholders::_1);_Select_fixer模板解析第一个参数用了is_placeholder_v,其也就特化来萃取_Ph中模板参数N的输入,也比较简单,得到_Jx为2,也就是将非绑定的第二个参数作为第一个传入,这里也就完成了参数的顺序的调整。不外乎函数实参到形参类型,形参类型推出模板输入,模板输入知道后再作新的输入,然后再特化萃取,常规操作。 贴一下代码:

template <class _Tx>
struct is_placeholder : integral_constant<int, 0> {}; // _Tx is not a placeholder

template <int _Nx>
struct is_placeholder<_Ph<_Nx>> : integral_constant<int, _Nx> {}; // _Ph is a placeholder

template <class _Tx>
struct is_placeholder<const _Tx> : is_placeholder<_Tx>::type {}; // ignore cv-qualifiers

template <class _Tx>
struct is_placeholder<volatile _Tx> : is_placeholder<_Tx>::type {}; // ignore cv-qualifiers

template <class _Tx>
struct is_placeholder<const volatile _Tx> : is_placeholder<_Tx>::type {}; // ignore cv-qualifiers

template <class _Ty>
_INLINE_VAR constexpr int is_placeholder_v = is_placeholder<_Ty>::value;

占位符类型替换

类型替换也是简单,前面分析已经搞清了占位符的顺序与从tuple获取参数,_Select_fixer::_Fix返回值用auto配合decltype就可以了,推出非绑定参数中tuple的类型,也是模板函数常用手法。

4.总结

std::bind 在 C++11 中引入,提供了一种将可调用对象(如函数、成员函数、函数对象等)与参数进行绑定的方式。然而,随着 C++ 的发展,尤其是 C++11 之后的版本,std::bind 的使用逐渐受到了一些限制,因为 lambda 表达式提供了更加灵活和直观的方式来实现类似的功能。以下是 std::bind 的一些优点和缺点:

优点:

  1. 灵活性std::bind 可以绑定函数、成员函数、函数对象等的参数,提供了一种灵活的方式来创建新的可调用对象。

  2. 与 std::function 结合使用std::bind 创建的可调用对象可以很容易地赋值给 std::function 对象,这允许你将绑定对象传递给需要 std::function 的函数或类。

  3. 支持成员函数绑定:你可以使用 std::bind 将成员函数与对象实例绑定在一起,从而可以像普通函数那样调用成员函数。

  4. 占位符:通过 std::placeholders::_1std::placeholders::_2 等占位符,你可以指定哪些参数在绑定时是未知的,以便在稍后的调用中提供。

缺点:

  1. 语法复杂std::bind 的语法相对复杂,尤其是当涉及到多个参数和占位符时,代码可能会变得难以阅读和理解。

  2. 性能开销:由于 std::bind 可能会创建包含状态的临时对象,这可能会引入一些性能开销。相比之下,内联的 lambda 表达式通常更加高效。

  3. 限制std::bind 不支持完美转发(perfect forwarding)或移动语义(move semantics),这可能会限制其在某些场景下的使用。

  4. 可读性和可维护性:复杂的 std::bind 表达式可能会降低代码的可读性和可维护性。相比之下,lambda 表达式通常更加直观和易于理解。

  5. lambda 表达式的替代:C++11 引入了 lambda 表达式,它们提供了一种更加简洁、灵活和直观的方式来定义匿名函数对象。在许多情况下,lambda 表达式可以替代 std::bind,并且通常更加受欢迎。

  6. 类型推断问题std::bind 在类型推断方面可能不如 lambda 表达式灵活。特别是当涉及到模板和自动类型推断时,lambda 表达式通常更容易使用。

        综上所述,尽管 std::bind 提供了一种灵活的方式来绑定可调用对象的参数,但在许多情况下,lambda 表达式提供了更加简洁、直观和高效的替代方案。因此,在编写新的 C++ 代码时,通常建议使用 lambda 表达式而不是 std::bind

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1658452.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

​​​​【收录 Hello 算法】4.4 内存与缓存

目录 4.4 内存与缓存 4.4.1 计算机存储设备 4.4.2 数据结构的内存效率 4.4.3 数据结构的缓存效率 4.4 内存与缓存 在本章的前两节中&#xff0c;我们探讨了数组和链表这两种基础且重要的数据结构&#xff0c;它们分别代表了“连续存储”和“分散存储”两种物理…

Qt常用基础控件总结

一、按钮部件 按钮部件共同特性 Qt 用于描述按钮部件的类、继承关系、各按钮的名称和样式,如下图: 助记符:使用字符"&“可在为按钮指定文本标签时设置快捷键,在&之后的字符将作为快捷键。比如 “A&BC” 则 Alt+B 将成为该按钮的快捷键,使用”&&qu…

铁山靠之数学建模 - Matlab入门

Matlab基础 1. Matlab界面与基本操作1.1 matlab帮助系统1.2 matlab命令1.3 matlab功能符号1.4 matlab的数据类型1.5 函数计算1.6 matlab向量1.7 matlab多项式1.8 M文件1.9 函数文件1.10 matlab的程序结构1.11 echo、warning和error函数1.12 交互输入1.13 程序调试1.14 设置断点…

游戏陪玩平台app小程序H5源码交付游戏陪玩接单软件游戏陪玩源码 陪玩小程序陪玩工作室运营模式陪玩管理系统游戏陪玩工作室怎么做

提供陪玩平台源码&#xff0c;陪玩系统源码&#xff0c;陪玩app源码&#xff0c;团队各部门配备齐全&#xff0c;分工明确&#xff0c;及时对接开发进度&#xff0c;保证开发效率 一、陪玩平台源码的功能介绍 1、派单大厅:陪玩系统源码的派单大厅内支持用户通过语音连麦的方式…

idea已配置的git仓库地址 更换新的Git仓库地址 教程

文章目录 目录 文章目录 更改流程 小结 概要更改流程技术细节小结 概要 先在idea控制台走一下流程 先将本地的git仓库删除 1. 查看当前远程仓库地址&#xff1a; 在终端或命令行中&#xff0c;导航到你的项目目录&#xff0c;并运行以下命令查看当前的远程仓库地址&#xff…

QT+MYSQL数据库处理

1、打印Qt支持的数据库驱动&#xff0c;看是否有MYSQL数据库驱动 qDebug() << QSqlDatabase::drivers(); 有打印结果可知&#xff0c;没有MYSQL数据库的驱动 2、下载MYSQL数据库驱动&#xff0c;查看下面的文章配置&#xff0c;亲测&#xff0c;可以成功 Qt6 配置MySQL…

智能BI(后端)-- 系统异步化

文章目录 系统问题分析什么是异步化&#xff1f;业务流程分析标准异步化的业务流程系统业务流程 线程池为什么需要线程池&#xff1f;线程池两种实现方式线程池的参数线程池的开发 项目异步化改造 系统问题分析 问题场景&#xff1a;调用的服务能力有限&#xff0c;或者接口的…

phpstudy(MySQL启动又立马停止)问题的解决办法

方法一&#xff1a;查看本地安装的MySQL有没有启动 1.鼠标右击开始按钮选择计算机管理 2.点击服务和应用程序 3.找到服务双击 4.找到MySQL服务 5.双击查看是否启动&#xff0c;如启动则停止他&#xff0c;然后确定&#xff0c;重新打开phpstudy,启动Mysql. 方法二&#xff…

OpenHarmony 实战开发——3.1 Release + Linux 原厂内核Launcher起不来问题分析报告

1、关键字 Launcher 无法启动&#xff1b;原厂内核&#xff1b;Access Token ID&#xff1b; 2、问题描述 芯片&#xff1a;rk3566&#xff1b;rk3399 内核版本&#xff1a;Linux 4.19&#xff0c;是 RK 芯片原厂发布的 rk356x 4.19 稳定版内核 OH 版本&#xff1a;OpenHa…

net7部署经历

1、linux安装dotnet命令&#xff1a; sudo yum install dotnet-sdk-7.0 或者直接在商店里安装 2、配置反向代理 127.0.0.1:5000》localhost 访问后报错 原因&#xff1a;数据表驼峰名&#xff0c; 在windows的数据表不区分大小写&#xff0c;但是在linux里面是默认区分的&…

xiuno(修罗)知乎模板二开优化魔板仿网盘资源社–模板加全套插件

使用说明 以服务器为例搭建教程 ①先安装 PHP7.1 版本 再安装数据库 Mysql ②解压文件&#xff1a;xiunobbs_4.0.4&#xff08;解压到根目录&#xff09;.zip ③解压②完成后找到【plugin】文件夹再解压&#xff1a;plugin(解压到 plugin 文件夹).zip 设置伪静态代码在上面&am…

记录如何查询域名txt解析是否生效

要查询域名的TXT记录&#xff0c;可以使用nslookup命令。具体步骤如下&#xff1a;12 打开命令行终端。输入命令 nslookup -qttxt 域名&#xff0c;将"域名"替换为你要查询的实际域名。执行命令后&#xff0c;nslookup会返回域名的TXT记录值。 如何查询域名txt解析是…

【C++后端项目】负载均衡OJ服务器

文章目录 一、演示项目二、所用技术与开发环境所用技术开发环境 三、项目宏观结构I. 风格&#xff1a;仿leetcodeII. 结构&#xff1a;Browser-Server模式III. 编写思路&#xff1a;编译服务 -> OJ服务 -> 前端设计 四、关于Git分支管理✨4.1 Git 分支结构4.2 Git 分支命…

【linux】主分区,扩展分区,逻辑分区,动态分区,引导分区,标准分区

目录 主分区&#xff0c;扩展分区&#xff0c;逻辑分区 主分区和引导分区 主分区&#xff0c;扩展分区&#xff0c;逻辑分区&#xff08;标准分区&#xff09; 硬盘一般划分为一个“主分区”和“扩展分区”&#xff0c;然后在扩展分区上再分成数个逻辑分区。 磁盘主分区扩展…

调用 gradio 创建聊天网页报错(使用远程服务器)

文章目录 写在前面1、使用默认IP地址&#xff08;失败&#xff09;2、使用本地IP地址&#xff08;失败&#xff09;3、使用远程服务器IP地址&#xff08;成功&#xff09; 写在前面 我复现了github上的 llama-chinese 的工作 使用的是 llama2&#xff0c;环境配置是在远程服务…

如何使用 ArcGIS Pro 计算容积率

容积率是指地上建筑物的总面积与用地面积的比率&#xff0c;数值越小越舒适&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro 计算容积率&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的建筑和小区数据&#xff0c;除了建筑和小区数据&am…

智能合约是什么?搭建与解析

智能合约是一种基于区块链技术的自动化执行合约&#xff0c;它通过编程语言编写&#xff0c;并在区块链网络上部署运行。智能合约是区块链技术的重要组成部分&#xff0c;它使得去中心化应用&#xff08;DApp&#xff09;的开发变得更加便捷和高效。本文将从智能合约的搭建、原…

如何解决 NPM依赖下载超时问题 :npm ERR! network timeout at: https://registry.npmjs.org/猫头虎

如何解决 NPM依赖下载超时问题 &#xff1a;npm ERR! network timeout at: https://registry.npmjs.org/猫头虎 博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试…

正交频分复用回顾(通俗易懂)

OFDM我们知道&#xff0c;叫做正交频分复用&#xff0c;它是4G的一个关键技术&#xff0c;4G的多址技术叫做OFDMA&#xff0c;也就是说4G是通过OFDM来作用户区分的&#xff0c;具体是什么意思呢&#xff1f;继续往下看。 图1 在2G和3G时代&#xff0c; 单用户都是用的一个载波…

Golang——Strconv包

func ParseBool(str string) (value bool, err error) strconv包实现了基本数据类型与其字符串表示的转换&#xff0c;主要有以下常用函数&#xff1a;Atoi()&#xff0c;Itoa()&#xff0c;parse系列函数&#xff0c;format系列函数&#xff0c;append系列函数。 1.1 string与…