C++中的constexpr


C++中的constexpr

https://blog.csdn.net/m0_52902391/article/details/120308866
https://zhxilin.github.io/post/tech_stack/1_programming_language/modern_cpp/cpp17/constexpr/#c14-constexpr

常量表达式

值不会改变且在编译期就能得到计算结果的表达式。
字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

constexpr 与 const

在定义常量时,const 和 constexpr 是等价的,都可以在程序的编译阶段计算出结果 都为常量表达式
但是const可以修饰函数的传入参数为只读(并非常量),而constexpr无法修饰

 // error
void func1(constexpr int p) {

}


// ok
void func2(const int p) {

}


void func3(const int p) {
    int a[p];//error ,此时p并非常量表达式,只是只读参数
}

用constexpr修饰函数

constexpr 并不能修改任意函数的返回值,这些函数成为常量表达式函数时,必须要满足以下几个条件:
(这些规则不仅对应普通函数适用,对应类的成员函数也是适用的)

1.函数必须要有返回值,并且 return 返回的表达式必须是常量表达式。

// error,不是常量表达式函数
constexpr void func1()
{
    int a = 100;
    cout << "a: " << a << endl;
}
 
// error,不是常量表达式函数
constexpr int func2()
{
    int a = 100;
    return a;
}

// ok
constexpr int func3()
{
    constexpr int a = 100;
    return a;
}
 

函数 func1() 没有返回值,不满足常量表达式函数要求
函数 func2() 返回值不是常量表达式,不满足常量表达式函数要求

2.整个函数的函数体中,不能出现非常量表达式之外的语句(using 指令、typedef 语句以及 static_assert 断言、return 语句除外)。

// error
constexpr int func1()
{
    constexpr int a = 100;
    constexpr int b = 10;
    for (int i = 0; i < b; ++i)
    {
        cout << "i: " << i << endl;
    }
    return a + b;
}
 
// ok
constexpr int func2()
{
    using mytype = int;
    constexpr mytype a = 100;
    constexpr mytype b = 10;
    constexpr mytype c = a * b;
    return c - (a + b);
}
 

在c++11中constexpr函数只能有一个return语句,不支持多个独立语句,最多也只能通过逗号表达式或三目表达式来表达。
C++14开始,constexpr函数可以使用多行语句编写

修饰模板函数

C++11 语法中,constexpr 可以修饰函数模板,但由于模板中类型的不确定性,因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。
如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。


#include< iostream >
using namespace std;
 
struct Person {
    const char* name;
    int age;
};
 
// 定义函数模板
template< typename T >
constexpr T dispaly(T t) {
    return t;
}
 
int main()
{
    struct Person p { "luffy", 19 };
    //普通函数
    struct Person ret = dispaly(p);
    cout << "luffy's name: " << ret.name << ", age: " << ret.age << endl;
 
    //常量表达式函数
    constexpr int ret1 = dispaly(250);
    cout << ret1 << endl;
 
    constexpr struct Person p1 { "luffy", 19 };
    constexpr struct Person p2 = dispaly(p1);
    cout << "luffy's name: " << p2.name << ", age: " << p2.age << endl;
    return 0;
}

在上面示例程序中定义了一个函数模板 display(),但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求:

struct Person ret = dispaly(p); 由于参数 p 是变量,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的
constexpr int ret1 = dispaly(250); 参数是常量,符合常量表达式函数的要求,此时 constexpr 是有效的
constexpr struct Person p2 = dispaly(p1); 参数是常量,符合常量表达式函数的要求,此时 constexpr 是有效的

修饰构造函数(编译期的对象构造)

如果想用直接得到一个常量对象,也可以使用 constexpr 修饰一个构造函数,这样就可以得到一个常量构造函数了。常量构造函数有一个要求:构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值。

#include <iostream>
using namespace std;
 
struct Person {
    constexpr Person(const char* p, int age) :name(p), age(age)
    {
    }
    const char* name;
    int age;
};
 
int main()
{
    constexpr struct Person p1("luffy", 19);
    cout << "luffy's name: " << p1.name << ", age: " << p1.age << endl;
    return 0;
}
 

constexpr lambda表达式

C++17对constexpr的适用范围再次进行扩展,已经可以运用在lambda表达式上了。我们曾经在关于闭包的文章中讲过,编译器在处理lambda表达式的时候,会自动为其合成一个闭包类型(仿函数),这个闭包类型包含一个operator()的重载操作符,lambda表达式的捕获列表将转换成该仿函数的成员变量,最后lambda表达式会被这个闭包类型的对象替换。

以下面这个例子为示范:

#include <iostream>
#include <functional>

std::function<int(int)> foo(int a, int b) {
    return [a, b](int x) {
        return a * x + b;
    };
}

