13164 字
66 分钟
C++ 模板笔记(六):标准库模板工具(ref、invoke、decay、void_t、declval 等)
2022-12-14

std::is_const#

is_const<T>::value

用于判断T类型是否是被const修饰的类型。

  • 若 T 为引用类型则 is_const<T>::value 始终为 false 。检查可能为引用的类型的常性的正确方式是移除引用: is_const<typename remove_reference<T>::type> 。
    • 原因是引用的const 限定符仅仅意味着不能通过引用参数修改值。 它仍然可以被其他方法修改。比如直接修改变量
int main()
{
std::cout << std::boolalpha;
std::cout << "is_const:" << std::endl;
std::cout << "int: " << std::is_const<int>::value << std::endl;
std::cout << "const int: " << std::is_const<const int>::value << std::endl;
std::cout << "const int&: " << std::is_const<const int&>::value << std::endl;
std::cout << "const int* " << std::is_const<const int*>::value << std::endl;
std::cout << "int* const:" << std::is_const<int* const>::value << std::endl;
return 0;
}
/*
is_const:
int: false
const int: true
const int&: false
const int*: false 这里的false是因为指针本身非const。也就是顶层非const。
int* const:true 这里的true是因为指针本身是const。也就是顶层为const
*/

模拟简单实现#

也是简单的匹配。

template<typename _Tp>
struct my_is_const{
void func(){
cout <<"not const" << endl;
}
};
template<typename _Tp>
struct my_is_const<_Tp const>{
void func(){
cout <<"const" << endl;
}
};
my_is_const<int> a;
my_is_const<const int>b;
a.func(); //not const
b.func(); // const

std::ref, std::cref, std::reference_warpper#

之前一直想整理一下这几个。这次一起探讨一下

在很多时候比如使用bind和thread这种一定会拷贝参数的标准库组件时,我们如果想要按照引用传递,就必须使用ref或cref包装一下。

  • 一句话总结。ref和cref都是reference_warpper的包装器。

  • cref只是变成了带const的ref。所以下面不分析cref了。

  • reference_warpper内部包含一个指向了原始对象类型的指针。其实他就是一个包含了一个指针的包装器对象。

    • 它有用户定义转换函数。
    • 他还有一个get()函数用于返回其原始对象的引用,通过这个方式可以像引用一样,内部修改的值可以传递到外面。
  • 其实应该说ref和cref是reference_warpper的辅助函数。

  • std::reference_warpper其实是一种强别名. 参考资料来自这里 看下CPO的故事(3)的后半部分

ref 源代码#

template<typename _Tp>
inline reference_wrapper<_Tp> ref(_Tp& __t) noexcept
{ return reference_wrapper<_Tp>(__t); }
template<typename _Tp>
void ref(const _Tp&&) = delete; //禁止接受右值
template<typename _Tp>
inline reference_wrapper<_Tp> ref(reference_wrapper<_Tp> __t) noexcept
{ return ref(__t.get()); }
  • 我们可以一眼看出,ref就是一个返回reference_wrapper的函数
  • 因为禁止接受了形参为右值的参数。所以ref不能包装右值。
std::ref(5);//不可以
  • ref的传入参数可以是一个普通的引用,也可以是另外一个reference_wrapper对象

reference_wrapper 源代码#

template<typename _Tp>
class reference_wrapper : public _Reference_wrapper_base<typename remove_cv<_Tp>::type> //这里去掉了CV。
{
_Tp* _M_data; //内部就是包含一个指向原始对象的指针。
public:
typedef _Tp type;
reference_wrapper(_Tp& __indata) noexcept
:_M_data(std::__addressof(__indata))
{
}
reference_wrapper(_Tp&&) = delete;
reference_wrapper(const reference_wrapper<_Tp>& __inref) noexcept
:_M_data(__inref._M_data)
{
}
reference_wrapper& operator=(const reference_wrapper<_Tp>& __inref) noexcept
{
_M_data = __inref._M_data;
return *this;
}
operator _Tp&() const noexcept //用户定义转换函数。使用了get
{ return this->get(); }
_Tp& get() const noexcept //get用于返回原始对象的引用。起到引用的效果。
{ return *_M_data; }
template<typename... _Args>
typename result_of<_Tp&(_Args&&...)>::type
operator()(_Args&&... __args) const
{
return __invoke(get(), std::forward<_Args>(__args)...);
}
};
  • 该类继承于_Reference_wrapper_base_

  • 有一个类成员_M_data,类型为所引用类型的指针。这个指针指向了原始对象。

  • 第一个构造函数通过调用std::__addressof函数,获得了指向引用参数的指针,并赋值给了_M_data(这也是为什么不支持右值引用的原因,因为取不到对应的地址)

    • 至于这里为什么要使用std::__addressof,是因为如果有些类重载了operator & 的话可能我们不能取到真实地址。使用这个函数不管是否重载了取地址操作符,都可以取到原始地址。具体细节不分析了。
  • 拷贝构造函数和赋值函数就只是简单地将_M_data的值进行传递而已,没有什么特殊操作。都是常规

  • 其余方法就是为了让reference_wrapper展现出和普通的引用一样的效果而进行的运算符重载。

    • 比如重载了operator ()可以让我们在reference_wrapper对象储存了一个可调用对象的时候,直接使用()调用其储存的可调用对象
    void this_func(){
    cout <<"called" << endl;
    }
    int main() {
    auto func_ptr = this_func;
    auto ref3 = ref(func_ptr); //ref3是一个reference_wrapper储存了一个函数指针
    ref3();//调用。
    }
    • 比如有get()可以获取一个原始对象的引用。
    • 用户定义转换函数使得它支持了向原始类型的隐式类型转换。

举一个非常不太对劲的例子

template <typename T>
void another_func(T a){
a.get() = 2000;
}
int main() {
int a = 1;
another_func(ref(a));
cout << a << endl; //修改为2000。
}
//或者是
void changeval(int& a){
a = 20000;
}
template <typename T>
void func(T a){
changeval(a); //因为有用户定义转换函数。这里的T本来是reference_wrapper类型的,但是可以隐式转换为原始类型。比如这里就是int&
}
int main() {
int a = 1;
func(ref(a));
cout << a << endl; //修改为20000
}

注意了。在这些情况下,reference_wrapper对象本身依旧是按值传递的。

https://blog.csdn.net/weixin_43798887/article/details/116562336

std::remove_reference#

  • 若类型 T 为引用类型,则提供成员 typedef type ,其为 T 所引用的类型。否则 type 为 T 。

这个的实现比较简单。

template<class T>
struct remove_reference{
typedef T type;
};
template<class T>
struct remove_reference<T&>{
typedef T type;
};
template<class T>
struct remove_reference<T&&>{
typedef T type;
};

当T是&或者&&的时候,自然都会有每一个匹配的版本。都会让其T本身的类型成为其类型。也就是脱去引用。

std::remove_cv/remove_volatile/remove_const#

  • 第一个移除最顶层CV
  • 第二个移除最顶层V
  • 第三个移除最顶层C

如果忘了什么叫顶层,去杂记2搜“顶层”

