上一篇:《深入理解C++11》笔记-智能指针unique_ptr、shared_ptr、weak_ptr
本篇开始接受第六章的内容:常量表达式。
我们先来看一个例子:
const int number(){ return 5; }
constexpr int number_expr(){ return 5; }
int main()
{
const int n = 5;
int a[n] = {0};
int b[number()] = {0}; // 编译失败
int c[number_expr()] = {0};
return 0;
}
可以看到,用number函数作为数组的长度是编译失败的,因为number函数需要在运行时才能得到结果,而数组定义需要在编译时就知道长度。而number_expr却能够编译通过,是因为它有constexpr关键字进行修饰,constexpr关键字让函数在编译阶段就计算出结果。constexpr还能作用于数据声明以及类的构造函数,下面我们继续了解。
常量表达式函数
常量表达式函数需要满足几个条件,否则不能用constexpr关键字进行修饰:
- 函数只能包含return语句。
- 函数必须有返回值。
- 在使用前必须已经定义。
- return返回语句中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式。
// 函数只能包含return语句
constexpr int func()
{
int a = 5; // 编译失败,C++11中不允许return语句之外的语句(C++14h后支持)
return a;
}
// 函数必须有返回值
constexpr void func() // 编译失败,没有返回值无法得到常量
{
return ;
}
// 在使用前必须已经定义
constexpr int func();
int a[func()] = {0}; // 编译失败,使用前只有声明没有定义,因为需要在编译时就计算出结果,没有定义无法计算
constexpr int func()
{
return 5;
}
int b[func()] = {0};
// return返回语句中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式
int g1 = 5;
constexpr int func1()
{
return g1; // 编译失败
}
const int g2 = 5;
constexpr int func2()
{
return g2; // 编译通过
}
int number1(){return 5;}
constexpr int func1()
{
return number1(); // 编译失败
}
constexpr int number2(){return 5;}
constexpr int func2()
{
return number2(); // 编译通过
}
常量表达式值
const int i = 0;
constexpr int j = 0;
当我们用constexpr修饰一个变量时,他在大部分时候和const修饰没有区别。不过i只要在全局范围内声明,编译器一定会为它产生数据;而对于j,如果没有地方调用它,编译器可以选择不为它生成数据。
并且,默认只有内置类型才能修饰为常量表达式值,自定义类型如果要成为常量表达式值,必须定义一个constexpr修饰的构造函数:
class Example{
public:
constexpr Example() : data(1) {}
private:
int data;
};
int main()
{
constexpr Example ex;
return 0;
}
另外,constexpr修饰的构造函数必须满足以下条件:
- 函数体必须为空。
- 初始化列表只能由常量表达式来赋值。
常量表达式的应用
常量表达式可以用于模板函数,但是因为模板函数类型的不确定性,所以模板函数是否会被实例化为一个能够满足编译时常量性的版本也是未知的。针对这种情况,C++11中规定,如果模板函数被实例化后不满足常量性,那么constexpr关键字将会被忽略。
class Example{
private:
int data;
};
template<typename T>
constexpr T func(T t)
{
return t;
}
int main()
{
constexpr int i = func(1);
Example ex;
Example ex1 = func(ex);
constexpr Example ex2 = func(ex); // 无法通过编译,因为Example没有常量性,模板的constexpr被忽略
return 0;
}
可以看到,因为Example类型没有定义常量构造函数,所以不具有常量性。针对以上代码稍作修改就能编译通过了:
class Example{
public:
constexpr Example():data(1){} // 定义常量构造函数
private:
int data;
};
template<typename T>
constexpr T func(T t)
{
return t;
}
int main()
{
constexpr int i = func(1);
constexpr Example ex; // 模板入参需要constexpr修饰
Example ex1 = func(ex);
constexpr Example ex2 = func(ex);
return 0;
}
有时候,constexpr还能用来节省程序运行时的时间,把计算步骤提前到编译阶段:
size_t r_fib(size_t n) noexcept
{
if (n == 0) return 0;
if (n == 1) return 1;
return r_fib(n - 1) + r_fib(n - 2);
}
constexpr size_t c_fib(size_t n) noexcept
{
return n == 0 ? 0 : (n == 1 ? 1 : c_fib(n - 1) + c_fib(n - 2));
}
template <size_t N>
struct t_fib_ {
static const long value = t_fib_<N - 1>::value + t_fib_<N - 2>::value;
};
template <> struct t_fib_<0> { static const long value = 0; };
template <> struct t_fib_<1> { static const long value =1; };
int main(void)
{
constexpr size_t n = 25;
clock_t startTime, endTime;
startTime = clock();
size_t r1 = r_fib(n);
endTime = clock();
std::cout << r1 << ", r_fib: " << endTime - startTime << "ms" << std::endl; // 600+ms
startTime = clock();
constexpr size_t r2 = c_fib(n);
endTime = clock();
std::cout << r2 << ", c_fib: " << endTime - startTime << "ms" << std::endl; // 1ms
startTime = clock();
long r3 = t_fib_<n>::value;
endTime = clock();
std::cout << r3 << ", t_fib: " << endTime - startTime << "ms" << std::endl; // 0ms
return 0;
}
上面的例子,用三种方式计算斐波那契数并打印出。分别是函数、常量表达式函数、模板元编程(详细介绍可以看https://www.cnblogs.com/jiayayao/archive/2017/02/10/6388099.html)。可以看到常量表达式和模板元编程的方式,运行时间都极少甚至是没有,因为两者都是在编译时就计算出了结果。
需要注意的是,并不是所有的编译器都实现了在编译时计算,以上的结果是使用xcode编译的结果,而在vs2015中并没有在编译时进行计算。另外,常量表达式函数要在编译时进行计算,还需要满足函数适用于生成元数据的条件(元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量等)。
下一篇:《深入理解C++11》笔记-变长模板