性能文章>【全网首发】诡异!std::bind in std::bind 编译失败>

【全网首发】诡异!std::bind in std::bind 编译失败原创

352634

你好,我是雨乐!

上周的某个时候,正在愉快的摸鱼,突然群里抛出来一个问题,说是编译失败,截图如下:

当时看了报错,简单的以为跟之前遇到的原因一样,随即提出了解决方案,怎奈,短短几分钟,就被无情打脸,啪啪啪😭。为了我那仅存的一点点自尊,赶紧看下原因,顺便把之前的问题也回顾下。

好了,言归正传(此处应为严肃脸),在后面的内容中,将从源码角度分析下之前问题的原因,然后再分析下群里这个问题。

从问题代码说起

好了,先说说之前的问题,在Index中,需要有一个更新操作,简化之后如下

class Index {
public:
    Index() {
        update_ = std::bind(&Index::Update, this, std::placeholders::_1, std::bind(&Index::status, this, std::placeholders::_1));
    }
    std::function<void(const std::string &)> update_;
private:
    void Update(const std::string &value, std::function<std::string(const std::string &)> callback) {
        if(callback) {
            std::cout << "Called update(value) = " << callback(value) << std::endl; 
        }
    }
    std::string Status(const std::string &value) {
        return value;
    }

};

int main() {
    Index idx;
    idx.update_("Ad0");
    return 0;
}

代码本身还是比较简单的,主要在std::bind这块,std::bind的返回值被用作传递给std::bind的一个参数。

编译之后,报错提示如下:

 错误:no match for ‘operator=’ (operand types are ‘std::function<void(const std::__cxx11::basic_string<char>&)>’ and ‘std::_Bind_helper<false, void (Index::*)(const std::__cxx11::basic_string<char>&, std::function<std::__cxx11::basic_string<char>(const std::__cxx11::basic_string<char>&)>), Index*, const std::_Placeholder<1>&, std::_Bind<std::_Mem_fn<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > (Index::*)(const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)>(Index*, std::_Placeholder<1>)> >::type {aka std::_Bind<std::_Mem_fn<void (Index::*)(const std::__cxx11::basic_string<char>&, std::function<std::__cxx11::basic_string<char>(const std::__cxx11::basic_string<char>&)>)>(Index*, std::_Placeholder<1>, std::_Bind<std::_Mem_fn<std::__cxx11::basic_string<char> (Index::*)(const std::__cxx11::basic_string<char>&)>(Index*, std::_Placeholder<1>)>)>}’)
         update_ = std::bind(&Index::Update, this, std::placeholders::_1, std::bind(&Index::status, this, std::placeholders::_1));

经过错误排查,本身std::bind()这个是没问题的,当加上如果对update_进行赋值,就会报如上错误,所以问题就出在赋值这块,即外部std::bind期望的类型与内部std::bind的返回类型不匹配。

定位

单纯从代码上看,内部std::bind()的类型也没问题,于是翻了下cppreference,发现了其中的猫腻,当满足如下情况时候,std::bind()的行为不同(modifies "normal" std::bind behaviour):

  • • std::reference_wrapper

  • • std::is_bind_expression

    ::value == true
  • • std::is_placeholder

    ::value

显然,我们属于第二种情况,即__std::is_bind_expression ::value == true__(另外两种情况不在本文讨论范围内哈)。

根据cppreference对第二种情况的描述:

  • • If the stored argument arg is of type T for which std::is_bind_expression

    ::value == true (for example, another  bind expression was passed directly into the initial call to  bind), then  bind performs function composition: instead of passing the function object that the bind subexpression would return, the subexpression is invoked eagerly, and its return value is passed to the outer invokable object. If the bind subexpression has any placeholder arguments, they are shared with the outer bind (picked out of  u1, u2, ...). Specifically, the argument  vn in the  *INVOKE* operation above is arg( std::forward (uj)...) and the type  Vn in the same call is  std::result_of<T cv &(Uj&&...)>::type&& (until C++17) std::invoke_result_t<T cv &, Uj&&...>&& (since C++17) (cv qualification is the same as that of  g).

上面这块理解比较吃力,简言之,如果传给std::bind()的参数T(在本例中,T为std::bind(&Index::status, this, std::placeholders::_1))满足std::is_bind_expression ::value == true ,那么就会报上面的错误。

为了分析这个原因,研究了下std::bind()(源码),下面结合源码,分析此次报错的原因,然后给出解决方案。

bind从实现上分为以下几类:

  • • 工具:is_bind_expression、is_placeholder、namespace std::placeholders、_Safe_tuple_element_t和__volget,前两个用于模板偏特化;

  • • _Mu:核心模块,此次问题所在。

  • • _Bind:_Bind和_Bind_result,std::bind的返回类型;

  • • 辅助:_Bind_check_arity、__is_socketlike、_Bind_helper和_Bindres_helper