int a = 200;
const int b = 200;
const int* ptr = &a;
int* const c_ptr = &a;
const int* const cc_ptr = &a;
using anothertype = remove_cv_t<const int>;
cout <<std::boolalpha;
cout << is_same_v<decltype(a), remove_cv_t<decltype(b)>> << endl;
cout << is_same_v<const int*, remove_cv_t<decltype(ptr)>> << endl;
cout << is_same_v<int*, remove_cv_t<decltype(c_ptr)>> << endl;
cout << is_same_v<const int*, remove_cv_t<decltype(cc_ptr)>> << endl;
/*
true
true
true
true
*/
  • 第一个,移除了b的最顶层const,变成了int
  • 第二个,移除了ptr的最顶层const,但是ptr本身并非const,而是ptr指向的对象是const。所以此时移除无效。依旧是const int*
  • 第三个,移除了c_ptr的最顶层const。这个c_ptr本身是const,所以移除后变成了int*
  • 第四个,移除了cc_ptr的最顶层const。这个cc_ptr不仅本身是const,指向的对象也是const。由于仅移除最顶层,所以变成const int*

简单实现#

依旧是非常直观的偏特化匹配

template< class T > struct remove_cv { typedef T type; };
template< class T > struct remove_cv<const T> { typedef T type; };
template< class T > struct remove_cv<volatile T> { typedef T type; };
template< class T > struct remove_cv<const volatile T> { typedef T type; };
template< class T > struct remove_const { typedef T type; };
template< class T > struct remove_const<const T> { typedef T type; };
template< class T > struct remove_volatile { typedef T type; };
template< class T > struct remove_volatile<volatile T> { typedef T type; };

std::address_of#

template< class T >
T* addressof(T& arg) noexcept; //1
template <class T>
const T* addressof(const T&&) = delete; //2
  • 获得对象或函数 arg 的实际地址,即使存在 operator& 的重载 (1)
  • 右值重载被删除,以避免取 const 右值的地址。(2)
  • 返回值是指向 arg 的指针。

我们在reference_weapper源代码分析一节中提到了address_of。它的核心作用是取得对象或函数的真实地址。因为有时候可能你使用的operator&是被重载过的。比如:

class Test
{
public:
int* operator&() //重载address-of也就是取址运算符
{
return &b;
}
int* a_addr()
{
return &a; //注意,这个&用的不是重载的。因为a是int类型,这句等于a.operator&(),也就是int自己的取址而不是这里Test的取址
}
int* b_addr()
{
return &b; //注意,这个&用的不是重载的。因为b是int类型,这句等于b.operator&(),也就是int自己的取址而不是这里Test的取址
}
private:
int a;
int b;
};
int main(int argc, char* argv[])
{
Test t;
std::cout << "&t.a:" << t.a_addr() << std::endl; //0x7ffe80a9fc10
std::cout << "&t.b:" << t.b_addr() << std::endl; //0x7ffe80a9fc14
std::cout << "&t:" << &t << std::endl; //0x7ffe80a9fc14 注意,这里变成了b的地址。
std::cout << "addressof(t):" << std::addressof(t) << std::endl;//0x7ffe80a9fc10
}

我们看到,直接对t.a和t.b取地址是正常的。但是使用了t的重载的operator&,由于我们让它取了t.b的地址,所以返回的一定是t.b的地址。但是我们用了addressof就可以避免这个问题。

实现分析#

template<class T>
typename std::enable_if<std::is_object<T>::value, T*>::type addressof(T& arg) noexcept{
return reinterpret_cast<T*>(//第四步
&const_cast<char&>(//第二步和第三步。第三步单独指的取址&
reinterpret_cast<const volatile char&>(arg))); //第一步
}
template<class T>
typename std::enable_if<!std::is_object<T>::value, T*>::type addressof(T& arg) noexcept{
return &arg;
}
  • 第一步,将arg由类型T&强制转换为const volatile char&,这样做有两个作用:
    • 一是防止后面(第三步)使用&操作符获取地址时触发原类型(即T)的重载操作(operator&),就像上面那个例子那样。
    • 二是reinterpret_cast不可以去掉原类型的const或volatile,但是可以合法的在原类型的基础上增加const或volatile, 所以,如果T原来就带有const或volatile的话, 通过reinterpret_cast去掉是不允许的, 因此需要加上const volatile来避免编译器报错, 也就是此时不用再管T是否本来就带有const或volatile属性了。
      • 可能会问为啥不直接const_cast?char和T是无关类型,只能用reinterpret_cast
      • 可能会问为啥要cast到引用&?因为如果cast到变量就会生成新变量,这样地址不就变了么?
  • 第二步,将前面转换得到的结果强制转换为char&类型,**此时如果转换成其它类型有可能会触发强制地址对齐的操作,这样的话真实地址就有可能会被改变了,最终造成程序错误。**需要注意的是这个转换过程使用的是const_cast,可以顺便将前面留下的const和volatile属性给去掉了。
  • 第三步使用&符号将前面的结果的地址给取出来(此时已经不会触发重载了)
  • 最后一步使用reinterpret_cast将前面获取到的地址转换回T*类型,并且此时也会保留T的const或volatile属性(如果有的话)

https://stackoverflow.com/questions/16195032/implementation-of-addressof

https://blog.csdn.net/weixin_43798887/article/details/117966866

重载取址运算符用在哪?代理类的时候会用到#

参考more effective C++ 条款30 P.224

一般当一个对象内涵一个代理类,通过某些操作返回一个代理类对象的指针的时候,由于不同类型指针之间不存在天然的转换关系,所以这个时候需要重载一个取址运算符来自然化语义。让对一个代理类取址的时候取到的是一个我们真正想取地址的类型的地址。

但是非常不建议使用,原因参考Modern C++ design 7.6 P.170

std::invoke [C++17]#

https://youtu.be/zt7ThwVfap0 非常好的视频。

std::invoke 是什么#

invoke是C++17新出的功能。

我们在上面11.1提到了可调用对象。比如有函数指针,成员函数指针,std::function等. invoke让我们有一个更统一的方式来调用任何的可调用对象。

std::invoke 和 std::function 和 其他可调用对象的关系#

template< class F, class... Args >
std::invoke_result_t<F, Args...>
invoke( F&& f, Args&&... args ) noexcept;

…

以参数 f 调用可调用 (Callable) 对象。如同以 INVOKE(std::forward<F>(f), std::forward<Args>(args)...) 。此重载只有在 std::is_invocable_v<F, Args...> 为 true 时才参与重载决议。

…

标准文档一向不说人话。

具体就是:std::function可以包装其他的可调用对象。而std::invoke可以调用如std::function这样的可调用对象。

  • 注意invoke是有返回值的。

https://stackoverflow.com/questions/39398740/what-is-the-difference-between-stdinvoke-and-stdfunction

std::invoke 怎么用#

std::invoke在模板类中调用成员函数指针的时候尤为有用。

