std::invoke_result的實現詳解

目錄

前言

本篇博文將詳細介紹一下libstdc++std::invoke_result的實現過程,由於個人水平不足,可能最終的實現過程略有誤差,還請各位指正。

invoke_result

std::invoke_result是C++17標準庫裡面的一個介面,它可以返回任何可調用的返回值,比如函數,函數對象,對象方法等。它的介面為[1]

template< class F, class... ArgTypes>
class invoke_result;

在C++17之前,std::invoke_result有一個類似功能的模板,名字為std::result_of,他們之間的差別只有在介面上有略微的差別,

template< class >
class result_of; // not defined

template< class F, class... ArgTypes >
class result_of<F(ArgTypes...)>;

std::result_of於C++17標記為deprecated,並在C++20中被移除。

標準庫中的invoke_result

我們首先看看一下標準庫中std::invoke_result的實現,了解一下其大致的結構。

graph TD;
invoke_result[std::invoke_result];
result_of[std::result_of];
__invoke_result[std::__invoke_result];
invoke_result –> __invoke_result;
result_of –> __invoke_result;

__result_of_impl[std::__result_of_impl];
__invoke_result –> __result_of_impl;

__result_of_memobj[std::__result_of_memobj];
__result_of_memfun[std::__result_of_memfun];
__result_of_other_impl[std::__result_of_other_impl];

__result_of_impl –> __result_of_memobj;
__result_of_impl –> __result_of_memfun;
__result_of_impl –> __result_of_other_impl;

__S_test[“__S_test()”];
__result_of_other_impl –> __S_test;

__result_of_memobj_ref[std::__result_of_memobj_ref];
__result_of_memobj_deref[std::__result_of_memobj_deref];
__result_of_memobj –> __result_of_memobj_ref;
__result_of_memobj –> __result_of_memobj_deref;

__result_of_memfun_ref[std::__result_of_memfun_deref];
__result_of_memfun_deref[std::__result_of_memfun_deref];
__result_of_memfun –> __result_of_memfun_ref;
__result_of_memfun –> __result_of_memfun_deref;

__result_of_memfun_ref_impl[std::__result_of_memfun_ref_impl];
__result_of_memfun_deref_impl[std::__result_of_memfun_deref_impl];
__result_of_memobj_ref_impl[std::__result_of_memobj_ref_impl];
__result_of_memobj_deref_impl[std::__result_of_memobj_deref_impl];

__result_of_memfun_ref –> __result_of_memfun_ref_impl;
__result_of_memfun_deref –> __result_of_memfun_deref_impl;
__result_of_memobj_ref –> __result_of_memobj_ref_impl;
__result_of_memobj_deref –> __result_of_memobj_deref_impl;

__result_of_memfun_ref_impl –> __S_test;
__result_of_memfun_deref_impl –> __S_test;
__result_of_memobj_ref_impl –> __S_test;
__result_of_memobj_deref_impl –> __S_test;

可以看到,整個實現有兩層判斷(有兩個判斷是類似的,可以看作是一層),其中第一個層位於std::__result_of_impl,用來判斷不同的調用方法,大致分為三種:

  1. 對象方法,比如以下程式碼中的struct Ainvoke_mem_fun,調用方法為obj.fun(args...)
struct A {
  int invoke_mem_fun();
};
  1. 對象成員,比如以下程式碼中的struct B中的invoke_mem_obj,調用方法為obj.fun(其實就是一個簡單的引用成員)。
struct B {
  int invoke_mem_obj;
};
  1. 其他,比如以下程式碼中的invoke_other,其調用方法為fun(args...),這一部分包括了函數對象和普通函數。
int invoke_other();

那麼std::__result_of_impl是如何區分出了這三種不同的調用方式呢?

libstdc++中實現了兩個類型判斷,is_member_object_pointeris_member_function_pointer。通過這兩個判斷不同的輸出,來區分三種不同的調用方式。

這兩個函數的結構差不多,首先會對判斷輸入的類型是否為對象的成員,判斷方法為進行類似下面程式碼的模式匹配

template<typename>
struct __mem_and_obj_type: public std::__failure_type{};

template<typename _Tp, typename _Cp>
struct __mem_and_obj_type<_Tp _Cp::*> {
 typedef _Tp mem_type;
 typedef _Cp obj_type;
};

