进击的 c++

2014-03-12 by fatboyzz in 码农 tags: c++

这篇文章是讲 c++ 完美转发的

前提:

  • c++ 的变量有值语义,所以为了避免调用大开销的复制构造函数,函数形参应该采用 const T& 或者 T&。

  • 这里不讨论原始指针、智能指针,因为指针本身是传值。

  • 如果需要在函数中修改形参的值,形参就只能用 T& 或者 T&&。所以 const T& 不在本篇文章讨论范围。

  • 希望能够写包装函数,为已有函数添加功能或者写高阶函数。如:std::make_shared() std::tuple() std::function() ...

假设下面就是一个复制开销很大的类。

class Object {
public:
    Object() {}
    Object(const Object &obj)
    {
        printf("slow copy\n");
    }
    Object(Object &&obj)
    {
        printf("fast move\n");
    }
};

还有对这个类进行处理的函数。

void solve(Object &&obj)
{
    printf("solve rvalue\n");
}

void solve(Object &obj)
{
    printf("solve lvalue\n");
}

以及测试用的 main。(wrap 函数后面定义)

int main()
{
    printf("-- lvalue --\n");
    Object obj;
    wrap(obj);

    printf("-- rvalue --\n");
    wrap(Object());

    printf("-- lvalue_reference --\n");
    Object &obj2 = obj;
    wrap(obj2);

    printf("-- rvalue_reference --\n");
    wrap(std::move(Object()));
    return 0;
}

下面尝试给 solve(Object) 写个包装函数 wrap(Object)。

这个包装函数应该能够实现完美转发。即:

  • 给 wrap 右值,调用右值引用版本的 solve 。
  • 给 wrap 左值,调用左值引用版本的 solve 。

尝试1:

void wrap(T obj)
{
    solve(obj);
}

输出:

-- lvalue --
slow copy
solve lvalue
-- rvalue --
solve lvalue
-- lvalue_reference --
slow copy
solve lvalue
-- rvalue_reference --
fast move
solve lvalue
形参类型 T in wrap type of obj
Object(左值) Object Object
Object(右值) Object Object
Object& Object Object
Object&& Object Object
  • 形参为左值时,错误的调用了 Object 的复制构造。
  • 形参为右值时,错误的重载了 solve lvalue 。 这是因为变量 obj 在 wrap 中是个左值。对于左值实参,重载的时候选用了引用版本。
  • T 在规约的时候是不会把引用加入的。

尝试2:

template <typename T>
void wrap(T &obj)
{
    solve(obj);
}
  • 编译错误,不能用右值调用 wrap 了。

尝试3:

template <typename T>
void wrap(T &&obj)
{
    solve(obj);
}

输出:

-- lvalue --
solve lvalue
-- rvalue --
solve lvalue
-- lvalue_reference --
solve lvalue
-- rvalue_reference --
solve lvalue
形参类型 T in wrap type of obj
Object(左值) Object& Object&
Object(右值) Object&& Object&&
Object& Object& Object&
Object&& Object&& Object&&
  • 用 T&& 作为形参类型时,使用了特殊的归约规则.
    • Object&&& -> Object&
    • Object&&&& -> Object&&
  • 形参为右值或右值引用时,错误的重载了 solve lvalue。 这是因为 Object && 类型的 obj 在 wrap 中是个左值。对于左值实参,重载的时候选用了引用版本。
  • 离成功只差一步了,如果能把 Object&& 类型的 obj 转化成一个右值,同时 Object& 类型的 obj 还是左值,完美转发就成立了。 而把右值引用变量的 obj 转化成一个右值可以通过类型转换的方式构造临时对象来完成。(Object&&)obj

尝试4:

template <typename T>
void wrap(T &&obj)
{
    solve(std::forward<T>(obj));
}

输出:

-- lvalue --
solve lvalue
-- rvalue --
solve rvalue
-- lvalue_reference --
solve lvalue
-- rvalue_reference --
solve rvalue
  • 参数类型转化的表格与尝试 3 是一样的,不再列出。
  • std::forward() 正是一个可以把右值引用变量 obj 转化成一个右值,同时左值引用变量 obj 还是左值的函数。
  • 期待已久的结果出现了。注意这里没有任何复制开销。

std::forward() 源码分析

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

template <class _Tp>
inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX11
_Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) _NOEXCEPT
{
    static_assert(!std::is_lvalue_reference<_Tp>::value,
        "Can not forward an rvalue as an lvalue.");
    return static_cast<_Tp&&>(__t);
}
形参类型 _Tp type of __t static_cast<_Tp&&>(__t)
Object(左值) Object Object& Object&&(右值)
Object(右值) Object Object&& Object&&(右值)
Object& Object& Object& Object&(左值)
Object&& Object&& Object& Object&&(右值)
  • 完美转发中只用到了后两条
  • _Tp 调用时已给出,就是形参 obj 的类型。
  • 形参类型是左值,__t 重载为 Object& 。形参类型为右值,__t 重载为 Object&&
  • static_assert 防止自作聪明的程序员写 std::forward<int&>(100) 这是错误的写法。_Tp 必须是实参本身的类型。

总结

其实前面的都是废话。如果要实现完美转发,关键两点。

  • 使用 T&& arg 作为形参
  • 转发给其他函数时 std::forward(arg)

比如:

template <typename Func, typename... Param>
auto apply(Func f, Param&&... p) -> decltype(f(p...))
{
    return f(std::forward<Param>(p)...);
}




Comments