看看语法

  • 调用函数对象

    • invoke(func, args...);
      //等同于
      func(args...);
  • 调用成员函数指针(pointer to member function)

    • 如果可调用对象是一个指向成员函数的指针,它会将 args...中的第一个参数当作this对象(不是指针)。Args...中其余的参数则被当做常规参数传递给可调用对象。

    • 具体原始调用方式在杂记2

    • invoke(pmf, obj, rest...);//通过对象调用
      //等同于
      (obj.*pmf)(rest...);
      invoke(pmf, ptr, rest...);//通过指针调用
      //等同于
      (ptr->*pmf)(rest...);
    • 为了化简使用,有一个特殊例子。如果可调用对象是一个指向成员函数的指针,它会将 args...中的第一个参数当作 this 对象(不是指针)。Args...中其余的参数则被当做常规参数传递给可调用对象。否则,所有的参数都被直接传递给可调用对象。

  • 调用成员变量指针 (pointer to member data)

    • 具体原始调用方式在杂记2

    • invoke(pmd, obj);//通过对象调用
      //等同于
      obj.*pmd;
      invoke(ptr, obj);//通过指针调用
      //等同于
      ptr->*obj;
  • 一点儿具体例子

//函数对象
template<typename _funcObj, typename... Args>
void test_func(_funcObj ptr, Args&&... args){
// ptr(args...);
std::invoke(ptr, forward<Args>(args)...); //注意加了forward
}
//成员函数指针,对象调用。
template<typename _Obj, typename _Ptr, typename... Args>
void test_memberfunc( _Ptr ptr, _Obj& obj, Args&&... args){
//(obj.*ptr)(args...);
std::invoke(ptr, obj, forward<Args>(args)...); //注意加了forward
}
//成员函数指针,指针调用。
template<typename _Obj, typename _Ptr, typename... Args>
void test_memberfunc_ptr(_Ptr ptr, _Obj* obj, Args&&... args){
//((*obj).*ptr)(args...);
std::invoke(ptr, obj, forward<Args>(args)...); //注意加了forward
}
myobj obj1(1);
function<void(int)> task = bind(&myobj::add, &obj1, placeholders::_1);
std::invoke(task, 5); //调用function包装的成员函数
test_memberfunc(&myobj::getval, obj1); //使用成员函数指针,对象调用
test_memberfunc(&myobj::add, obj1, 8);
test_memberfunc_ptr(&myobj::add, &obj1, 8); //使用成员函数指针,指针调用
test_func(&myobj::add, &obj1, 8); //使用提到的特殊例子,使用成员函数指针。会被特殊处理。
  • 注意直接调用模板函数必须指明模板参数:
template<typename T>
T func(T obj){
cout << "called" << endl;
return obj;
}
int main() {
auto f = std::invoke(func<int>, 100); //必须显式指明<int>
return 0;
}

这里由于是直接调用一个模板函数。invoke没办法帮我们推导这个函数应该用哪个模板参数实例化。我们上面用到的都是包了一层的,也就是invoke的时候,invoke内的参数有确定类型。比如上面的ptr在传入invoke的时候,类型已经确定为_Ptr。而当前这里还需要推导,但是无法推导。所以必须显式指明。

std::invoke 源码和实现#

目前有点看不懂

https://www.cnblogs.com/windpiaoxue/p/10009172.html

https://codechina.gitcode.host/programmer/cpp-update/3-C++17-features-spotlights-2.html#stdinvoke

std::invoke 什么时候用#

  • 非泛型编程没必要用,除非痛恨成员函数指针语法。
  • 泛型编程可以使用,但是不要指定其invoke的模板参数。把它留给invoke决定

实现invoke功能是非常困难的事情。

std::invoke 的陷阱,到底要不要使用forward#

书中提到了,invoke中传递的参数不应该使用完美转发, 因为第一次调用可能会偷取相关参数的值(使用移动构造),导致随后的调用中出现错误。

但是如果一个对象是不可拷贝仅可移动的呢?比如unique_ptr,这时候就必须要使用forward了。来自这里

void p_2(unique_ptr<int> myptr){
cout <<"p_2 called"<< endl;
cout << *myptr << endl;
}
unique_ptr<int> ptrs(new int(10));
test_func(p_2, move(ptrs)); //外面记得也要移动

当然了,问题出现在这里。如果你无意间传递了一个右值进去:

void p_3(myobj x){
cout <<"p_3 called"<< endl;
cout << *x.val << endl;
}
test_func(p_3, myobj(234));
template<typename _funcObj, typename Args> //为了方便测试,这里不用变参列表了。
void test_func(_funcObj ptr, Args&& args){
// ptr(args...);
std::invoke(ptr, forward<Args>(args)); //如果这里使用了完美转发
cout << *args.val << endl; //args的右值特性被forward保留,触发移动构造。这里会段错误。
}

所以说,具体要不要用forward需要根据情况使用。

std::invoke的返回值#

从函数签名中,我们看到了invoke是有返回值的。

template<typename T>
T func(T obj){
cout << "called" << endl;
return obj;
}
int main() {
decltype(auto) f = std::invoke(func<int>, 100); //f 类型是 int。刚才说过此处必须显示指明这个函数的模板参数。
cout << f << endl; //输出100
return 0;
}
  • 之所以要用decltype(auto)而非auto的原因是因为auto会导致类型退化。
    • 我们提到过,auto一定会推导出返回类型为对象类型并且应用退化。
    • 而auto&或auto&&一定会推导为引用类型。
    • 而decltype(auto)则可以根据具体返回值的类型进行推导。
  • 那么为什么不用auto&&呢?因为auto&&会延长临时对象的生命周期
    • 返回值是临时对象,纯右值,使用右值(此处顺带万能引用)引用会延长其生命周期。

我们看到了这玩意的返回值类型是:

template< class F, class... Args >
std::invoke_result_t<F, Args...>

那我们就要研究一下这个是个什么东西了。

std::result_of[C++11~17]/invoke_result[C++17]#

  • 首先强调一点。对于如F(args...)这样的东西,由于2.6部分提到的原因,我们把它看成是是以 Args... 为参数而以 F 为返回类型的函数类型。这一点非常重要,因为后面会遇到这个错误,同时它也是为什么我们在C++20后弃用result_of。
  • 第二点:你不能问编译器一个函数对象的返回类型是什么。因为其返回类型很可能和使用的参数有关。所以你需要问:使用这一堆参数调用这个函数对象的时候的返回类型是什么。
template< class >
class result_of; // 不定义
template< class F, class... ArgTypes >
class result_of<F(ArgTypes...)>;
  • 作用是在编译时推导INVOKE 表达式的返回类型。

    • 这个INVOKE表达式是广义的可调用类型。
  • 成员type是以参数 ArgTypes... 调用可调用 (Callable) 类型 F 的返回类型。仅若 F 能以参数ArgTypes...在不求值语境中调用才得到定义。

  • 再次强调,F和ArgTypes都必须是类型!!!!

我们看几个例子:

struct F{
int operator ()(int i) {
return i;
}
};
int f(int i){
return i;
}
int main(){
cout << std::boolalpha;
cout << is_same_v<std::result_of_t<F(int)>, int><< endl; //可以
cout << is_same_v<std::result_of_t<f(int)>, int><< endl; //不可以
return 0;
}
  • 为什么F(int)可以但是f(int)不可以?这里非常重要
  • F是类型。仿函数是对象。它是类类型。所以F是个类型。而f不是类型,f是函数。
    • 上面其实不严谨。具体一点:F(int)是一个函数类型。这个类型指的是一个函数它接受一个int参数,返回F类型对象。很明显这不是F类型,同时没有F到int的转换,为什么这样做可以?原因在2.6写了。也就是它会把F和int类型拆开。最后会变成declval<F>()(declval<int>())这个样子。但是这样做有个非常大的问题,也是为何result_of在C++20被弃用。
cout << is_same_v<F, F><< endl; //可以。F是类型。我们可以有F obj
cout << is_same_v<f, f><< endl; //不可以。f不是类型。f是函数
  • 所以为了获取到函数类型,有几种方案。第一种是使用using定义一个函数引用别名或函数指针别名即可。
using f_ref = int(&)(int); //函数引用
using f_ptr = int(*)(int); //函数指针
using fdef = int(int); //函数类型
cout << is_same_v<std::result_of_t<f_ref(int)>, int><< endl; // OK
cout << is_same_v<std::result_of_t<f_ptr(int)>, int><< endl; // OK
int(*pf)(int) = f;
cout << is_same_v<std::result_of_t<pf(int)>, int><< endl; // 错误
cout << is_same_v<std::result_of_t<fdef(int)>, int><< endl; // 错误

为什么第三种错误?因为using f_ptr = int(*)(int); 是定义类型别名。而int(*pf)(int) = f;是声明一个变量。说到底,f_ptr依旧是类型,而pf变成了函数指针本身,而函数指针是实体,并不是类型。pf的类型是int(*)(int)。

为什么第四种错误?这里有个非常关键的语法层面的问题。 对于如F(args...)这样的东西,由于2.6部分提到的原因,我们把它看成是是以 Args... 为参数而以 F 为返回类型的函数类型。再次回到函数头,我们能看到class result_of<F(ArgTypes...)>;这个声明。

所以我们现在result_of_t<fdef(int)>进去之后会变成啥呢?会变成一个以fdef为返回类型,以int为参数的函数类型。那么fdef是啥呢?fdef是一个函数类型。C++函数不是一等公民,不能传入和返回函数,但是可以传入和返回函数的指针或引用。

所以这个不能跑的原因是,我们无法定义一个返回函数的函数。而为什么用函数指针或引用的时候能跑?因为我们可以定义一个返回函数指针或函数引用的函数。

  • 第二种是使用decltype。我们这里拆解来看
using f_1 = decltype(f); //decltype函数名
using f_2 = decltype((f)); //decltype函数表达式加括号了。相当于上面的f_ref
cout << is_same_v<std::result_of_t<f_1(int)>, int><< endl; //不可以
cout << is_same_v<std::result_of_t<f_2(int)>, int><< endl; //可以

为什么第一个f_1不可以?我们说过,F必须是一个类型而不能是函数。而且此时f_1是函数类型。所以f_1(int)又变成了一个返回函数的函数。所以还是有问题。第二个可以是因为函数加括号后decltype的是表达式的type,所以返回引用类型。所以可以。

  • 第三种是直接使用函数模板推导让其退化为函数指针。
template<typename T>
void func(T f){
//T会被推导为int (*)(int)。等同于上面的f_ptr
std::puts(__PRETTY_FUNCTION__);
cout <<boolalpha;
cout <<is_same<typename result_of<T(int)>::type, int>::value <<endl;
}
int sample(int){
return 0;
}
int main(){
func(sample);
}

总结解决方案:#

  • 使用using直接定义函数指针或函数引用。

  • 使用decltype的时候,把函数做为表达式传入。

  • 或者是在使用的时候加一个&(依旧相当于变为函数引用),比如

cout << is_same_v<std::result_of_t<f_1&(int)>, int><< endl; //可以
//其实就相当于变为函数引用
cout << is_same_v<f_1&,int(&)(int)><< endl; //相当于函数引用类型
cout << is_same_v<f_1,int(int)><< endl; //相当于函数类型
  • 直接使用函数模板推导让其退化为函数指针。

进入invoke_result的世界#

我们已经提过了result_of由于设计上的问题导致的诡异行为。所以invoke_result的定义是这样的

template< class F, class... ArgTypes>
class invoke_result;
  • F 不能是函数类型或数组类型(但能是到它们的引用);
  • 若任何 Args 拥有“ T 的数组”类型或函数类型 T ,则它被自动调整为 T* ;
  • F 或任何 Args... 都不能是抽象类类型;
  • 若任何 Args... 拥有顶层 cv 限定符,则舍弃之;
  • Args... 均不可为 void 。

最明显的两点区别是:

  • 首先,不会把模板参数合成为一个函数类型了。也就是不会产生F(args...)的问题。
  • 其次,明确限制F不能是函数类型。

所以使用的时候就不再是跟着括号了,而是逗号即可。

is_same<std::result_of_t<f_2(int)>, int>;// result_of_t
is_same<std::invoke_result_t<f_2, int>, int>; // invoke_result_t

其他资料如更细节的语法等:

https://stackoverflow.com/questions/54065009/using-stdinvoke-to-call-templated-function

https://blog.csdn.net/tcy23456/article/details/110583343

https://blog.csdn.net/hanxiaoyong_/article/details/120618869

例子#

比如下面我们的想法是对一个有T类型元素的vector容器vec,对其每一个元素调用可调用对象c,然后将返回值存入新vector,排序后并返回。

template<typename T, typename Callable>
auto transform_sort(const vector<T>& vec, Callable c){
vector<decay_t<result_of_t<Callable&(const T&)>>> ret; //查看对元素施加callable对象后,返回的值类型。然后创建这个类型的容器
for(const T& item: vec){ //对每一个元素
ret.push_back(invoke(c, item));//使用invoke施加callable,然后存入ret
}
sort(ret.begin(), ret.end());
return ret;
}
int my_callable(const string& s){
return s.size();
}
int main(){
vector<string> my_vec{"a","abcd","abc","ab"};
for(const auto& item: transform_sort(my_vec, my_callable)){
cout << item << endl;
}
/*
输出
1
2
3
4
*/
return 0;
}
  • result_of_t<Callable&(const T&)>>这里的细节是使用了Callable&而非Callable。也就是解决方案2。这样避免了我们上面提到的诡异行为。
  • 使用decay_t的原因是,可能callable的返回类型是引用或带有cv限定的。有这样元素的的容器是不合法的。
  • invoke的语法不多讲。第一个是可调用对象,第二个是其参数。
  • 最后的结果我们看到了,我们的callable目的是返回这个字符串的长度。所以整体目的是检查vector内每一个字符串的长度,然后存到vector中并返回。

建议#

建议不要使用result_of,除非必须场合。

std::decay#

之前总谈到它,现在不整理不行了。不然看不懂书了。

template< class T >
struct decay;
  • 对类型 T 应用左值到右值(lvalue-to-rvalue)、**数组到指针(array-to-pointer)和函数到指针(function-to-pointer)**的隐式转换,移除 cv 限定符,并定义结果类型为成员 typedef type 。正式而言:
    • 若 T 指名“ U 的数组”或“到 U 的数组的引用”类型,则成员 typedef type 为 U* 。
      • 强制给数组和数组引用类型退化为指针。
    • 否则,若 T 为函数类型 F 或到它的引用,则成员 typedef type 为std::add_pointer<F>::type 。
      • 强制给函数和函数引用类型退化为指针
    • 否则,成员typedef type为 std::remove_cv<std::remove_reference<T>::type>::type
      • 强制给普通成员移除引用和CV限定符。

看看原理:

template<typename _Tp>
class decay
{
typedef typename remove_reference<_Tp>::type __remove_type; //先去掉引用
public:
typedef typename __decay_selector<__remove_type>::__type type; //然后使用下面的 __decay_selector
};
template<typename _Up,
bool _IsArray = is_array<_Up>::value,
bool _IsFunction = is_function<_Up>::value>
struct __decay_selector; //主模板,判断是不是数组或函数类型。!!!注意这里is_function判断的是函数类型而非函数指针类型。
template<typename _Up>
struct __decay_selector<_Up, false, false> //如果不是数组也不是函数,则直接去掉CV即可。第一步已经去掉过引用了。
{ typedef typename remove_cv<_Up>::type __type; };
template<typename _Up>
struct __decay_selector<_Up, true, false> //如果是数组类型,则直接弄成*就行
{ typedef typename remove_extent<_Up>::type* __type; };
template<typename _Up>
struct __decay_selector<_Up, false, true> //如果是函数类型,
{ typedef typename add_pointer<_Up>::type __type; }; //使用add_pointer转换为指针类型。这里需要细说。后面会有介绍
  • 其实从上面我们也发现了。主要核心就是把三种情况分开讨论,使用三个偏特化的模板进行匹配。
  • 比较需要注意的一点是,针对数组类型退化的时候,CV特性会被保留。
  • 至于add_pointer,我们下面说。

https://blog.csdn.net/weixin_43798887/article/details/118311126

std::void_t [C++17]#

这部分需要结合6.6部分来看。

template <class... _Types>
using void_t = void;
  • 功能:能够配合我们检测到应用SFINAE(替换失败并不是一个错误)特性时出现的非法类型。换句话说,给进来的类型 _Types 必须是一个有效的类型,不能是一个非法类型。
    • 它可以判断一个类内是否有个特定的类型别名、成员函数或成员变量

这个东西看起来贼鸡肋,我们分析分析它的简单使用。假设现在我们的目的是判断一个类内是否有一个叫type的类型

判断是否存在成员类型#

struct A {
using mytype = int; //类A含有mytype类型
};
struct B{
using mytype = void; //类B也含有mytype类型
};
template<typename T1 , typename T2 = void > //主模板,第二个参数默认值是void,非常重要
struct has_type_member : std::false_type { //主模板继承false_type
};
template<typename T1> //针对第二个参数进行偏特化
struct has_type_member<T1, std::void_t<typename T1::mytype>>: std::true_type { //偏特化继承true_type,并且使用void_t进行类型判断。
};
int main(){
auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;
cout << f << endl;
cout << a << endl;
cout << b << endl;
/*
输出
0
1
1
*/
}
  • 我们解析一下标准例子。为什么是这个结果。
  • 首先,我们拥有类A和类B。他们都有一个mytype类型。
    • 其次,我们的判断类的主模板继承自false_type,第二个参数的默认值是void
    • 判断类的偏特化继承自true_type,针对第二个参数进行偏特化。
  • 我们在6.6中强调了:当模板参数有默认值的时候,它会被隐式填进去,而不是被忽略。它依旧存在。也就是我们要把默认值理解为:虽然我们不需要显式指定,但是它依旧存在。
auto f = has_type_member<float>::value; //扩展为has_type_member<float, void>::value;
auto a = has_type_member<A>::value; //扩展为has_type_member<A, void>::value;
auto b = has_type_member<B>::value; //扩展为has_type_member<B, void>::value;
  • 我们首先看f。我们使用<float>的时候会由于有默认参数被扩展为<float, void>。然后开始进行匹配。首先选用特化模板。T1会被替换为float。然后在第二个模板参数std::void_t<typename float::mytype>>进行匹配的时候,由于float没有mytype类型,不合法。此时会发生替换失败。SFINAE发生。所以这个模板会被丢弃。回退到主模板,应用到主模板的void。因为我们提供的是<float,void>。而主模板完美匹配。这个时候实例化出来的是继承自false_type的。所以是0

  • 然后我们看a。我们使用<A>的时候会由于有默认参数被扩展为<A, void>。然后开始进行匹配。首先选用特化模板。T1会被替换为A。然后在第二个模板参数std::void_t<typename A::mytype>>进行匹配的时候,发现A有type类型。合法。所以此时通过决议。我们看到了,此时void_t的推导通过,void_t就是void。此时整个std::void_t<typename A::mytype>>会变为void。

    • 关键的来了。我们提供的是<A,void> ,主模板可以匹配。同时特化版本也可以匹配,因为此时也被替换成了<A,void>。但是由于下面的特化版本更特化,我们知道全特化版本 > 特化版本 > 主模板。所以此时选择了偏特化版本,继承了true_type所以是1
  • b同a。

为什么必须要用void_t?#

我们看下面的例子:

我们假设类A和类B和上面的代码一样,只修改我们的判断类。

template<typename T1 , typename T2 = void > //主模板,第二个参数默认值是void,非常重要
struct has_type_member : std::false_type { //主模板继承false_type
};
template<typename T1> //针对第二个参数进行偏特化
struct has_type_member<T1, typename T1::mytype>: std::true_type { //偏特化继承true_type,注意这里不用void_t,直接判断其type
};
int main(){
auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;
cout << f << endl;
cout << a << endl;
cout << b << endl;
/*
输出
0
0 //这个变了!!
1
*/
}
  • 第一个f原因同上。我们不多解释。
  • 我们来看a。A会被扩展为<A,void>。然后开始匹配。首先选用特化模板。我们发现A类有mytype,所以正确推导。**但是,A::mytype是int类型,导致这时候模板实例化出来的会变成has_type_member<A, int>。**但是我们模板实参给的是<A,void>。此时发现不匹配。于是该版本被丢弃。然后发现主模板可以匹配。所以此时会选用主模板而非特化模板。主模板继承自false_type。此时自然是0。
  • b的原因和上面一样,但是由于B::mytype是void,实例化出来的<B,void> 和 实参<B,void>匹配。所以选择了偏特化版本。此时自然是1。

如果像6.6那样更换了主模板的默认值,会发生什么?#

template<typename T1 , typename T2 = int > //主模板,此时第二个参数默认值是int
struct has_type_member : std::false_type { //主模板继承false_type
};
template<typename T1> //针对第二个参数进行偏特化
struct has_type_member<T1, std::void_t<typename T1::mytype>>: std::true_type { //偏特化继承true_type,并且使用void_t进行类型判断。
};
int main(){
auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;
cout << f << endl;
cout << a << endl;
cout << b << endl;
/*
输出
0
0 //这个变了
0 //这个也变了
*/
}
  • f理由同上不解释。
  • 我们来看a。A会被扩展为<A,int>。然后开始匹配。首先选用特化模板。我们发现A类有mytype,所以正确推导。**这时候模板实例化出来的会变成has_type_member<A, void>。但是,我们模板实参给的是<A,int>。此时发现不匹配。**于是该版本被丢弃。然后发现主模板可以匹配。所以此时会选用主模板而非特化模板。主模板继承自false_type。此时自然是0。
  • 对于b,B会被扩展为<B, int>。然后开始匹配。首先选用特化模板。我们发现B类有mytype,所以正确推导。**这时候模板实例化出来的会变成has_type_member<B, void>。但是,我们模板实参给的是<B,int>。此时发现不匹配。**于是该版本被丢弃。然后发现主模板可以匹配。所以此时会选用主模板而非特化模板。主模板继承自false_type。此时自然是0。

