首页 > 试题广场 >

请你详细介绍一下C++11中的可变参数模板、右值引用和lam

[问答题]

请你详细介绍一下C++11中的可变参数模板、右值引用和lambda这几个新特性。

参数包

在C++11中展开参数包不一定需要递归展开。可以利用一些小特性来展开,比如说例子中的print函数:

template <class ...Args>
void print(Args&& ...args) {
    int dummy[] = {
        ((std::cout << std::forward<Args>(args) << ','), 0)...
    };
}

这是利用了逗号表达式的特性,最终dummy数组中所有元素都是0,但是输出也都顺序执行了。其顺序由list initialization保证(list initialization也是C++11新特性。其中出现的表达式,其求值顺序按其出现顺序排列,也即从左至右顺序求值)。这个未使用的数组正常的编译器都会将其优化掉,不会占用空间。如果实在担心的话……

struct dummy {
    template <class ...Ts>
    dummy(Ts&& ...ts) {}
};

template <class ...Args>
void print(Args&& ...args) {
    dummy d{ ((std::cout << std::forward<Args>(args) << ','), 0)... };
}

如果不需要保证顺序,可以用一个变参函数来接收所有的表达式(函数参数虽然是从右至左压栈,但参数的求值顺序不确定)

template <class ...Ts>
void do_nothing(Ts&& ...ts) {}

template <class ...Args>
void print(Args&& ...args) {
    do_nothing(((std::cout << std::forward<Args>(args) << ','), 0)...);
}

这种方式只适用于对所有参数包中的参数执行统一的操作,比如说对每个参数执行一个函数。如果你需要求和这类操作的话,还是要递归展开。不过C++17引入了fold expression,会将其简化很多:

// auto推导函数返回值类型,C++14特性
template <class ...Args>
auto sum(Args&& ...args) {
    return 0 + ... + args; // 从左加到右
    // 或者
    return args + ... + 0; // 从右加到左
}

lambda

每个lambda对应一个你写不出名字的闭包类型(但是编译器知道),哪怕代码是一样的。这个闭包类型重载过operator()。你甚至可以继承一个lambda的类型(虽然写不出来,但可以decltype求出来)。可继承这个特性能够写一个配合C++17的std::variant使用的工具类出来。

此外闭包类型内还含了一个向函数指针转换的自定义转换函数,因此不带capture的lambda可以转换成对应的函数指针,比如说[](int a, int b) { return a + b; }可以转换成int(*)(int, int)。转换的时候在lambda前面加一个+就好(这是一元操作符加号)

int (*add)(int, int) = +[](int lhs, int rhs) { return lhs + rhs; };

C++14额外强化了lambda,允许其参数类型自动推导(每个auto对应一个模板类型参数),也可以包含变参参数包:

auto add = [](const auto &lhs, const auto &rhs) { return lhs + rhs; };

auto var_add = [](auto&& ...args) {
    auto sum = // whatever ;
    return sum;
};

此时相当于闭包类型里的operator()是模板函数。

关于capture,C++11可以指定全体按引用捕获(&),全体按值捕获(=),单独按引用捕获(&name),单独按值捕获(name),按引用捕获当前对象(this,需要lambda在成员函数内定义,不然不存在this)。C++14允许你在capture中初始化一个只在lambda内生效的值/引用变量,具体和定义一个变量类似,只是去掉类型。比如[v = std::vector<int>{{1, 2, 3}}]就给lambda添加了一个v,其类型是std::vector<int>,内容为1, 2, 3。C++17允许你按值捕获当前对象(*this)。

不指定mutable的lambda,它的operator()是const成员函数,因此所有按值捕获的capture是不能修改的。

右值引用

单独的右值引用其实没有用,也没有额外生成任何代码。真正的移动语义需要右值引用配合右值重载函数,移动构造函数,移动赋值来起作用。

std::move的实现相当简单,就是

template <class T>
typename std::remove_reference<T>::type&& move(T &&t) {
    return static_cast<typename remove_reference<T>::type&&>(t);
}

直接把T转换成T&&。(要remove_reference是因为T可能不是T,还可能是T&

std::forward由于引用折叠的存在,似乎可以实现成这个样子:

template<typename T>
T&& std::forward(T&& param) {
    return static_cast<T&&>(param);
}

写起来就是std::forward(rvalref),但实际上不行。右值引用绑定之后,延长了右值的生命周期,右值引用变量在后续使用中会被视作左值引用,如此实现的std::forward在任何情况下产生的都是左值引用。解决办法就是强迫手动指定模板参数T,并分别重载左值和右值引用情况。

template <class T>
T&& forward(typename std::remove_reference<T>::type& t) {
    return static_cast<T&&>(t);
}

template <class T>
T&& forward(typename std::remove_reference<T>::type&& t) {
    static_assert(!std::is_lvalue_reference<T>::value,
                  "Can not forward an rvalue as an lvalue.");
    return static_cast<T&&>(t);
}
发表于 2019-08-28 16:57:06 回复(0)