诡异!std::bind in std::bind 编译失败

你好,我是雨乐!

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

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

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

从问题代码说起

好了,先说说之前的问题,在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::functionconst 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<falsevoid (Index::*)(const std::__cxx11::basic_string<char>&, std::functionbasic_string<char>(const std::__cxx11::basic_string<char>&)>), Index*, const std::_Placeholder<1>&, std::_Bindchar, 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::_Bindvoid (Index::*)(const std::__cxx11::basic_string<char>&, std::functionbasic_string<char>(const std::__cxx11::basic_string<char>&)>)>(Index*, std::_Placeholder<1>, std::_Bindbasic_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

  • • std::is_placeholder

显然,我们属于第二种情况,即__std::is_bind_expression

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

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

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

为了分析这个原因,研究了下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, truefalse>
    {
    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 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这个条件,所以在最外层的std::bind()中,直接对最里层的std::bind()进行调用,而最里层的std::bind()所绑定的status()的返回类型是std::string,而外层std::bind()所绑定的Update成员函数需要的参数是std::string和std::function,因为参数类型不匹配,所以导致了编译错误。

解决

方案一

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

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

编译通过!

方案二

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

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

编译通过!

方案三

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

在方案二中,使用static_cast<>进行类型转换的方式,来解决编译报错问题,不妨以此为突破点,只有在std::is_bind_expression::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::type >
typename std::enable_if< !std::is_bind_expression< U >::value, T&& >::type Transfer(T&& t) {
    return std::forward(t);
}

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

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

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

再次进行编译,成功?。

群里的问题

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

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

#include 
#include 
#include 
using Handler = std::function<void(intconst std::string &)>;
using SeriesHandler = std::function<void(intconst 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&), 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(123); // 成功

再看一个例子:

#include 

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,其中arg的参数个数为0到n(sizeof...(arg) >= 0);而第二个bind()其支持的初始化类型(即b的类型)为std::function,其中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