搭配decltype判断是否存在成员变量#

上面一节是判断是否存在类型别名。这一节看一下判断是否存在成员变量。

struct A {
int val = 234; //类A含有val变量
};
struct B{
float val2 = 345; //类B不含有val变量,但是含有val2变量
};
template<typename T1 , typename T2 = void > //主模板,第二个参数默认值是void,非常重要
struct has_type_member : std::false_type { //主模板继承false_type
};
template<typename T1> //针对第二个参数进行偏特化
struct has_type_member<T1, std::void_t<decltype(T1::val)>>: std::true_type { //偏特化继承true_type,并且使用void_t且搭配decltype进行类型判断。
};
int main(){
auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;
cout << f << endl;
cout << a << endl;
cout << b << endl;
/*
输出
0
1
0
*/
}
  • 核心原理就是我们在这里需要搭配decltype来对某一个变量的名字进行判断是否合法,但不推导。如果有这个变量则类型合法。如果没有则非法。
  • 额外注意!!!在模板内的decltype表达式是不推导语境。我们需要等到特化被生成后,编译器才会开始进入推导语境。也就是目前仅仅对T进行替换但不推导表达式的实际类型。
  • 我们直接看a。A被扩展为<A,void>。然后选择偏特化版本开始匹配。此时进入模板参数推导阶段,所有的T1被替换为A。但是注意,我们说了decltype此处不推导。只是替换。所以此时特化为:
template<> //针对第二个参数进行偏特化
struct has_type_member<A, std::void_t<decltype(A::val)>>: std::true_type {
};
  • 此时模板参数已被推导完毕,进入下一阶段。此时decltype是可评估语境。由于类A有成员变量val,所以此时decltype(A::val)是合法的。此时它会变成int。我们说过任何经过void_t判断合法的东西都会使整个void_t参数变为void。所以此时特化成
has_type_member <A,void>
  • 和上面一样,虽然主模板和特化版本都匹配,但还是匹配最特殊的,这里就是偏特化版本,自然就是true。

  • b和a的原理几乎一致。唯一区别是进行偏特化后,B没有val成员变量。所以decltype非法。所以整个void_t也非法。触发SFINAE特性移出重载集合,转为匹配主模板。

判断是否存在成员函数#

我们下面讲了declval。

  • 这个是第一种方式,通过<成员函数的返回值>是否合法来判断。限制是函数不能有参数。
struct A {
void funcA();
};
struct B{
void funcB();
};
template<typename T1 , typename T2 = void > //主模板,第二个参数默认值是void,非常重要
struct has_type_member:std::false_type { //主模板继承false_type
};
template<typename T1> //针对第二个参数进行偏特化
struct has_type_member<T1, std::void_t<decltype(std::declval<T1>().funcA())>>: std::true_type { //使用decltype和declval进行成员函数判断。
};
int main(){
auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;
cout << f << endl;
cout << a << endl;
cout << b << endl;
/*
输出
0
1
0
*/
}
  • 具体不细说了。核心还是判断类型是否存在也就是decltype内的表达式是否合法。注意两点
  • 第一点declval是函数模板。所以别忘了调用一下。
  • 第二点是,decltpe此处我们的目的是通过函数返回值来判断是否存在函数。由于declval是用来判断函数返回值的,所以必须要“调用”一下。所以这里必须得让他成为表达式。原因在这里和这里
  • **有一个问题,如果这个成员函数有参数咋办?这里就不能这么用了。因为如果签名需要参数,但是我们没给,就非法。**所以这时候就需要通过成员函数地址来判断了。

https://blog.csdn.net/baidu_41388533/article/details/109700163

https://blog.csdn.net/ding_yingzi/article/details/79983042

https://stackoverflow.com/questions/27687389/how-does-void-t-work

  • 另一种方式,通过<取成员(函数)地址>是否合法来判断。
    • 这个方式同样可以检测是否有成员变量。
struct A {
A() = delete;
void myfunc(int s){
//类A含有myfunc
}
};
struct B{
void myfuncB(){
//类B不含有myfunc,但是含有myfuncB
}
};
template<typename T1 , typename T2 = void > //主模板,第二个参数默认值是void,非常重要
struct has_type_member:std::false_type { //主模板继承false_type
};
template<typename T1>
struct has_type_member<T1, std::void_t<decltype(&T1::myfunc)>>: std::true_type { //通过成员函数地址判断。OK
};
int main(){
auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;
cout << f << endl;
cout << a << endl;
cout << b << endl;
/*
输出
0
1
0
*/
}

有没有其他写法?#

来自这里

先说结论,以下三种写法是同一个效果:

void_t<decltype(expression)>; //1
decltype(void(expression)); //2
decltype(expression, void()); //3
  • 第一种就是经典写法。
  • 第二种是表达式计算完毕后void转型。所以如果表达式合法,最后的类型结果也是void
  • 第三种是利用逗号表达式。第一个表达式计算完毕后,如果合法,就丢弃第一个结果继续计算void()。最后的类型结果也是 void。

所以如果换成上面的例子:

template<typename T1>
struct has_type_member<T1, std::void_t<decltype(&T1::myfunc)>>: std::true_type { //1
};
template<typename T1>
struct has_type_member<T1, decltype(void(&T1::myfunc))>: std::true_type { //2
};
template<typename T1>
struct has_type_member<T1, decltype(&T1::myfunc, void())>: std::true_type { //3
};

std::declval#

declval基本用于推导成员函数的返回类型。千万注意

template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;

有没有发现这个函数的声明和我们上面的testfunc很像?都是只要声明不要定义,骗过编译器。

**函数模板 std::declval()可以被用作某一类型的对象的引用的占位符。**该函数模板没有定义,因此不能被调用(也不会创建对象)。因此它只能被用作不会被计算的操作数(比如 decltype 和 sizeof)。也因此,在不创建对象的情况下,依然可以假设有相应类型的可用对象。

书里的话非常抽象。我们看看标准文档怎么说的:

将任意类型 T 转换成引用类型,使得在 decltype 说明符的操作数中不必经过构造函数就能使用成员函数。

通常在模板中使用 std::declval,模板接受的模板实参通常可能无构造函数,但有同一成员函数,均返回所需类型。

注意,std::declval 只能用于不求值语境,且不要求有定义;求值包含此函数的表达式是错误。正式的说法是 ODR 使用此函数的程序非良构。

这里就非常明确了。declval可以被当做占位符的意思是。declval<T>就相当于创建了一个T类型的对象。但是由于实际并不创建,所以我可以通过不创建实际对象来使用某一个成员函数。所以,使用declval创建虚拟占位符意味着不调用构造。所以在某些时刻会节约其调用成本。但是它只能返回类型,所以它只能用在不求值语境中,比如decltype。(我们已经反复强调decltype并不真正求值。我们在杂记4中详细介绍了decltype的不求值语境。)

