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 }