编译期计算
  ZXL8ALkrtBPG 2023年11月02日 34 0
cpp

模板元函数基本概念

支持在编译期调用并完成计算的函数即称为模板元函数,由于是在编译期完成,进而改善运行时的性能。元函数实际上即为C++中的一个模板类。

元函数的通常形式为:

template<typename T, typename Ts>                         // 元函数列表
struct MetaFunction {                                     // 元函数名
    using Type = some-type-define;                        // 返回的类型元数据
    static constexpr int value = some-comile-time-cal     // 返回的数值元数据
};

和函数调用一样,元函数调用也有形参、实参的概念,即template parameter 和template argument;

数值元函数

编译期计算斐波那契数列的第N项

template<size_t N>
struct Fibonacci {
    static const size_t value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};

template<>
struct Fibonacci<0> {
    static const size_t value = 0;
};
static_assert(Fibonacci<10>::value == 55);

其中Fibonacci即为一个数值元函数,在编译期计算值;

类型元函数

类型元函数在模板泛型编程中经常遇到,例如std::remove_reference:

template<class T> struct remove_reference { typedef T type; };
template<class T> struct remove_reference<T&> { typedef T type; };
template<class T> struct remove_reference<T&&> { typedef T type; };

混合元编程

这里混合:可理解为利用模板元函数在编译期生成代码,并在运行期执行生成后的代码;引用【3】中的代码为例子:

template<typename T,std::size_t N>
struct DotProductT
{
    static inline T result(const T* a, const T* b)
    {
        return (*a)*(*b)+DotProductT<T, N-1>::result(a + 1, b + 1);
    }
};
template<typename T>
struct DotProductT<T,0>
{
    static inline T result(const T*, const T*)
    {
        return T{};
    }
};

template<typename T,std::size_t N>
auto DotProductProcess(std::array<T,N> const& x,std::array<T,N> const& y)
{
    return DotProductT<T,N>::result(x.begin(),y.begin());
}
std::array<int, 5> a{1, 2, 3, 4, 5};
    DotProductProcess(a, a);

constexpr

constexpr在c++11引入,并在c++14和c++17中得到增强;经过constexpr声明的对象,和const修饰的对象一样,具有只读属性,并且变量作为编译器常量,但const对象不一定在编译期来初始化;

即所有constexpr对象都是const对象,而并非所有的const对象都是constexpr对象【2】;

1)constexpr修饰的变量【1】

1.its type must be a LiteralType. // 字面类型
2.it must be immediately initialized
3. the full-expression of its initialization, including all implicit conversions, constructors calls, etc, must be a constant expression // 常量表达式

自定义constexpr类型:

struct Dot {
    constexpr Dot(int val) : m_val(val) { };
    constexpr ~Dot() = default;
    constexpr int GetVal() const
    {
        return m_val;
    }
    constexpr int SetVal(int val)
    {
        return m_val = val;
    }
private:
    int m_val;
};

constexpr Dot d(5); // compile OK

int val = 5;
constexpr Dot d(val); // compile error:初始化非常量表达式

int val = 5;
Dot d(val); // compile OK

3)constexpr修饰的静态成员变量,在C++17中自带inline属性,因此不需要再进行类外定义;

4)constexpr修饰的函数

当函数入参为常量时则在编译期完成计算;而入参为运行时变量则退化为普通函数;

constexpr int Add(int a, int b)
{
    return a + b;
}
// 编译期
static_assert(Add(10, 10) == 20); 
constexpr int result = Add(10, 10);

// 运行期
int a = 10;
static_assert(Add(a, a) == 20);

在C++11中,constexpr有较多的约束:

// C++11 constexpr functions use recursion rather than iteration,支持递归但不支持迭代
// in C++11, they must do so from the conditional operator ?:
// C++11 constexpr functions had to put everything in a single return statement

C++14进行了增强改进,constexpr函数内支持for循环迭代和函数临时变量:

constexpr int Add(int a, int b)
{
    int result = 0;
    for (int tmp = a; tmp <= b; tmp++) {
        result += tmp;
    }
    return result;
}

C++17则支持编译器if 判断:

constexpr bool Compare(int a)
{
    if constexpr (sizeof(a) < sizeof(char)) {
        return true;
    }
    return false;
}

C++20开始vector/string可以在constexpr函数中编译期使用;

通过constexpr可以改造Fibonacci数列计算,性能上相比模板元函数更具优势(减少了模板实例化);

constexpr int Fibonacci(int n)
{
    if (n == 0 || n == 1) {
        return n;
    } else {
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
}

注:在【2】中条款15:只要有可能使用constexpr,就使用它

consteval and constinit

consteval

通过constexpr修饰的函数在上一节中介绍既可以在编译期也可以在运行期运行;

consteval为C++20引入,其声明函数或函数模板为立即函数 ,即函数只支持在编译期运行,同时隐式声明为inline;

consteval int Add(int a, int b)
{
    return a + b;
}

// 编译期
static_assert(Add(10, 10) == 20); 
constexpr int result = Add(10, 10);

// 运行期
int a = 10;
Add(a, a);  // 编译失败,consteval修饰的函数不支持运行期,非常量表达式

constinit

constinit用于显式地指定变量的初始化方式为常量表达式 , 生命周期必须为静态生命周期或thread_local 生命周期 ;同时constinit不具有只读属性,constexpr/const具有只读属性;

constinit int a = 10;
void Process()
{
    static constinit int a = 10;
    static constinit int b = Add(10, 10);
    b = 20; // 不具有只读属性
    constinit thread_local int c = 20;
}

枚举值与静态常量

在上文数值元函数中使用const修饰静态常量,此时类内常量在运行时并没有分配内存空间,简化例子为:

struct Test {
    static const size_t value = 10;
};

void Process(const size_t&)
{
}

static_assert(Test::value == 10); // compile:OK
Process(Test::value); // compile error:undefined reference to `Test::value'

Process函数形参为左值引用,因此需要Test::value中的地址,此时则需要内存并且进行类外定义:

const size_t Test::value;

或者添加inline修饰:

struct Test {
    static inline const size_t value = 10;
};

声明为 constexpr 的静态成员变量是隐式的内联变量 ,因此使用constexpr修饰也可以解决;

注:不同编译器现象可能有差别,在cppinsights 测试即使不用inline声明也可以获取左值地址

类内常量也可以修改为枚举:

struct Test {
    enum { value = 10 };
};

void Process(const size_t&)
{
}

static_assert(Test::value == 10);
Process(Test::value);

枚举值不是左值,因此不存在上述所说的地址问题,但是局限于integral types,而constexpr并没有此局限性;

参考资料

【1】https://en.cppreference.com/w/cpp/language/constexpr

【2】Effective ModernC++

【3】C++ templates

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  ZXL8ALkrtBPG   2023年11月02日   50   0   0 cpp
  PQYWJLBjS0G7   2023年11月02日   49   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   69   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   40   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   39   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   48   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   83   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   35   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   29   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   36   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   18   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   59   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   70   0   0 Listcpp
  ZXL8ALkrtBPG   2023年11月02日   47   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   69   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   31   0   0 cpp
ZXL8ALkrtBPG