這一部分程式碼是從我實現的部分截取出來的,其中最主要的部分就是_Tp _Cp::*,他分開成員類型和對象類型,如果能分開則為對象的成員。

第二步則是判斷上一步分離得到的成員類型是否為函數類型,如果是函數類型,那麼其為對象方法,否則為對象成員。

由此,通過那兩個類型的判斷,可以有三種不同的結果,true,falsefalse,truefalse,false,不同的結果對應著不同的調用方法。看到這裡,肯定會有人思考,有沒有可能結果都是true呢?

首先從libstdc++實現上這是不可能的,因為在實現第二步的時候,is_member_object_pointer幾乎是直接對is_member_function_pointer取反的(實現上取反)。所以,不可能同時出現兩個都為true的情況。

而實際中,卻的確有可能出現這種情況(我也不知道是不是我那裡弄錯了,先寫下來吧)

struct STest {
  double operator() (int, double) {
    std::cout << "Test" << std::endl;
    return 0;
  }
};

struct CTest {
  struct STest s;
  int s_m(int, double){};
};

這種情況下,如果我們想獲得CTest::s的返回值,是有一個很奇怪的情況的,我們是把它當作函數呢,還是一個成員呢?如果從調用的情況來看,CTest::sCTest::s_m一樣,都是obj.fun(args...),但是實際中,std::invoke_result會將它視為對象成員。所以如果我們使用std::invoke_result<decltype(&CTest::s), CTest, int, double>::type是會報錯的,正確的方法是只能使用std::invoke_result<decltype(&CTest::s), CTest>::type。由於沒有去查看文獻,所以不清楚這一部分是bug還是feature或者是UB,有時間再詳細考究一下吧。

好了,目前已經詳細說完了std::invoke_result的第一層判斷,接下來我們來看看第二層判斷。

第二層判斷有兩個部分,我們只看其中的一個部分,因為實際上這兩個部分都是一樣的。

這一層的判斷位於對象成員和對象方法部分。即區分std::__result_of_memfun_derefstd::__result_of_memfun_ref。這兩個的不同之處在於,前者的對象為指針。區分的方法也很簡單粗暴,直接判斷是否參數裡面的對象類型與上文中使用_Tp _Cp::*獲得的對象類型_Cp是否一致或者為繼承關係,如果為是,則使用std::__result_of_memfun_ref否則為std::__result_of_memfun_deref

通過上面兩層的判斷,已經成功的將不同的調用方法進行了分類,接下來就是使用decltype來獲取返回值了。這一部分就很簡單了,由上面的幾次分類,分為了五種不同的調用(加上不同的對象)。

  1. __invoke_memfun_ref
(std::declval<_Tp1>().*std::declval<_Fp>())(std::declval<_Args>()...)
  1. __invoke_memfun_deref
((*std::declval<_Tp1>()).*std::declval<_Fp>())(std::declval<_Args>()...)

3. `__invoke_memobj_ref`

```C++
std::declval<_Tp1>().*std::declval<_Fp>()
  1. __invoke_memobj_deref
(*std::declval<_Tp1>()).*std::declval<_Fp>()
  1. __invoke_other
std::declval<_Fn>()(std::declval<_Args>()...)

但是比較好奇的是,程式沒有直接通過上面的方法獲得,而是構造了一個return_type _S_test(int),然後獲取的。除了返回的類型外,std::invoke_result還保存了調用的類型,即之前提到的那五種,這一部分應該是為了實現std::invoke而保存的。

我的實現

為了學習std::invoke_result,我也實現了一個類似的ink::invoke_result。層次結構類似,不過將_S_test()刪去了,改成了直接的實現。

github gist: ink_invoke_result.cpp

後記

閱讀標準庫裡面的實現的確可以學到很多東西,即使以後不會寫類似的程式碼,但是在使用的時候也會對C++有更加清晰的理解。同時也發現了C++目前的一個缺陷,在實現這種介面的時候,免不了要進行多層包裝,而每一層的包裝,標準庫都是直接將其暴露在std命名空間中,這導致了在使用程式碼提示的時候極大的降低了提示的效率和美觀(一大堆的以下劃線開頭的item真的非常非常非常難看,而且頭大)。

部落格原文: //www.cnblogs.com/ink19/p/cpp_invoke_result.html


  1. std::result_of, std::invoke_result – cppreference.com ↩︎

Tags: