詭異!std::bind in std::bind 編譯失敗

張哥說技術發表於2023-03-09

你好,我是雨樂!

上週的某個時候,正在愉快的摸魚,突然群裡丟擲來一個問題,說是編譯失敗,截圖如下:

詭異!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::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<falsevoid (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 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<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(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<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(123); // 成功

再看一個例子:

#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部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2938824/,如需轉載,請註明出處,否則將追究法律責任。

相關文章