C++11 是第二个真正意义上的 C++ 标准,是一次重大升级。C++11 增加了很多现代编程语言的特性,比如自动类型推导、智能指针、lambda 表达式等。它不但提高了开发效率,还让程序更加健壮和优雅。本文将总结一些需要转换思维的新技术,而不仅仅分享一些一看即懂的技术点。
auto + decltype 跟踪返回值类型(trailing-return-type)
返回值类型后置语法,是为了解决函数返回值类型依赖于参数而导致难以确定返回值类型的问题。如下所示:
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
auto var = add(1, 2);
可以尝试,像这种返回值类型依赖入参的情况,如果不使用返回值后置语法,将无法实现像上面那样的简洁调用。虽然C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。使用了auto关键字以后,编译器会在编译期间自动推导出变量的类型。但auto仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。然而,auto并不能适用于所有的自动类型推导场景。上面的就是其中之一,必须有decltype的辅助才能达到正确推导。
右值引用
在此之前,引用语义并不清晰,实际上如不加const,其只能对变量(左值)进行引用。现在,引用被明确区分为左值引用和右值引用。原来的T &被明确称为左值引用,新增的T &&被明确称为右值引用,专用于且一般只能用于对右值的引用,并由此诞生了移动语义和完美转发等概念。其实原来的const T& 也可以引用右值,但存在隐式创建临时变量。另外,和常量左值引用不同的是,右值引用还可以对右值进行修改。
C++左值引用和右值引用
引用类型 | 可以引用的值类型 | 使用场景 | |||
---|---|---|---|---|---|
非常量左值 | 常量左值 | 非常量右值 | 常量右值 | ||
非常量左值引用 | Y | N | N | N | 无 |
常量左值引用 | Y | Y | Y | Y | 常用于类中构建拷贝构造函数 |
非常量右值引用 | N | N | Y | N | 移动语义、完美转发 |
常量右值引用 | N | N | Y | Y | 无实际用途 |
移动构造
在C++11之前,用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数,这里还涉及到深拷贝和浅拷贝的方式。另外,程序中的赋值操作,函数调用等,可能还会导致临时变量的产生。而临时变量的产生、销毁以及发生的拷贝操作本身就很隐晦(某些编译器会这些过程做专门的优化),并不会影响程序的正确性,因此很少进入程序员的视野。但是当程序的规模较大,执行流程复杂时,能尽量减少临时对象的产生和堆数据的相互拷贝就显得重要了。
新的移动语义,就是以移动而非拷贝的方式初始化类对象。移动构造是移动语义的具体实现,其可以将其他对象的数据所有权转移到该对象,看似“移动对象”,但实际上并没有移动任何数据,而只是“占为己有”。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会调用拷贝构造函数。
move()
C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,该函数可以将左值对象转化为右值对象。新对象将剥夺原对象对其数据的所有权,原对象无法再访问该数据,看似效果好像原对象的数据被移动了,但实际并没有。
完美转发
它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为 “万能引用” )。通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。但对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。因此,该新标准还引入了一个模板函数 forword<T>()
,我们只需要调用该函数,就可以很方便地解决此问题。
void otherdef(int &t) {
cout << "lvalue\n";
}
void otherdef(const int &t) {
cout << "rvalue\n";
}
// 重载函数模板,分别接收左值和右值
// 接收右值参数
template <typename T>
void myfunction(T&& t) {
// otherdef(t);
otherdef(forward<T>(t));
}
const和constexpr的区别
之前的const语义不清晰,现在其语义仅仅是:只读 。为何之前语义不清晰?虽然C++的const修饰的已经不同于C的做法,修饰普通变量时能起到真正常量的作用,通过强制类型转换都无法修改。但当const修饰引用时,又会和引用语义发生冲突,导致const并不能起到常量的效果。可以参考如下例子。
const int constVar = 88;
int *pconstVar = (int*)&constVar;
*pconstVar = 66;
cout << constVar << endl; // 88 强制修改都没有改变。修饰普通变量是,const似乎起到了真正的常量的效果
cout << *pconstVar << endl; // 66
int temp = 11;
const int &rTemp = temp;
temp = 22;
cout << rTemp << endl; // 22 然而,像这样的做法就使const的常量语义失效了,所有C++11将const的常量语义去掉了
而constexpr的语义是:常量 。其只能用编译阶段就能确定的常量表达式来初始化。constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力。但注意constexpr只能直接修饰内置基本类型,自定义类型需要实现consexpr构造函数。
function类模板
#include <functional>
template< class R, class... Args >
template< class R, class... Args >
class function<R(Args...)>;
R是调用时产生的类型 Args是可变参数的形参类型
该模板的成员函数
成员函数 | 说明 |
---|---|
(constructor) | 构造 |
(destructor) | 析构 |
operator= | 赋值一个新目标 |
operator bool | 检查是否包含一个有效目标 |
operator() | 调用目标 |
target | 获取指向目标的指针 |
target_type | 获取目标的类型,类型是通过typeid运算符确定的 |
std::function是一个通用多态 函数包装器(function wrapper) 模板,最早来自boost库,对应其boost::function函数包装器。 一个std::function类型对象实例可以包装下列这几种 可调用元素(callable element) 类型:函数、函数指针、Lambda表达式、bind表达式、类成员函数指针、数据成员指针或任意类型的函数对象(例如定义了operator()并拥有函数闭包)。被包装的可调用对象被称为std::function的目标。std::function对象可被拷贝和转移,并且可以使用指定的 调用特征(call signature) 来直接调用目标。当std::function对象未包装任何实际的可调用元素(空目标),调用该std::function对象将抛出std::bad_function_call异
常。
一个充分说明用法的例子:
struct Foo {
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_+i << '\n'; }
int num_;
};
void print_num(int i)
{
std::cout << i << '\n';
}
struct PrintNum {
void operator()(int i) const
{
std::cout << i << '\n';
}
};
int main()
{
// store a free function 普通函数
std::function<void(int)> f_display = print_num;
f_display(-9);
// store a lambda Lambda表达式
std::function<void()> f_display_42 = []() { print_num(42); };
f_display_42();
// store the result of a call to std::bind bind表达式
std::function<void()> f_display_31337 = std::bind(print_num, 31337);
f_display_31337();
// store a call to a member function 非静态,普通成员函数
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
const Foo foo(314159);
f_add_display(foo, 1);
f_add_display(314159, 1);
// store a call to a data member accessor 数据成员
std::function<int(Foo const&)> f_num = &Foo::num_;
std::cout << "num_: " << f_num(foo) << '\n';
}
参考文章:
Reference - C++ Reference (cplusplus.com)
C++11 - cppreference.com
C语言中文网
实验学习代码:
#include <iostream>
#include <string>
#include <functional>
#include <csignal>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif // _WIN32
using namespace std;
class Once {
public:
typedef function<void(void)> task;
template<typename FUNC>
Once(const FUNC& onConstructed, task onDestructed = nullptr) {
onConstructed();
_onDestructed = std::move(onDestructed);
}
Once(nullptr_t a = nullptr, task onDestructed = nullptr) {
_onDestructed = std::move(onDestructed);
}
~Once() {
if (_onDestructed) {
_onDestructed();
}
}
private:
Once() = delete;
Once(const Once &) = delete;
Once(Once &&) = delete;
Once &operator=(const Once &) = delete;
Once &operator=(Once &&) = delete;
private:
task _onDestructed;
};
class FuncObj {
public:
int operator()() const
{
cout << "FuncObj at: " << getIdentifer() << endl;
return 0;
}
string getIdentifer() const
{
return to_string(reinterpret_cast<uint64_t>(this));
}
};
// 重载被调用函数,查看完美转发的效果
void otherdef(int & t) {
cout << "lvalue\n";
}
void otherdef(const int & t) {
cout << "rvalue\n";
}
// 重载函数模板,分别接收左值和右值
template <typename T>
void myfunction(T&& t) {
// otherdef(t);
otherdef(forward<T>(t));
}
void sigHandler(int sigNum)
{
if (sigNum != 0) {
cout << "catch signal: " << sigNum << ", to exit.";
exit(EXIT_SUCCESS);
}
}
int main(int argc, char *argv[])
{
cout << "function main() starts\n";
string source(" hello world \n");
string chars("\r\n\t ");
string szmap(0xFF, '\0');
for (auto &ch : chars) {
szmap[(unsigned char &)ch] = '\1';
}
while (source.size() && szmap.at((unsigned char &)source.back())) {
source.pop_back();
}
while (source.size() && szmap.at((unsigned char &)source.front())) {
source.erase(0, 1);
}
cout << source << endl; // "hello world"
int a = 1;
const int b = 1;
int &b1 = a;
const int &c = 1;
int &&d = 1;
const int &&e = 1;
constexpr int f = 1;
myfunction(a); // lvalue
myfunction(b); // rvalue
myfunction(b1); // lvalue
myfunction(c); // rvalue
myfunction(d); // lvalue ???
myfunction(e); // rvalue ???
myfunction(f); // rvalue
FuncObj fObj;
Once once1(fObj); // output FuncObj at: xxx
// Once once2(FuncObj()); // dont output FuncObj at: xxx ???
Once once2((FuncObj())); // 无意间发现,为啥上条语句不输出了。原来编译器误认为上句是一个函数声明,具体细节啥不清楚。但可以加()避免该歧义
signal(SIGINT, &sigHandler);
// C++的const修饰的已经不同于C的做法,是真正的常量,会在符号表中放入常量;编译过程中若发现使用常量则直接以符号表中的值替换;c++编译器虽然可能为const常量分配栈空间 ,但不会使用其存储空间中的值。见下面的例子。编译器对 const 常量进行类型检查和作用域检查;
const int constVar = 88;
int *pconstVar = (int*)&constVar;
*pconstVar = 66;
cout << constVar << endl; // 88 并没有改变
cout << *pconstVar << endl; // 66
const int &rconstVar = 99;
pconstVar = (int*)&rconstVar;
*pconstVar = 55;
cout << rconstVar << endl; // 55 const引用变量的值通过强制处理改变了,这显然违背了const的常量语义
cout << *pconstVar << endl; // 55
int temp = 11;
const int &rTemp = temp;
temp = 22;
cout << rTemp << endl; // 22 甚至不用那么费劲,像这样的做法就使const的常量语义失效了,所有C++11将const的常量语义去掉了
constexpr int numexpr = 99;
// constexpr int &numexpr1 = numexpr; // 语法有误
pconstVar = (int*)&numexpr;
*pconstVar = 44;
cout << numexpr << endl; // 99 常量表达式同样没有,且不可创建constexpr引用,所以是真正的常量。
cout << *pconstVar << endl; // 44
while (true) {
cout << "Listening...\n";
#ifdef _WIN32
Sleep(1000);
#else
usleep(1000);
#endif // _WIN32
}
return 0;
}