因为本文的目的是分析编译报错原因,所以仅分析_Mu模块,这是bind()的核心,其他都是围绕着这个来的,同时它也是本文问题的根结所在,所以分析此代码即可(至于其他模块,将在下一篇文章进行分析,从源码角度分析bind实现),代码如下:

template<typename _Signature>
    struct is_bind_expression<const volatile _Bind<_Signature>>
    : public true_type { };

template<typename _Arg,
           bool _IsBindExp = is_bind_expression<_Arg>::value,
           bool _IsPlaceholder = (is_placeholder<_Arg>::value > 0)>
    class _Mu;

 template<typename _Arg>
    class _Mu<_Arg, true, false>
    {
    public:
      template<typename _CVArg, typename... _Args>
        auto
        operator()(_CVArg& __arg,
                   tuple<_Args...>& __tuple) const volatile
        -> decltype(__arg(declval<_Args>()...))
        {
          // Construct an index tuple and forward to __call
          typedef typename _Build_index_tuple<sizeof...(_Args)>::__type
            _Indexes;
          return this->__call(__arg, __tuple, _Indexes());
        }
 
    private:
      // Invokes the underlying function object __arg by unpacking all
      // of the arguments in the tuple.
      template<typename _CVArg, typename... _Args, std::size_t... _Indexes>
        auto
        __call(_CVArg& __arg, tuple<_Args...>& __tuple,
               const _Index_tuple<_Indexes...>&) const volatile
        -> decltype(__arg(declval<_Args>()...))
        {
          return __arg(std::get<_Indexes>(std::move(__tuple))...);
        }
    };

首先,需要说明下,std::bind()的实现依赖于std::tuple(),将对应的参数放置于tuple中,最终调用会是__arg(std::get<_Indexes>(std::move(__tuple))...)这种方式。

由于函数模板不能偏特化,所以引入了模板类,也就是上面的class _Mu。该类模板用于转换绑定参数,在需要的时候进行替换或者调用。其有三个参数:

  • • _Arg是一个绑定参数的类型

  • • _IsBindExp指示它是否是bind表达式

  • • _IsPlaceholder指示它是否是一个占位符

如果结合本次的示例,那么_Arg的类型是Index::Update,_IsBindExp为true,而这跟上面的特化template<typename _Arg> class _Mu<_Arg, true, false>正好相对应。

_Mu有一个成员函数operator()(...),其内部调用__call()函数,而__call()函数内部,则会执行__arg(std::get<_Indexes>(std::move(__tuple))...),如果结合文中的Index示例,则这块相当于执行了Status(value)调用。(ps:此处所说的std::bind()是Index示例中嵌套的那个std::bind()操作)。

其实,截止到此处,错误原因已经定位出来了,这就是因为最外层的std::bind()参数中,其有一个参数T(此时T的类型为std::bind(&Index::status, this, std::placeholders::_1)),因为满足std::is_bind_expression ::value == true 这个条件,所以在最外层的std::bind()中,直接对最里层的std::bind()进行调用,而最里层的std::bind()所绑定的status()的返回类型是std::string,而外层std::bind()所绑定的Update成员函数需要的参数是std::string和std::function<std::string(const std::string &)>,因为参数类型不匹配,所以导致了编译错误。

解决

方案一

既然前面分析中,已经将错误原因说的很明白了(类型不匹配),因此,我们可以将Update()函数重新定义:

void Update(const std::string &value, std::function<std::string(const std::string &)> callback) {
   // do sth
}

编译通过!

方案二

既然编译器强调了类型不匹配,那么尝试将内层的std::bind()进行类型转换:

update_ = std::bind(&Index::Update, this, std::placeholders::_1, static_cast<std::function<std::string(const std::string &)>>(std::bind(&Index::status, this, std::placeholders::_1)));

编译通过!

方案三

在前面的两个方案中,方案一通过修改Update()函数的参数(将之前的第二个参数从std::function()修改为std::string),第二个方案则通过类型转换,即将第二个std::bind()的类型强制转换成Update()函数需要的类型,在本小节,将探讨一种更为通用的方式。

在方案二中,使用static_cast<>进行类型转换的方式,来解决编译报错问题,不妨以此为突破点,只有在std::is_bind_expression<T>::value == TRUE的时候,才需要此类转换,因此借助SFINAE特性进行实现,如下:

template<typename T>
class Wrapper : public T {
 public:
    Wrapper(const T& t) : T(t) {}
    Wrapper(T&& t) : T(std::move(t)) {}
 };