int main() {
    std::cout << foo(1, 2)(2) << std::endl; //4
}

编译器会将其转换为如下形式:

#include <iostream>
#include <functional>

class SomeFunctor {
public:
    SomeFunctor(int a, int b)
        : m_a(a), m_b(b)
    { }

    int operator()(int x) {
        return m_a * x + m_b;
    }

private:
    int m_a;
    int m_b;
};

std::function<int(int)> foo(int a, int b) {
    return SomeFunctor(a, b);
}

int main() {
    std::cout << foo(1, 2)(2) << std::endl; //4
}

C++17对lambda表达式做了如下改进:

  • 若lambda表达式捕获的变量是字面量类型(literal type),则整个lambda表达式也将表现为字面量类型。

  • 增加constexpr lambda表达式语法。

  • 若一个lambda表达式满足constexpr函数的要求,即使没有明确声明为constexpr,编译器也会将其推导为constexpr lambda表达式。

constexpr lambda表达式的显式语法如下:

[capture_list] (params_list) constexpr -> return_type {function_body}

在完整体的lambda表达式中,constexpr添加到参数列表和返回值类型之间,举个例子:

void foo() {
    auto fib = [](int n) constexpr -> int {
        int a = 0, b = 1;
        for (int i = 0; i < n; ++i) {
            int c = a + b;
            a = b;
            b = c;
        }
        return a;
    };
    static_assert(fib(5) == 5, "");
}

constexpr lambda表达式和constexpr函数一样,需要满足的条件

  • 返回值的类型必须是字面量类型
  • 参数类型必须是字面量类型
  • 函数体还需满足:
    • 没有使用inline语句
    • 没有使用goto或label
    • 没有使用try…catch语句
    • 没有声明或使用非字面量类型的变量或使用thread local storage

只要满足上述提到的条件,即使没有将lambda声明为constexpr,编译器也会自动将其视为constexpr lambda表达式

且与constexpr函数一样,constexpr lambda表达式中,如果参数或捕获列表的参数不是编译期常量,则constexpr lambda表达式会退化为普通的lambda表达式

当一个lambda表达式被显式或隐式地声明为constexpr,则它可以被转换为一个constexpr的函数指针

constexpr if 编译期分支选择

传统的if-else语句是在运行期进行判断和选择的,无法运用在编译期,所以在泛型编程中,无法使用if-else来直接做一些判断。在C++17之前,只能被拆分为一个泛型版本和一个特化版本

//C++17之前的写法
void print()
{
    std::cout << std::endl;
}

template <typename T, typename... Ts>
void print(T head, Ts... tail)
{
    std::cout << head << std::endl;
    print(tail...);
}

C++17引入了constexpr if,支持在编译期进行判断,即在编译期就可以直接知道结果,可以广泛应用于泛型编程。以上例子使用C++17实现则可以将两步合二为一

template <typename T, typename... Ts>
void print(T head, Ts... tail)
{
    std::cout << head << std::endl;
    if constexpr (sizeof...(tail) > 0)
        print(tail...);
}

优化std::enable_if写法

constexpr if在泛型编程中的一个应用是,可以替代冗长的std::enable_if的写法。在C++17之前,需要为了不同类型条件去写各式各样的特化模版,利用SFINAE加上std::enable_if,会写得非常复杂

//C++17之前
template<typename T> 
std::enable_if_t<std::is_integral<T>::value, std::string> to_string(T t) {
    return std::to_string(t);
}
 ​
template<typename T>
std::enable_if_t<!std::is_integral<T>::value, std::string> to_string(T t) {
    return t;
}

//C++17
template <typename T>
auto to_string(T t) {
    if constexpr(std::is_integral<T>::value)
        return std::to_string(t);
    else //此处else不可省略
        return t;
}

int main() {
    std::cout << to_string("ok") << std::endl;
    std::cout << to_string(123) << std::endl; //如果省略else则会报错:error: 'auto' in return type deduced as 'int' here but deduced as 'std::string' in earlier return statement
    return 0;
}

上面的例子中,else不可省略,否则会产生编译器推导错误,无法编译通过。

编译器在做优化时,甚至会把没有使用的分支省略掉,当然另一个分支必须支持C++语法,而老的C++标准即使使用了if,另一个分支也会被编译。对于constexpr if,编译器只会实例化条件通过的子句。由于返回值类型我们可以用auto让编译器自动推导,所以每个constexpr if子句的返回值类型也不需要完全一致。

注:

1.const用于修饰不能被修改的对象,但const对象的值通常在程序运行期间才能确定

2.constexpr用于修饰常量表达式或可返回常量表达式的constexpr函数,在编译时能确定值。

3.constexpr函数都是inline函数


文章作者: xucanxx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xucanxx !
  目录