• C++20:constexpr、consteval和constinit


    缘由

    C++之父Bjarne Stroustrup和Gabriel Dos Reis于2003年向C++标准委员会提出了一种用于编译期求值的更好的机制。他们的目标:

    • 让编译时计算达到类型安全
    • 通过将计算移至编译时,提升运行效率
    • 支持嵌入式编程
    • 直接支持元编程(而非模板元编程)
    • 让编译时编程和“普通编程”类似

    2003年提议实现:允许在常量表达式中使用以constexpr为前缀的函数,还允许常量表达式使用简单的用户自定义类型,即字面值常量,这些字面值常量是一种所有运算都是constexpr的类型。

    C++标准

    C++标准委员会从C++11开始支持Bjarne Stroustrup和Gabriel Dos Reis的提议:

    • C++11引入constexpr关键字,此时constexpr可修饰变量亦可函数,但是要求函数必须是纯函数;
    • C++14标准移除了C++11的大部分限制,允许constexpr函数使用局部变量,同时实现对其他函数的支持;
    • C++17 将 constexpr 关键字引⼊到 if 语句,允许lambda声明为constexpr;
    • C++20允许将字面值类型用做模板参数。

    到C++20对constexpr的完整支持,constexpr标准化跨越了13年,由4个标准实现。C++20也是最接近最初语言设计模板的版本,但是constexpr 函数的设计其实也不够严谨,所以 C++20 又引入了 consteval;由于constexpr仅能实现编译时常量求值,为了解决编译时非常量求值问题,C++20又引入了constinit关键字。

    具备下述条件的函数,我们称之为纯函数

    • 函数无法访问非本地对象
    • 函数不能对调用者的环境产生副作用

    C++11

    C++11中的constexpr可修饰变量亦可修饰函数。但是无论是修饰变量还是函数,都要求其必须能在编译期常量求值。

    constexpr变量

    C++11标准规定,满足下述条件的变量,可声明为constexpr变量:

    • 变量的类型必须是字面类型
    • 变量必须立即被初始化
    • 变量的初始化包括所有隐式转换、构造函数调用等的全表达式必须是常量表达式
    • 变量的类型不能是类类型或类类型的数组

    constexpr表达式

    constexpr表达式是指值不会改变并且在编译过程就可求值的表达式。声明为constexpr的变量一定const变量,而且必须用常量表达式初始化。

    例如:

    constexpr int hoursOfDay = 24; // 24 是常量表达式
    constexpr int minutesOfHour = 60; // 60 是常量表达式
    constexpr int minutesOfDay =  hoursOfDay * minutesOfHour; // hoursOfDay和minutesOfHour都是常量表达式,所以hoursOfDay * minutesOfHour也是常量表达式。
    constexpr int seconds = secondsOfDay(); // secondsOfDay必须是常量函数,否则此声明非法。
    
    • 1
    • 2
    • 3
    • 4

    constexpr与自定义类型

    对应自定义类型,不能用constexpr 直接修饰类型,例如:

    constexpr struct Rectangle  // constexpr 修饰自定义类型无效
    {
    	int length;
    	int width;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果要定一个结构体/或类常量对象,可采用这样的实现模式,例如:

    struct Rectangle
    {
    	int length;
    	int width;
    };
    
    constexpr Rectangle rect{1, 2};
    constexpr int length = rect.length;
    constexpr int width = rect.width;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    constexpr与指针

    如果一个指针声明为constexpr,那么限定符constexpr仅对指针有效,与指针所指对象无关。例如:

    
    #include 
     
    int main()
    {
        int i = 1;
        int j = 2;
        std::cout << "i=" << i << std::endl;
        constexpr int* p = &i;
        *p = 8;
        std::cout << "i=" << i << std::endl;
        //p = &j; // 由于p有constexpr限定,导致此行代码编译失败
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    代码执行结果

    i=1
    i=8
    
    • 1
    • 2

    constexpr函数

    C++11标准规定,满足下述条件的函数,可声明为constexpr函数:

    • 函数必须为非虚函数
    • 函数体不能包含try和goto语句
    • 函数的返回值和入参必须都是字面类型
    • 函数体只能包含:
      – 空语句(仅分号)
      – static_assert 声明
      – 类或枚举的 typedef 声明及别名声明
      – using 声明
      – using 指令
      – 如果函数不是构造函数,函数体仅能存在一条return 语句
    • 构造函数可声明为constexpr(而且该类必须无虚基类),析构函数不可声明为constexpr
      – 函数体不能=delete
      – 每个子对象都必须初始化,而且子对象都必须存在 constexpr 构造函数

    对于 constexpr 函数模板和类模板的 constexpr 函数成员,必须至少有一个特化满足上述要求。

    • constexpr非构造函数最多只能包含一行return语句,例如constexpr 斐波那契数列。
    // 此斐波那契数列实现的复杂度等同于迭代的方法,基本上为O(n)。
    constexpr long int fibonacci(int n) 
    { 
    	return (n <= 1)? n : fibonacci(n-1) + fibonacci(n-2); //只能包含一个retrun语句
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • constexpr只能引用全局字面值常量(编译阶段即可确定取值的变量),例如:
    constexpr int DAYS_OF_YEAR = 365;
    constexpr int daysOfYear() 
    {
    	return DAYS_OF_YEAR;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • constexpr只能调用其他constexpr函数,不能调用非constexpr函数。例如:
    constexpr int hoursOfYear()
    {
    	return 24 * daysOfYear();
    }
    
    • 1
    • 2
    • 3
    • 4
    • constexpr可以用于模板类和函数模板,而且可将非 constexpr 模板的显式专用化声明为 constexpr,例如:
    // 例 1: 函数模板
    // Compile-time computation of array length
    template<typename T, int N>
    constexpr int length(const T(&)[N])
    {
        return N;
    }
    
    const int nums2[length(nums) * 2] { 1, 2, 3, 4, 5, 6, 7, 8 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    // 例 2: 将非 constexpr 模板的显式专用化声明为 constexpr
    #include 
    
    template<uint64_t N>
    struct Fibonacci
    {
      static constexpr uint64_t value = Fib<N - 1>::value + Fib<N - 2>::value
    };
    
    template<>
    struct Fibonacci<1>
    {
        static constexpr uint64_t value = 1;
    };
    
    template<>
    struct Fibonacci<2>
    {
        static constexpr uint64_t value = 1;
    };
    
    int main()
    {
        auto value = Fibonacci<26>::value;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    C++14

    C++11标准中,constexpr 修饰函数除了可以包含 using 指令、typedef 语句以及 static_assert 断⾔ 外,只能包含⼀条 return 语句。而C++14标准允许 constexpr函数使用局部变量,同时实现对其他函数的支持,所以constexpr 修饰的函数可包含 if/switch 等条件语句,也可包含 for 循环。

    虽然C++14放开了很多限制,但是依然存在部分C++11存在的严格限制:

    • 函数体内不能有goto和try块,以及任何static和局部线程变量;
    • 在函数中只能调用其他constexpr函数;
    • 该函数也不能有任何运行时才会有的行为,比如抛出异常、使用new或delete操作等等;
    • constexpr函数依然必须是纯函数

    所以在C++14中,如果把斐波那契函数可以改成普通函数一样,它的可读性会大大提升。

    constexpr unsigned fibonacci(unsigned i) 
    {
     	switch (i) 
       	{
        case 0: 
        	return 0;
        	break;
        case 1: 
        	return 1;
        	break;
        default: 
        	return fibonacci(i - 1) + fibonacci(i - 2);
        	break;
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    常量模板

    常量模板是变量模板的一种特别形式,常量模板的表示常量的类型是模板,但是常量的值是const属性。我们可以利用constexpr 实现常量模板。

    template<typename T = long double>
    constexpr T pi = T{3.1415926535897932385};
    
    • 1
    • 2

    C++17

    C++17对C++14标准增加了2个扩展:(一)将 constexpr 这个关键字引⼊到 if 语句;(二)将 constexpr 与Lambda Expression结合。

    constexpr与if

    C++17 基于C++14,将 constexpr 这个关键字引⼊到 if 语句,允许在代码中声明常量表达式的判断条件。我们称constexpr if这种结合方式叫静态if,静态if格式为:

    if constexpr(cond)
         statement1; // Discarded if cond is false
    else
         statement2; // Discarded if cond is true
    
    • 1
    • 2
    • 3
    • 4

    基于静态if,获取变量数值的模板函数举例:

    template <typename T>
    auto getValue(T t) 
    {
        if constexpr (std::is_pointer_v<T>)
        {
            return *t;
        }
        else
        {
            return t;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    constexpr与Lambda Expression

    C++17标准允许lambda expression在编译期求值,但是constexpr lambda expression也准寻下述C++17标准:

    • Lambda Expression捕获的参数必须是字面值类型(Literal Type)
        int y = 32;
        auto answer = [y]() constexpr
        {
            int x = 10;
            return y + x;
        };
    
        constexpr int increment(int n)
        {
            return [n] { return n + 1; }();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 如果 lambda 结果满足 constexpr 函数的要求,则 lambda 是隐式的 constexpr;
        auto answer = [](int n)
        {
            return 32 + n;
        };
    
        constexpr int response = answer(10);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 如果 lambda 是隐式或显式的 constexpr,并且将其转换为函数指针,则生成的函数也是 constexpr:
        auto increment = [](int n)
        {
            return n + 1;
        };
    
        constexpr int(*inc)(int) = increment;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    C++20

    C++20标准兼容C++11,C++14,C++17标准,一方面对constexpr扩展;另一方面引入consteval和constinit解决constexpr存在的缺陷,使之臻于完美。

    C++20标准的扩展。第一个是非类型模板参数的约束释放;第二个是编译时内存分配;第三个是编译时多态,即constexpr虚拟函数的引入;第四个constexpr允许try-catch;第五个 constexpr 中改变联合体的活跃成员。

    虽然C++20增加了多项扩展,但是C++20 constexpr关键字依然存在两个缺陷,第一个缺陷是无法强制函数在编译器求值,第二个缺陷是无法解决编译时非常量求值问题。

    非类型模板参数

    C++20 之前,模板的非类型模板参数仅支持简单的数值类型,但是C++20对此做出了重大调整。

    • 在简单数值类型的基础上,增加对float类型的支持。
    template <size_t N>
    constexpr int f() {...}
    
    template <float val>
    constexpr float g() {...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 允许使用auto由编译期进行类型推导的非类型参数
    template <auto ...>
    struct ArgList
    {
    };
    
    ArgList<'C', 0, 2L, nullptr> argList;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • STL对非类型字符串模板参数的支持。 C++20 之前,你不能将字符串用作非类型的模板参数,那现在我们可以使用stl中的basic_fixed_string解决这个问题,例如:
    template<std::basic_fixed_string T>
    class StringTemplate 
    {
        static constexpr char const* name = T;
    public:
        void hello() const 
        {
        	return name;
        }
    };
    
    int main() 
    {
        StringTemplate<"Hello!"> stringTemplate;
        stringTemplate.hello();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    一个可保障非类型字符串模板参数正常工作的basic_fixed_string定义:

    #include 
    
    template<unsigned N>
    struct basic_fixed_string
    {
        char m_str[N + 1]{};
        
        constexpr basic_fixed_string(char const* s)
        {
            for (unsigned i = 0; i != N; ++i)
            {
                m_str[i] = s[i];
            }
        }
        constexpr operator char const*() const
        {
            return m_str;
        }
    
        // C++20 三目运算符
        auto operator<=>(const basic_fixed_string&) const = default;
    };
    
    // CTAD 自定义类模板推送指引 C++17 开始支持
    template<unsigned N> basic_fixed_string(char const (&)[N]) -> basic_fixed_string<N - 1>;
    
    template<basic_fixed_string name>
    class StringTemplate
    {
    public:
        auto hello() const { return name; }
    };
    
    int main()
    {
        basic_fixed_string fixedString("Hello!!!!");
        std::cout << fixedString << std::endl;
        
        StringTemplate<"Hello!"> foo;
        std::cout << foo.hello() << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    编译时内存分配

    C++20编译时内存分配,constexpr函数可以进行有限制的动态内存分配和和使用std::vector/std::string。C++20之前只有std::array可以在编译期使用。当然这依然是有限制的使用:

    • constexpr函数中不能使用std::unique_ptr / std::shared_ptr
    • 动态内存的生命周期必须在constexpr函数的上下文中,即不能返回动态内存分配的指针
    • 不能返回 std::vector / std::string 对象。

    例如,constexpr helloWorld的定义就是C++20标准不允许的。:

    constexpr auto helloWorld()
    {
        std::string s1{"hello "};
        std::string s2{"world"};
        std::string s3 = s1 + s2;
        return s3;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    但是仅函数内部使用 std::vector/std::string,new/delete,则这又是constexpr函数所允许的。举例如下:

    // 例1:基于new/delete采用constexpr函数求和
    constexpr int sum(int n)
    {
        auto p = new int[n];
    
        std::iota(p, p + n, 1);
    
        auto t = std::accumulate(p, p + n, 0);
        
        delete[] p;
        return t;
    }
    
    static_assert(sum(10) == 55);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    // 例2:基于vector采用constexpr函数求和
    constexpr int sum(int n) 
    {
        std::vector<int> v(10);
    
        std::iota(v.begin(), v.end(), 1);
        auto t = std::accumulate(v.begin(), v.end(), 0);
    
        return t;
    }
    
    static_assert(sum(10) == 55);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    编译时多态

    constexpr可以在编译时内存分配,那编译时多态有了应用条件。与此同时在编译期使用dynamic_cast和typeid也是可行的。

    struct Box
    {
        double width{0.0};
        double height{0.0};
        double length{0.0};
    };
    
    struct Product
    {
        constexpr virtual ~Product() = default;
        constexpr virtual Box getBox() const noexcept = 0;
    };
    
    struct Notebook : public Product
    {
        constexpr ~Notebook() noexcept {};
        constexpr Box getBox() const noexcept override
        {
            return {.width = 30.0, .height = 2.0, .length = 30.0};
        }
    };
    
    struct Flower : public Product
    {
        constexpr Box getBox() const noexcept override
        {
            return {.width = 10.0, .height = 20.0, .length = 10.0};
        }
    };
    
    constexpr bool canFit(const Product &prod, const Box &minBox)
    {
        const auto box = prod.getBox();
        return box.width < minBox.width && box.height < minBox.height && box.length < minBox.length;
    }
    
    int main()
    {
        constexpr Notebook nb;
        constexpr Box minBox{100.0, 100.0, 100.0};
        static_assert(canFit(nb, minBox));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    允许try-catch

    C++20 constexpr函数虽然允许try-catch,但是这依然是有限制的,开发者不能在函数中throw异常。

    允许变更union活跃成员

    允许变更union活跃成员是C++20标准P1330R0提案,此提案的核心目标是允许在constexpr函数中重新指定union的当前有效成员。

    union Foo 
    {
    	int i;
    	float f;
    };
    constexpr int use() 
    {
    	Foo foo{};
    	foo.i = 3;
    	foo.f = 1.2f; // C++20之前不合法,C++20标准合法
    	return 1;
    }
    
    static_assert(use());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    consteval

    constexpr修饰函数仅表示支持在编译期求值(是否真的在编译期求值,不确定),但是在有些时候我们要求必须在编译期求值。这就是consteval引入的价值。consteval修饰函数,要求函数必须在编译期求值。

    consteval int min(std::initializer_list<int> array)
    {
        int low = std::numeric_limits<int>::max();
    
        for (auto& i : array)
        {
            if (i < low)
            {
                low = i;
            }
        }
    
        return low;
    }
    
    static_assert(min({ 1, 2, 3 }) == 1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    constinit

    constinit修饰变量,保证变量在编译期初始化。目标是为了解决 Static Initialization Order Fiasco文档,即相互影响的静态存储周期的变量之间,由于动态初始化的不确定性而导致的问题。

    • constinit不能和consteval/constexpr同时使用
    • constinit修饰引入对象,此时constinit与constexpr等价
    • 只能使用constexpr或consteval函数初始化constinit变量
    constexpr int sum(int n)
    {
        auto p = new int[n];
    
        std::iota(p, p + n, 1);
    
        auto t = std::accumulate(p, p + n, 0);
        
        delete[] p;
        return t;
    }
    
    consteval int min(std::initializer_list<int> array)
    {
        int low = std::numeric_limits<int>::max();
    
        for (auto& i : array)
        {
            if (i < low)
            {
                low = i;
            }
        }
    
        return low;
    }
    
    constinit auto g_min = min({ 1, 2 });
    constinit auto g_sum = sum(2);
    
    int main()
    {
        static_assert(min({ 1, 2, 3 }) == 1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
  • 相关阅读:
    系统运维利器,百万服务器运维实战总结!一文了解最新版SysAK|龙蜥技术
    ITSource 分享 第3期【在线个人网盘】
    读配置、讲原理、看面试真题,我只能帮你到这了。。。
    【【萌新的STM32学习-27--USART异步通信配置步骤】】
    js中的原型链
    弹性伸缩:高可用架构利器(架构+算法+思维)
    广东建筑覆膜板厂家
    【企业架构框架】TOGAF 10 现已发布并可用!
    MyBatis篇---第二篇
    公司电脑屏幕录制软件有什么功能
  • 原文地址:https://blog.csdn.net/liuguang841118/article/details/127754252