在constexpr
函数中,我们介绍了 constexpr
函数,这些函数可以在编译时或运行时进行求值。例如:
#include <iostream>
constexpr int greater(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
std::cout << greater(5, 6) << '\n'; // greater(5, 6) 可能在编译时或运行时求值
constexpr int g { greater(5, 6) }; // greater(5, 6) 必须在编译时求值
std::cout << g << '\n'; // 输出 6
return 0;
}
在这个例子中,greater()
是一个 constexpr
函数,而 greater(5, 6)
是一个常量表达式,它可以在编译时或运行时求值。因为 std::cout << greater(5, 6)
在非 constexpr
上下文中调用了 greater(5, 6)
,所以编译器可以选择在编译时或运行时对 greater(5, 6)
进行求值。当 greater(5, 6)
用于初始化 constexpr
变量 g
时,greater(5, 6)
在 constexpr
上下文中被调用,因此必须在编译时求值。
现在考虑以下类似的例子:
#include <iostream>
struct Pair
{
int m_x {};
int m_y {};
int greater() const
{
return (m_x > m_y ? m_x : m_y);
}
};
int main()
{
Pair p { 5, 6 }; // 输入值是 `constexpr` 值
std::cout << p.greater() << '\n'; // p.greater() 在运行时求值
constexpr int g { p.greater() }; // 编译错误:greater() 不是 `constexpr`
std::cout << g << '\n';
return 0;
}
在这个版本中,我们有一个名为 Pair
的聚合结构体,而 greater()
现在是一个成员函数。然而,由于成员函数 greater()
不是 constexpr
,p.greater()
不是一个常量表达式。当 std::cout << p.greater()
在非 constexpr
上下文中调用 p.greater()
时,p.greater()
将在运行时求值。然而,当我们尝试使用 p.greater()
来初始化 constexpr
变量 g
时,我们得到了一个编译错误,因为 p.greater()
不能在编译时求值。
由于 p
的输入值是 constexpr
值(5 和 6),似乎 p.greater()
应该能够在编译时求值。但我们该如何做到这一点呢?
常量表达式成员函数
与非成员函数一样,成员函数也可以通过使用 constexpr
关键字来声明为常量表达式函数。常量表达式成员函数可以在编译时或运行时求值。
#include <iostream>
struct Pair
{
int m_x {};
int m_y {};
constexpr int greater() const // 可在编译时或运行时求值
{
return (m_x > m_y ? m_x : m_y);
}
};
int main()
{
Pair p { 5, 6 };
std::cout << p.greater() << '\n'; // 没问题:p.greater() 在运行时求值
constexpr int g { p.greater() }; // 编译错误:p 不是 `constexpr`
std::cout << g << '\n';
return 0;
}
在这个例子中,我们将 greater()
声明为 constexpr
函数,因此编译器可以在运行时或编译时对其进行求值。
当我们在运行时表达式 std::cout << p.greater()
中调用 p.greater()
时,它在运行时求值。
然而,当 p.greater()
用于初始化 constexpr
变量 g
时,我们得到了一个编译错误。尽管 greater()
现在是 constexpr
,但 p
仍然不是 constexpr
,因此 p.greater()
不是一个常量表达式。
常量表达式聚合类型
好吧,既然我们需要 p
是 constexpr
,那么我们干脆把它声明为 constexpr
:
#include <iostream>
struct Pair // Pair 是一个聚合类型
{
int m_x {};
int m_y {};
constexpr int greater() const
{
return (m_x > m_y ? m_x : m_y);
}
};
int main()
{
constexpr Pair p { 5, 6 }; // 现在是 `constexpr`
std::cout << p.greater() << '\n'; // p.greater() 在运行时或编译时求值
constexpr int g { p.greater() }; // p.greater() 必须在编译时求值
std::cout << g << '\n';
return 0;
}
由于 Pair
是一个聚合类型,而聚合类型隐式支持 constexpr
,因此我们完成了。这可以正常工作!由于 p
是一个 constexpr
类型,且 greater()
是一个 constexpr
成员函数,因此 p.greater()
是一个常量表达式,可以在只允许常量表达式的地方使用。
相关内容
我们在第 13.8 课——结构体聚合初始化中介绍了聚合类型。
常量表达式类对象和常量表达式构造函数
现在让我们把 Pair
变成一个非聚合类型:
#include <iostream>
class Pair // Pair 不再是聚合类型
{
private:
int m_x {};
int m_y {};
public:
Pair(int x, int y): m_x { x }, m_y { y } {}
constexpr int greater() const
{
return (m_x > m_y ? m_x : m_y);
}
};
int main()
{
constexpr Pair p { 5, 6 }; // 编译错误:p 不是字面量类型
std::cout << p.greater() << '\n';
constexpr int g { p.greater() };
std::cout << g << '\n';
return 0;
}
这个例子几乎和前面的例子一模一样,只是 Pair
不再是聚合类型(因为它有私有数据成员和构造函数)。
当我们编译这个程序时,我们得到了一个关于 Pair
不是“字面量类型”的编译错误。这是什么情况?
在 C++ 中,字面量类型是任何可能在常量表达式中创建对象的类型。换句话说,除非类型符合字面量类型,否则对象不能是 constexpr
。而我们的非聚合 Pair
不符合字面量类型。
术语
字面量和字面量类型是不同的(但相关)概念。字面量是一个插入到源代码中的 constexpr
值。字面量类型是可以用作 constexpr
值类型的类型。字面量总是具有字面量类型。然而,具有字面量类型的值或对象不必是字面量。
字面量类型的定义较为复杂,可以在 cppreference 上找到总结。但值得一提的是,字面量类型包括:
- 标量类型(那些持有单个值的类型,例如基本类型和指针)
- 引用类型
- 大多数聚合类型
- 具有
constexpr
构造函数的类
现在我们明白了为什么我们的 Pair
不是字面量类型。当类对象被实例化时,编译器会调用构造函数来初始化对象。而我们 Pair
类中的构造函数不是 constexpr
,因此它不能在编译时被调用。因此,Pair
对象不能是 constexpr
。
解决方法很简单:我们只需将构造函数也声明为 constexpr
:
#include <iostream>
class Pair
{
private:
int m_x {};
int m_y {};
public:
constexpr Pair(int x, int y): m_x { x }, m_y { y } {} // 现在是 `constexpr`
constexpr int greater() const
{
return (m_x > m_y ? m_x : m_y);
}
};
int main()
{
constexpr Pair p { 5, 6 };
std::cout << p.greater() << '\n';
constexpr int g { p.greater() };
std::cout << g << '\n';
return 0;
}
这按预期工作,就像我们聚合版本的 Pair
一样。
最佳实践
如果你希望你的