constexpr
概述
在 C++11 引入、在 C++14 得到大幅改进的 constexpr 关键字,字面意思是 constant expression,常量表达式。
一个 constexpr 变量是一个编译时完全确定的常数。
一个 constexpr 函数至少对于某一组实参可以在编译期间产生一个编译期常数。
一个 constexpr 函数不保证在所有情况下都会产生一个编译期常数(因而也是可以作为普通函数来使用的)。编译器也没法通用地检查这点。编译器唯一强制的是:constexpr 变量必须立即初始化初始化只能使用字面量或常量表达式(后者不允许调用任何非 constexpr 函数)。
#include <array>
constexpr int sqr(int n)
{
return n * n;
}
int main()
{
constexpr int n = sqr(2);
std::array<int, n> a;
return 0;
}
要检验一个 constexpr 函数能不能产生一个真正的编译期常量,可以把结果赋给一个 constexpr 变量。成功就确认了,在这种调用情况下能真正得到一个编译期常量。
constexpr 和编译期计算
使用编译期常量,跟那些类模板里的 static const int 变量一样,是可以进行编译期计算的。
constexpr int factorial(int n)
{
if (n == 0) return 1;
return n * factorial(n - 1);
}
int main()
{
constexpr int n = factorial(5);
cout << n << endl;
return 0;
}
汇编代码里面可以看到78h(120)。
constexpr int n = factorial(5);
00C41AD8 mov dword ptr [n],78h
cout << n << endl;
00C41ADF mov esi,esp
00C41AE1 push offset std::operator<<<std::char_traits<char> > (0C41460h)
00C41AE6 mov edi,esp
00C41AE8 push 78h
在这个 constexpr 函数里,是不能写 static_assert(n >= 0) 的。
一个 constexpr 函数仍然可以作为普通函数使用,传入一个普通 int 是不能使用静态断言的。
替换方法是在 factorial 的实现开头加入:
if (n < 0) { throw std::invalid_argument("please non-negative number!"); }
constexpr 和 const
注意 const 在类型声明的不同位置会产生不同的结果。
- const char* == char const* :是指向常字符的指针,指针指向的内容不可更改。
- char * const:指向字符的常指针,指针本身不可更改。
本质上,const 用来表示一个运行时常量。在 C++ 里,const 后面带上 constexpr 用法,也代表编译期常数。
在有了 constexpr 之后应该使用 constexpr 在这些用法中替换 const 。
从编译器的角度,为了向后兼容性,const 和 constexpr 在很多情况下还是等价的。
注意一个 constexpr 变量仍然是 const 常类型。像 const char* 类型是指向常量的指针、自身不是 const 常量一样,下面这个表达式里的 const 也是不能缺少的:
constexpr int a = 0; constexpr const int& b = a; //去除const报错:无法从“const int”转换为“int &”
报错原因是:编译器就会认为你是试图将一个普通引用绑定到一个常数上。
按照 const 位置的规则,constexpr const int& b 实际该写成 const int& constexpr b。
constexpr 不需要像 const 一样有复杂的组合,因此永远是写在类型前面的。
内联变量
C++17 引入了内联(inline)变量的概念,允许在头文件中定义内联变量,然后像内联函数一样,只要所有的定义都相同,那变量的定义出现多次也没有关系。对于类的静态数据成员,const 缺省是不内联的,而 constexpr 缺省就是内联的。
用 & 去取一个 const int 值的地址、或将其传到一个形参类型为 const int& 的函数去的时候(ODR-use:one definition rule),就会体现出来区别。
struct S
{
static const int s = 0;
};
int main()
{
cout << S::s << endl; // OK
vector<int> v;
// push_back(const T& v)
v.push_back(S::s);
std::cout << v[0] << std::endl; // 链接抱错
return 0;
}
注意:MSVC 缺省不报错,使用标准模式/Za 命令行选项会出现这个问题。
ODR-use 的类静态常量也需要有一个定义,在没有内联变量之前需要在某一个源代码文件(非头文件)中写:
const int S::s = 0;
解决问题简单方法是把 S里的 static const 改成 static constexpr 或 static inline const。
- 类的静态 constexpr 成员变量默认就是内联的。
- const 常量和类外面的 constexpr 变量不默认内联,需要手工加 inline 关键字才会变成内联。
o_c.h
需要 cpp17才能编译,很方便的输出标准容器内容的简单工具,参考链接。
// /std:c++17
#ifndef __O_C_H__
#define __O_C_H__
#include <ostream> // std::ostream
#include <type_traits> // std::false_type/true_type/decay_t/is_same_v
#include <utility> // std::declval/pair
// 检测某个类型是否为std::pair
template <typename T>
struct is_pair : std::false_type {};
template <typename T, typename V>
struct is_pair<std::pair<T, V>> : std::true_type {};
template <typename T>
// onst 常量和类外面的 constexpr 变量不默认内联
inline constexpr bool is_pair_v = is_pair<T>::value;
// 使用 SFINAE ,来检测模板参数 T 的对象是否已经可以直接输出到 std::ostream
template <typename T>
struct has_output_fun {
template <class V>
static auto output(V* ptr) -> decltype(std::declval<std::ostream&>() << *ptr, std::true_type());
template <class V>
static std::false_type output(...);
// 类的静态 constexpr 成员变量默认就是内联的。
static constexpr bool value = decltype(output<T>(nullptr))::value;
};
// 一个内联 constexpr 变量来简化表达
template <typename T>
inline constexpr bool has_output_fun_v = has_output_fun<T>::value;
//声明一个 std::pair 的输出函数
template <typename T, typename V>
std::ostream& operator<<(std::ostream& os, const std::pair<T, V>& pr);
// 定义了一个 key_type 类型,就认为遇到了关联容器,输出形式为“x => y”,而不是“(x, y)”。
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element, const Cont&, const std::true_type) -> decltype(std::declval<typename Cont::key_type>(), os);
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element, const Cont&, ...) -> decltype(os);
// 主输出函数的定义启用有两个不同的 SFINAE 条件
// 用 decltype 返回值的方式规定了被输出的类型必须有 begin() 和 end() 成员函数。
// 用 enable_if_t 规定了只在被输出的类型没有输出函数时才启用这个输出函数。
// 否则,对于 string 这样的类型,编译器发现有两个可用的输出函数,就会导致编译出错。
template < typename T, typename = std::enable_if_t<!has_output_fun_v<T>>>
auto operator<<(std::ostream& os, const T& container) -> decltype(container.begin(), container.end(), os)
{
using std::decay_t;
using std::is_same_v;
// 对非字符类型,我们在开始输出时,先输出“{ ”。这儿使用了 decay_t,
// 是为了把类型里的引用和 const/volatile 修饰去掉,只剩下值类型。
// 如果容器里的成员是 char,把 char& 和 const char& 还原成 char。
using element_type = decay_t<decltype(*container.begin())>;
constexpr bool is_char_v = is_same_v<element_type, char>;
if constexpr (!is_char_v) {
os << "{ ";
}
if (!container.empty()) {
auto end = container.end();
bool on_first_element = true;
for (auto it = container.begin(); it != end; ++it) {
if constexpr (is_char_v) {
if (*it == '\0') {
break;
}
}
if constexpr (!is_char_v) {
if (!on_first_element) {
os << ", ";
}
else {
on_first_element = false;
}
}
// 使用标签分发技巧来输出容器里的元素。output_element 不纯粹使用标签分发,还会检查容器是否有 key_type 成员类型。
output_element(os, *it, container, is_pair<element_type>());
}
}
if constexpr (!is_char_v) {
os << " }";
}
return os;
}
// output_element两个重载实现
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element, const Cont&, const std::true_type)-> decltype(std::declval<typename Cont::key_type>(), os)
{
os << element.first << " => " << element.second;
return os;
}
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element, const Cont&, ...) -> decltype(os)
{
os << element;
return os;
}
// 输出pair
template <typename T, typename V>
std::ostream& operator<<(std::ostream& os, const std::pair<T, V>& pr)
{
os << '(' << pr.first << ", " << pr.second << ')';
return os;
}
#endif
测试
map<string, int> m {{"A", 1}, {"B", 2}, {"C", 3}};
cout << m << endl; //{ A => 1, B => 2, C => 3 }
- 本文链接:https://morisa66.github.io/2021/03/04/constexpr/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。