struct A {
A() = delete;
int foo();
};
int main() {
decltype(A().foo()) foo_no; // 不OK因为构造函数无法访问
decltype(std::declval<A>().foo()) foo = 1; // OK
decltype(A&&.foo())foo = 1; //理论上和上面的相等,但是不能编译
}

我们下面那一行在理论上等于上面那一行。因为decltype不求值,所以这里可以编译通过。因为我们的目的是查看foo这个函数到底返回什么类型。这个函数也不会被调用。所以你看我们根本没有给foo提供定义。他们只关心类型。也就是说,我们告诉编译器:请假设我们现在有这个对象,那么请看看使用这个对象和这些参数来调用这个成员函数的时候,返回值是什么类型。

  • 注意一下,declval是一个函数,所以记得要调用一下

再强调一次,不要花时间思考为什么两行语义相等但是第二行不通过。只要编译器认为它是值就可以了。不要仔细思考decltype是如何被实现的。

关于为啥declval要添加右值引用#

必须添加引用是因为可以使用不完整类型。比如数组那种int[10]直接按照值返回是不行的。使用右值引用是因为可以正确施加引用折叠以避免改变类型。

对于可引用的类型,其返回类型总是相关类型的右值引用,这能够使declval 适用于那些不能够正常从函数返回的类型,比如抽象类的类型(包含纯虚函数的类型)或者数组类型。

因此当被用作表达式时,从类型 T 到 T&&的转换对declval<T>()的行为是没有影响的:其结果都是右值(如果 T 是对象类型的话。上面decltype笔记2.3),对于右值引用,其结果之所以不会变是因为存在引用折叠。

  • 所以要注意,如果希望返回类型不是一个引用,记得使用decay

https://stdrc.cc/post/2020/09/12/std-declval/#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%94%E5%9B%9E%E5%BC%95%E7%94%A8

https://stackoverflow.com/questions/25707441/why-does-stddeclval-add-a-reference

https://stackoverflow.com/questions/28532781/how-does-stddeclvalt-work

  • 另外一点要注意:在 noexcept 异常规则中提到,一个表达式不会因为使用了declval而被认成是会抛出异常的。当 declval 被用在 noexcept 运算符上下文中时,这一特性会很有帮助(参见第 19.7.2节)。

如何理解 declval#

在实际使用中,我们只需要理解为declval<T>()是告诉编译器假装在这里我们有一个T类型的对象。我们忽略掉它是引用的事实,忽略掉它的实现(添加右值引用)。这看起来怪异也无所谓。因为它只能用在不求值语境。

  • 在不求值语境中,任何函数调用都只需要有声明即可,并不必须提供定义。所以不求值语境中重要的是类型,而不是实例。在整个不求值的语境中,都是一堆类型在进行推导。压根没有值在这里计算。所以实例无所谓。

所以我们说过:必须添加引用是因为可以使用不完整类型。比如数组那种int[10]直接按照值返回是不行的。使用右值引用是因为可以正确施加引用折叠以避免改变类型。所以说,如果不考虑这些奇怪的场景,只考虑简单,最基本的例子,你甚至可以直接写成这样:

template<class T>
T declval() noexcept;

所以我们可以理解为declval也是一种类型萃取。尤其是在不求值语境中帮我们进行推理。

同时,很多需要SFINAE的时候都需要让表达式内嵌套一个表达式。什么意思?比如这个is_polymophic的实现:

template<class T, class> //主模板
struct IP_impl : false_type {
};
template<class T> //偏特化
struct IP_impl<T, decltype(dynamic_cast<void*>(declval<remove_cv_t<T>*>()))> : true_type {
};
template<class T>
struct is_polymorphic : IP_impl<T, void*> { //外层。注意这里第二个参数必须是void*
//原因是因为如果是void,就算是多态也会匹配到主模板。
//因为我们提供的第二个参数是void,但是如果满足多态,第二个参数会被偏特化模板推导为void*,这时候就不满足了
//然后就会fallback到主模板。
};
  • 这里我主要想强调一下为什么必须declval。我们先不要管remove_cv_t。首先dynamic_cast是可以转换成void*的。但是想使用dynamic_cast则必须要虚函数表。所以只要有虚函数表,就是多态类。回到declval。如果我们这里直接使用dynamic_cast<void*>(T*)可以吗?不可以。因为dynamic_cast的括号里需要表达式。T*是类型,不是表达式。如果我们换成(T*)nullptr可以,但是某些编译器会warning。所以在这里,declval不仅帮助我们获得了一个虚拟对象,更是满足了一些要求提供表达式的场合。
    • 在理解过程中,我们忽略掉declval<T*>()会变成T*&&类型的这件事。语义上虽如此,但是不影响程序执行。
  • 说回来,我们上面提到了,甚至可以写成返回T的declval。在这个例子中就满足。
template<typename T>
T fuckval() noexcept; //随便写一个
template<typename T, typename>
struct ip_jmpl:false_type{
};
template<typename T>
struct ip_jmpl<T, decltype(dynamic_cast<void*>(fuckval<T*>()))>:true_type{ //换成fuckval完全没问题。
void f(){
std::puts(__PRETTY_FUNCTION__);
}
};
template<typename T>
struct ip_po : ip_jmpl<T, void*> {
};

来自这里

https://stackoverflow.com/questions/28532781/how-does-stddeclvalt-work

为啥有的declval用引用类型,有的不用?#

比如这段代码:

else if constexpr (_Reversed_subrange<_Ty> == 1) {
using _It = decltype(_STD declval<_Rng&>().begin().base());
return {_St::_Subrange_sized,
noexcept(subrange<_It, _It, subrange_kind::sized>{
_STD declval<_Rng&>().end().base(), _STD declval<_Rng&>().begin().base(),
_STD declval<_Rng&>().size()})};
} else if constexpr (_Can_reverse<_Rng>) {
return {_St::_Reverse, noexcept(reverse_view{_STD declval<_Rng>()})};
}

答案是declval会应用正常引用折叠。会在某些情况让你得到意想不到的结果。

struct A {};
struct B {};
struct C {
A f() && { return A{}; }
B f() & { return B{}; }
};
int main() {
static_assert(std::is_same_v<A, decltype(std::declval<C>().f())>);
static_assert(std::is_same_v<B, decltype(std::declval<C&>().f())>);
}

来自这里

std::add_pointer#

template< class T >
struct add_pointer;
  • 若 T 为引用类型,则提供成员 typedef type ,其为指向被引用类型的指针。
    • 也就是直接换成指针
  • 否则,若 T 指名对象类型、无 cv 或引用限定的函数类型或(可有 cv 限定的) void 类型,则提供成员 typedef type ,其为类型 T*
    • 注意这里:如果是对象类型,无CV或引用限定的函数类型或(可有 cv 限定的) void 类型。
    • 也就是给对象或函数换成指针类型
  • 否则(若 T 为 cv 或引用限定的函数类型),提供成员 typedef type ,其为类型 T 。
    • 注意这里,如果是有CV或引用限定的函数类型。
    • 也就是这时候它还是T,并不添加指针。

