发表于

源码剖析之C++11新特性

最近出去面试了一下,感觉对自己的触动挺大的,就是很多细节的问题自己并没有研究很深,源码阅读不够。有个问题是C++11有什么新功能?我当时有点脑袋空空,后来回来我想自己确实应该总结一下。

C++11新特性

当有人问C++11有什么新东西的时候,我总结起来有以下部分

  • 原子操作库
  • 多线程库
  • 正则表达式库
  • std::function
  • std::array
  • std::initializer_list
  • std::tuple
  • 萃取器(type traits)
  • 移动语义和值分类(value category)

其中最后的两块,我觉得是最为经典和基础的,这两款不理解是没法理解模板的,也就更无从谈起标准库了。

值分类、引用

值分类是个很有意思的东西;

  • 左值和右值是按照等号两边来区分的
  • 左值引用和右值引用都是左值,语法上理解引用就是T* const
  • 有名字的右值引用是左值;右值引用还能是右值,std::move就是生成右值引用的右值。
  • 常左值引用既可以接受左值,又可以接受右值。
  • 左值引用和右值引用作为右值按引用传递参数时会退化为引用的对象本身,std::decay_t在编译期实现了类似功能
  • 右值引用是有可能延长引用对象生存期的,可能放在堆上
  • 模板中有引用折叠概念,且大量使用

退化可以这么理解,因为按引用传递需要引用对象,因此T&或T&&引用需要退化为类型T本身。

#include <memory>
#include <cstdio>

struct A
{
  char x;
  int y;
  void print()
  {
    printf("x,y is %c,%d\n",x,y);
  }
};

void f(A&& a)
{
  a.printf();
}

int main(){
  A ax = {'a',1};
  A& bx = ax;
  A&& cx = std::move(ax);
  A&& dx = std::move(A{'b',2});
  //A&& ex = bx; //bx是左值引用A&的左值,这里退化为ax还是左值,不能绑定到右值引用
  //A&& fx = dx; //dx是右值引用A&&的左值,这里退化为ax还是左值,不能绑定到右值引用
  A& gx = bx;
  A&& hx = A{'c',3};

  //打印
  ax.print();
  bx.print();
  cx.print();
  dx.print();
  //ex.print();
  //fx.print();
  gx.print();
  hx.print();

  //无论传递A&还是A&&的左值,编译时错误信息都是A类型不能绑定到A&&,所以是退化过的
  //f(bx);
  //f(cx);

  return 0;
}

指针和引用的区别:看了很多人的说法,还是很模糊的。比较好的说法是,从C++语言层面看引用是T* const,但是从机器码层面两者是没有区别的,且也很难界定,因为机器码只操作地址。

为什么要发明右值引用呢!

  • 实现通用引用(Universal Reference),无论什么T类型,T&&都是引用,从而实现编译期计算(结合《深入理解C++对象模型》的计算偏移量功能),编译期是没有堆栈的,无法有个能够取地址的对象。
  • 要帮助实现移动语义,也就是移动构造/赋值。std::move可以生成右值引用的右值。

std::ref和std::reference_wrapper

std::ref生成右值std::reference_wrapper,然后再隐式转换为左值。

template<class _Tp>
class reference_wrapper:public __weak_result_type<_Tp>
{
public:
  using type = _Tp;
  type* __f_;

  operator type& () const{return *__f_;};//隐式转换为左值,最重要功能!

  //本身也是可调用对象
  template <class... _ArgTypes>
  typename __invoke_of<type&, _ArgTypes...>::type
  operator()(_ArgTypes&&... __args) const
  {
    return __invoke(get(),_VSTD::forward<_ArgTypes...>(__args)...);
  }

  …
};

template<class _Tp>
reference_wrapper<_Tp>
ref(_Tp& __t)
{
  return reference_wrapper<_Tp>(__t);
}

template<class _Tp>
reference_wrapper<_Tp>
ref(reference_wrapper<_Tp>  __t)
{
  return ref(__t.get());
}

这个函数的目的是解决函数式编程中的一些问题。在函数对象的生成中,如果你给左值引用,那么函数对象对应参数生成的是T*类型,而不是引用类型。所以左值引用是要包装器的。

template<class _Fp, class ..._BoundArgs>
inline _LIBCPP_INLINE_VISIBILITY
__bind<_Fp, _BoundArgs...>
bind(_Fp&& __f, _BoundArgs&&... __bound_args)
{
    typedef __bind<_Fp, _BoundArgs...> type;
    return type(_VSTD::forward<_Fp>(__f), _VSTD::forward<_BoundArgs>(__bound_args)...);
}

template<class _Rp, class _Fp, class ..._BoundArgs>
inline _LIBCPP_INLINE_VISIBILITY
__bind_r<_Rp, _Fp, _BoundArgs...>
bind(_Fp&& __f, _BoundArgs&&... __bound_args)
{
    typedef __bind_r<_Rp, _Fp, _BoundArgs...> type;
    return type(_VSTD::forward<_Fp>(__f), _VSTD::forward<_BoundArgs>(__bound_args)...);
}

可以看到std::bind是完美转发给了type这个函数,返回的类型是__bind或者__bind_r。简单起见我们看看__bind的源码。

template<class _Fp, class ...BoundArgs>
class __bind : public __weak_result_type<typename decay_t<_Fp>>
{
protected:
    typedef typename decay_t<_Fp> _Fd;
    typedef tupble<typename decay_t<BoundArgs>...> __bound_args_;
private:
    _Fd __f_;
    _Td __bound_args_;
public:
    ...
    operator()(_Args&& ...__args)
    {
        return _VSTD::__apply_functor(__f_, __bound_args_, __indices(),
                                   tuple<_Args&&...>(_VSTD::forward<_Args>(__args)...));
    }
};

...
__apply_functor(...)
{
  return _VSTD::__invoke(…);
}

丛中可以发现生成的__bind类型的函数对象是将_Fp和BoundArgs参数包都退化处理的,所以参数都是按照拷贝赋值运算符进行的。

可以发现std::bind的实现需要利用

  • std::forward
  • std::decay_t
  • std::tuple
  • std::invoke

来实现,比较复杂。至于为什么std::bind传递参数的时候不能直接用&t,而必须用std::ref,是因为形参是&t则推导出类型是T*而不是T&,那么函数对象的类型和函数指针是不匹配的,会发生无模板匹配的编译期错误。

其实std::thread的实现也是的,先要退化推导出来的类型,然后用std::tuple将所有的参数类型(包括返回类型)都装起来,然后通过std::invoke进行调用。

std::forward和std::move

std::forward和std::move很有意思,两者的函数实现都是强制类型转换

// forward
template <class _Tp>
inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
_Tp&&
forward(typename remove_reference<_Tp>::type& __t) _NOEXCEPT
{
    return static_cast<_Tp&&>(__t);
}

template <class _Tp>
inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
_Tp&&
forward(typename remove_reference<_Tp>::type&& __t) _NOEXCEPT
{
    static_assert(!is_lvalue_reference<_Tp>::value,
                  "can not forward an rvalue as an lvalue");
    return static_cast<_Tp&&>(__t);
}

// move
template <class _Tp>
inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT
{
    typedef _LIBCPP_NODEBUG_TYPE typename remove_reference<_Tp>::type _Up;
    return static_cast<_Up&&>(__t);
}

但是两者还是有区别的

  • std::forward,左值引用到左值引用,右值引用到右值引用
  • std::move,通用引用到右值引用

decltype/std::declval

这个函数只有定义没有实现,因为运行期这个函数没用,只能用于编译期。

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

其实就是返回了右值引用,本质上是编译期T类型已经编译成二进制,因此可以不在运行时构造该对象,而知道其成员数据的类型,或者成员函数的返回类型。通常是配合decltype来一起用的。

这里有个很有意思的问题,为什么不用std::add_lvalue_reference_t来实现呢?用T类型本身肯定是不行的,因为编译期不会构造对象。那么T&和T&&之间的抉择,为什么选择了万能引用?因为模板参数T可能本身就是引用!所以万能引用保证了推导出来的必定是引用!

只要是引用就可以不用真的构造对象,而知道该类型的相关信息。非常的经典!

移动语义

移动的源代码非常简单

template <class _Tp>
inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT
{
    typedef _LIBCPP_NODEBUG_TYPE typename remove_reference<_Tp>::type _Up;
    return static_cast<_Up&&>(__t);
}

这里的是MacOS下的标准库里的。利用模板的右值引用形参和引用折叠规则,推导出_Tp类型,然后用萃取器remove_reference获得无引用类型,然后强制类型转换为右值引用。本质就是std::move可以把左值引用和左值转换成右值,如果给右值本质上是不做任何动作的。

那么移动语义std::move的主要作用是什么呢?从语法上讲是生成了左值引用的右值!从功能上讲是帮助实现了移动赋值运算符,实现了转移资源所有权,本质上还是进行了一次拷贝的!

   T operator=(T&)
   T operator=(const T&)

传统的拷贝复制,参数是const T&也就是常左值引用,有非常大的局限性,不能在拷贝过程中修改源对象的内部成员。但是转移所有权的时候,是可能有要求的,典型的比如std::unique_ptr在移动过程中,需要把内部的指针变量置空,然后再调用析构函数,如果拷贝复制的话因为限定了const是不能够操作的。

总结一下就是这两种拷贝赋值都存在不足,第一个不能接受右值,第二个拷贝过程中不能修改源对象,无法实现资源转移。

Type Traits

标签分发,典型的就是STL中的各种算法,根据iterator_category_tag的不同来执行不同的操作

谓词的实现,都是继承自true_type和false_type,都是以is开头的,比如is_same_v

void函数返回

void f(int& x)
{
  return x==0?void():(printf("%d",x),f(--x));
}

利用return来实现递归,很有意思。