C++之父Bjarne Stroustrup和Gabriel Dos Reis于2003年向C++标准委员会提出了一种用于编译期求值的更好的机制。他们的目标:
2003年提议实现:允许在常量表达式中使用以constexpr为前缀的函数,还允许常量表达式使用简单的用户自定义类型,即字面值常量,这些字面值常量是一种所有运算都是constexpr的类型。
C++标准委员会从C++11开始支持Bjarne Stroustrup和Gabriel Dos Reis的提议:
到C++20对constexpr的完整支持,constexpr标准化跨越了13年,由4个标准实现。C++20也是最接近最初语言设计模板的版本,但是constexpr 函数的设计其实也不够严谨,所以 C++20 又引入了 consteval;由于constexpr仅能实现编译时常量求值,为了解决编译时非常量求值问题,C++20又引入了constinit关键字。
具备下述条件的函数,我们称之为纯函数
- 函数无法访问非本地对象
- 函数不能对调用者的环境产生副作用
C++11中的constexpr可修饰变量亦可修饰函数。但是无论是修饰变量还是函数,都要求其必须能在编译期常量求值。
C++11标准规定,满足下述条件的变量,可声明为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必须是常量函数,否则此声明非法。
对应自定义类型,不能用constexpr 直接修饰类型,例如:
constexpr struct Rectangle // constexpr 修饰自定义类型无效
{
int length;
int width;
};
如果要定一个结构体/或类常量对象,可采用这样的实现模式,例如:
struct Rectangle
{
int length;
int width;
};
constexpr Rectangle rect{1, 2};
constexpr int length = rect.length;
constexpr int width = rect.width;
如果一个指针声明为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;
}
代码执行结果
i=1
i=8
C++11标准规定,满足下述条件的函数,可声明为constexpr函数:
对于 constexpr 函数模板和类模板的 constexpr 函数成员,必须至少有一个特化满足上述要求。
// 此斐波那契数列实现的复杂度等同于迭代的方法,基本上为O(n)。
constexpr long int fibonacci(int n)
{
return (n <= 1)? n : fibonacci(n-1) + fibonacci(n-2); //只能包含一个retrun语句
}
constexpr int DAYS_OF_YEAR = 365;
constexpr int daysOfYear()
{
return DAYS_OF_YEAR;
}
constexpr int hoursOfYear()
{
return 24 * daysOfYear();
}
// 例 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 };
// 例 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;
}
C++11标准中,constexpr 修饰函数除了可以包含 using 指令、typedef 语句以及 static_assert 断⾔ 外,只能包含⼀条 return 语句。而C++14标准允许 constexpr函数使用局部变量,同时实现对其他函数的支持,所以constexpr 修饰的函数可包含 if/switch 等条件语句,也可包含 for 循环。
虽然C++14放开了很多限制,但是依然存在部分C++11存在的严格限制:
所以在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;
}
}
常量模板是变量模板的一种特别形式,常量模板的表示常量的类型是模板,但是常量的值是const属性。我们可以利用constexpr 实现常量模板。
template<typename T = long double>
constexpr T pi = T{3.1415926535897932385};
C++17对C++14标准增加了2个扩展:(一)将 constexpr 这个关键字引⼊到 if 语句;(二)将 constexpr 与Lambda Expression结合。
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
基于静态if,获取变量数值的模板函数举例:
template <typename T>
auto getValue(T t)
{
if constexpr (std::is_pointer_v<T>)
{
return *t;
}
else
{
return t;
}
}
C++17标准允许lambda expression在编译期求值,但是constexpr lambda expression也准寻下述C++17标准:
int y = 32;
auto answer = [y]() constexpr
{
int x = 10;
return y + x;
};
constexpr int increment(int n)
{
return [n] { return n + 1; }();
}
auto answer = [](int n)
{
return 32 + n;
};
constexpr int response = answer(10);
auto increment = [](int n)
{
return n + 1;
};
constexpr int(*inc)(int) = increment;
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对此做出了重大调整。
template <size_t N>
constexpr int f() {...}
template <float val>
constexpr float g() {...}
template <auto ...>
struct ArgList
{
};
ArgList<'C', 0, 2L, nullptr> argList;
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();
}
一个可保障非类型字符串模板参数正常工作的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;
}
C++20编译时内存分配,constexpr函数可以进行有限制的动态内存分配和和使用std::vector/std::string。C++20之前只有std::array可以在编译期使用。当然这依然是有限制的使用:
例如,constexpr helloWorld的定义就是C++20标准不允许的。:
constexpr auto helloWorld()
{
std::string s1{"hello "};
std::string s2{"world"};
std::string s3 = s1 + s2;
return s3;
}
但是仅函数内部使用 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);
// 例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);
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));
}
C++20 constexpr函数虽然允许try-catch,但是这依然是有限制的,开发者不能在函数中throw异常。
允许变更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());
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);
constinit修饰变量,保证变量在编译期初始化。目标是为了解决 Static Initialization Order Fiasco文档,即相互影响的静态存储周期的变量之间,由于动态初始化的不确定性而导致的问题。
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;
}