通过add_pointer来找到另一种SFIANE的方法(void_t的替代品。也就是函数模板的SFINAE)#

在add_pointer的模拟实现中,有这样的代码:

namespace detail {
template <class T>
struct type_identity { using type = T; }; // 或使用 std::type_identity (C++20 起)
template <class T>
auto try_add_pointer(int) -> type_identity<typename std::remove_reference<T>::type*>;
template <class T>
auto try_add_pointer(...) -> type_identity<T>;
} // namespace detail
template <class T>
struct add_pointer : decltype(detail::try_add_pointer<T>(0)) {};
  • 为什么这里会有一个try_add_pointer(int) 和 try_add_pointer(...) ?
    • 首先,这是一个函数。它的返回类型是type_identity<typename std::remove_reference<T>::type*>这个东西。
    • 我们替换T然后先找特化版本。如果这个实参的传入让后面的尾置返回类型合法,推导成功就自然而然选择这个特化版本。
    • 如果这个实参的传入导致后面的尾置返回类型非法,推导失败,会触发SFINAE。这时候这个特化版本就会被丢弃。由于...能够接受任何类型的任何数量的参数,(就像我们上面8.4里面提到的一样)所以其他任何导致特化版本非法的使用都会被匹配到这个通用版本。
    • 所以说,这里到底是int还是float不重要。传入1还是0也不重要。

举一个不太好的但是能解释原理的例子:

struct A {
typedef int mytype;
};
struct B{
typedef int mytypea;
};
template <class T>
auto try_add_pointer(T) -> typename T::mytypea{
cout <<"no" << endl;
}
template <class T>
auto try_add_pointer(...) -> typename T::mytype{
cout <<".." << endl;
}
int main(){
A obj;
B obj1;
try_add_pointer<A>(obj1); //匹配到..
try_add_pointer(obj1); //错误
try_add_pointer<B>(obj); //错误
}
  • 首先注意一点,可变参数...是最低优先级。

  • 为何第一个匹配到..而第二个错误?

  • 第一个能成功的原因是,首先我们指定了函数模板是A,所以T换成A。进入特化版本,A没有mytypea。此时甚至都不必考虑obj1是B类型和T不匹配 的问题,特化直接被丢弃。注意,这里不考虑隐式转换。看1.2部分。

    • 下一步,注意看,我们的变参包并没有指明类型,也就是变参包的类型是真正的任意。此时没办法进行推导。因为尾置返回类型的T压根没有一个参照。入参没有类型,T从何来?
    • 所以我们这里有显式指明模板参数类型。这里我们显式指明T为A。然后发现A有mytype,所以匹配成功。选择泛化版本。
  • 所以第二个错误的原因是T无法推导。变参包并没有指明类型,也就是变参包的类型是真正的任意。此时没办法进行推导。因为尾置返回类型的T压根没有一个参照。入参没有类型,T从何来?

    • 如果想修复这个错误,那就变成auto try_add_pointer(T...) -> typename T::mytype{, 加一个T让形参参与推导即可
  • 第三个错误的原因是指明类型为B,T换成B,进入特化版本。B虽然有mytypea,但是和指明的入参类型不符合。所以特化版本被丢弃。

    • 此时看...版本,我们指明T为B,但是B没有mytypea,所以这个泛化版本也无法匹配。最终错误。

所以正常应该这么写

struct A {
typedef int mytype;
};
struct B{
typedef int mytypea;
};
template <class T>
auto try_add_pointer(T) -> typename T::mytypea{
cout <<"no" << endl;
}
template <class T>
auto try_add_pointer(T...) -> typename T::mytype{ //让...参与类型推导
cout <<".." << endl;
}
int main(){
A obj;
B obj1;
try_add_pointer(obj); //OK 不指明模板参数类型。输出..
try_add_pointer(obj1); //OK 不指明模板参数类型。输出no
}
  • 这里都可以了是因为A类型虽然没有mytypea,但是可以匹配到泛化版本。因为有mytype
  • obj1是B类型,这时候因为没有像上面那样子指明类型导致冲突,所以T自然被推导为B。同时有mytypea。自然可以。

https://stackoverflow.com/questions/57506069/a-question-regarding-the-implementation-of-stdadd-pointer

std::conditional#

template<bool B, class T, class F>
struct conditional { using type = T; };
template<class T, class F>
struct conditional<false, T, F> { using type = F; };
  • 提供成员 typedef type ,若 B 在编译时为 true 则定义为 T ,或若 B 为 false 则定义为 F 。

实现也很简单。没什么多说的。

  • 注意,如果T或F之一是ill-formed,则整个conditional表达式也会是ill-formed ---来自这里

CRTP#

先简单介绍,之后慢慢看。

  • CRTP的一个最明显的特点就是把派生类作为基类的模板参数

std::type_identity (C++20)#

这个是C++20提案的一个小工具. 我们看下它的实现:

template <typename T>
struct type_identity {
using type = T;
};

这个东西看起来没什么用. 但是它主要作用是防止某一个参数类型参与类型推导. 核心原理是使用了不推导语境的一条规则:

用有限定标识指定的类型的 嵌套名说明符(作用域解析运算符 :: 左侧的所有内容)

我们看一个例子

template <typename T>
T add2(T a, T b) {
return a + b;
}
auto sum2 = add2(0.5, 1); // 不可以

因为参数类型被第一个参数推导为double 所以第二个参数类型也必须为double 因为模板参数类型推导不允许隐式类型转换, 所以会推导失败.

如果我们使用type_identity

template <typename T>
T add(T a, typename type_identity<T>::type b) {
return a + b;
}
auto sum = add(0.5, 1); // T is "double" OK

这样就可以了. 只不过第二个参数可以被隐式转换为了double

第一个参数的类型决定了T是什么类型,其他所有人都必须遵守。我们可以使用它来指定第二个参数是不可推断的

另一个在任务队列中常见的情况:

void enqueue(std::function<void(void)> const& work) {
cout << __PRETTY_FUNCTION__ << endl;
// dosomething
}
template <typename... Args>
void enqueue(std::function<void(Args...)> const& work, Args... args) {
enqueue([=] { work(args...); });
}

这里其实就是一个任务包装器. 把带函数和其参数打包到一个std::function当做一个任务.

如果现在我这样执行, 会有错误:

enqueue([](int v) { std::cout << v; }, 42);

原因就是参与了类型推导. std::function和lambda是两种不同类型, 尽管lambda可以转换为std::function. 这个时候就可以使用我们的type_identity 因为这个类型推导失败并不能代表什么, 同时我们很希望忽略掉它.

template <typename... Args>
void enqueue(typename type_identity<std::function<void(Args...)>>::type const& work, Args... args) {
cout << __PRETTY_FUNCTION__ << endl;
enqueue([=] { work(args...); });
}

参考:

blog来自这里 .可查看文章内的bonus chatter

点击这里查看完整原始proposal

点击这里查看cppreference. 在[不推导语境]一节中的1)

C++ 模板笔记(六):标准库模板工具(ref、invoke、decay、void_t、declval 等)
https://sterben-01.github.io/posts/模板-6/
作者
Sterben_01
发布于
2022-12-14
许可协议
CC BY-NC-SA 4.0