template<typename T, typename U = typename std::decay<T>::type >
typename std::enable_if< !std::is_bind_expression< U >::value, T&& >::type Transfer(T&& t) {
    return std::forward<T>(t);
}

template<typename T, typename U = typename std::decay<T>::type >
typename std::enable_if< std::is_bind_expression< U >::value, Wrapper< U > >::type Transfer(T&& t) {
    return Wrapper<U>(std::forward<T>(t));
}

相应的,对std::bind()那行也进行修改,代码如下:

update_ = std::bind(&Index::Update, this, std::placeholders::_1, Transfer(std::bind(&Index::status, this, std::placeholders::_1)));

再次进行编译,成功😁。

群里的问题

好了,接着回到群里的那个问题。

为了分析该问题,私下跟提问的同学进行了友好交流,才发现他某个函数是重载的,而该重载函数的参数为参数个数和类型不同的std::function(),下面是简化后的代码:

#include <functional>
#include <iostream>
#include <string>
using Handler = std::function<void(int, const std::string &)>;
using SeriesHandler = std::function<void(int, const std::string &, bool)>;

void reg(int n, const std::string &str) {
  std::cout << "n = " << n << ", str = " << str << std::endl;
}

void fun(const std::string &route, const Handler &handler) { 
  handler(1, "2"); 
}

void fun(const std::string &route, const SeriesHandler &handler) {
  
}

int main() {
  fun("/abc", std::bind(reg, std::placeholders::_1, std::placeholders::_2));
  return 0;
}

编译器报错如下:

test.cc:41:75: 错误:调用重载的‘fun(const char [5], std::_Bind_helper<false, void (&)(int, const std::__cxx11::basic_string<char>&), const std::_Placeholder<1>&, const std::_Placeholder<2>&>::type)’有歧义
   fun("test", std::bind(reg, std::placeholders::_1, std::placeholders::_2));
                                                                           ^
tt.cc:32:6: 附注:candidate: void fun(const string&, const Handler&)
 void fun(const std::string &route, const Handler &handler) {
      ^
tt.cc:36:6: 附注:candidate: void fun(const string&, const SHandler&)
 void fun(const std::string &route, const SHandler &handler) {
      ^

好了,先看下cppreference对这个问题的回答:

If some of the arguments that are supplied in the call to g() are not matched by any placeholders stored in g, the unused arguments are evaluated and discarded.

也就是说传给g()函数的参数在必要的时候,可以被丢弃,举例如下:

void fun() {
}
auto b = std::bind(fun);
b(1, 2, 3); // 成功

再看一个例子:

#include <functional>

void f() {
}

int main() {
  std::function<void(int)> a = std::bind(f);
  std::function<void()> b = std::bind(f);

  a(1);
  b();
  return 0;
}

综上两个例子,做个总结,代码如下:

void f() {}
void f(int a) {}

auto a = std::bind(f)
auto b = std::bind(f, std::placeholders::_1)

在上面两个bind()中,第一个支持初始化类型(即a的类型)为std::function<void(arg...)>,其中arg的参数个数为0到n(sizeof...(arg) >= 0);而第二个bind()其支持的初始化类型(即b的类型)为std::function<void(arg...)>,其中arg的参数个数为1到n(sizeof...(arg) >= 1)。

那么可以推测出:

auto c = std::bind(reg, std::placeholders::_1, std::placeholders::_2);

c支持的参数个数>=2,在编译器经过测试,编译正确~~

那么回到群里的问题,在main()函数中:

fun("/abc", std::bind(reg, std::placeholders::_1, std::placeholders::_2));

其有一个参数std::bind()(是不是跟前面的代码类似😁),这个std::bind()匹配的std::function()的参数个数>=2,即std::bind()返回的类型支持的参数个数>=2,而fun()有两个重载函数,其第二个参数其中一个为2个参数的std::function(),另外一个为3个参数的std::function(),再结合上面的内容,main()函数中的fun()调用显然都匹配两个重载的fun()函数,这是,编译器不知道使用哪个,所以干脆报错。

好了,既然知道原因了,那就需要有解决办法,一般有如下几种:

  • • 使用lambda替代std::bind()

  • • 静态类型转换,即上一节中的static_cast

    ,转换成需要的类型

今天的文章就到这,我们下期见!

你好,我是雨乐,从业十二年有余,历经过传统行业网络研发、互联网推荐引擎研发,目前在广告行业从业8年。本公众号专注于架构、技术、线上bug分析等干货,欢迎关注

 

点赞收藏
分类:标签:
高性能架构探索

公众号《高性能架构探索》

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
4
3