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

当时看了报错,简单的以为跟之前遇到的原因一样,随即提出了解决方案,怎奈,短短几分钟,就被无情打脸,啪啪啪😭。为了我那仅存的一点点自尊,赶紧看下原因,顺便把之前的问题也回顾下。
好了,言归正传(此处应为严肃脸),在后面的内容中,将从源码角度分析下之前问题的原因,然后再分析下群里这个问题。
好了,先说说之前的问题,在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
• std::is_placeholder
显然,我们属于第二种情况,即__std::is_bind_expression
根据cppreference对第二种情况的描述:
• If the stored argument
argis of typeTfor 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, 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这个条件,所以在最外层的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 ing, 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
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70024923/viewspace-2938824/ ,如需转载,请注明出处,否则将追究法律责任。