14-17 Constexpr 聚合和类
在第F.1课——常量表达式函数中,我们介绍了常量表达式函数,这类函数可在编译时或运行时进行求值。例如:
#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) may be evaluated at compile-time or runtime
constexpr int g { greater(5, 6) }; // greater(5, 6) must be evaluated at compile-time
std::cout << g << '\n'; // prints 6
return 0;
}

在此示例中,greater() 是 constexpr 函数,而 greater(5, 6) 是常量表达式,可在编译时或运行时求值。由于 std::cout << greater(5, 6) 在非 constexpr 上下文中调用 greater(5, 6),编译器可自由选择在编译时或运行时求值 greater(5, 6)。当 greater(5, 6) 用于初始化常量表达式变量 g 时,该调用处于常量表达式上下文中,必须在编译时进行求值。
现在考虑以下类似示例:
#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 }; // inputs are constexpr values
std::cout << p.greater() << '\n'; // p.greater() evaluates at runtime
constexpr int g { p.greater() }; // compile error: greater() not constexpr
std::cout << g << '\n';
return 0;
}

在此版本中,我们定义了一个名为Pair的聚合结构体,其中greater()已成为成员函数。然而,由于成员函数greater()并非constexpr,p.greater()便不属于常量表达式。当std::cout << p.greater()调用p.greater()时(在非constexpr上下文中),p.greater()将在运行时进行求值。然而当尝试用 p.greater() 初始化常量表达式变量 g 时,会因 p.greater() 无法在编译时求值而引发编译错误。
由于 p 的输入参数(5 和 6)均为常量表达式值,p.greater() 似乎应当支持编译时求值。但具体该如何实现呢?
常量表达式成员函数
与非成员函数类似,成员函数可通过使用 constexpr 关键字实现常量表达式特性。常量表达式成员函数可在编译时或运行时进行求值。
#include <iostream>
struct Pair
{
int m_x {};
int m_y {};
constexpr int greater() const // can evaluate at either compile-time or runtime
{
return (m_x > m_y ? m_x : m_y);
}
};
int main()
{
Pair p { 5, 6 };
std::cout << p.greater() << '\n'; // okay: p.greater() evaluates at runtime
constexpr int g { p.greater() }; // compile error: p not constexpr
std::cout << g << '\n';
return 0;
}

在此示例中,我们将 greater() 声明为 constexpr 函数,因此编译器可在运行时或编译时对其进行求值。
当我们在运行时表达式中调用 p.greater()(如 std::cout << p.greater())时,它会在运行时进行求值。
然而,当使用 p.greater() 初始化常量表达式变量 g 时,会引发编译器错误。尽管 greater() 现为常量表达式,但 p 仍非常量表达式,因此 p.greater() 并非常量表达式。
常量表达式聚合体 Constexpr aggregates
好的,既然需要 p 成为常量表达式,那就直接将其定义为常量表达式:
#include <iostream>
struct Pair // Pair is an aggregate
{
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 }; // now constexpr
std::cout << p.greater() << '\n'; // p.greater() evaluates at runtime or compile-time
constexpr int g { p.greater() }; // p.greater() must evaluate at compile-time
std::cout << g << '\n';
return 0;
}

由于 Pair 是聚合类型,而聚合类型隐式支持 constexpr,至此完成。这确实可行!由于 p 是 constexpr 类型,且 greater() 是 constexpr 成员函数,p.greater() 属于常量表达式,可在仅允许常量表达式的位置使用。
相关内容:
我们在第 13.8 课——结构体聚合初始化中探讨过聚合类型。
常量构造类对象与常量构造函数
现在让我们将Pair改为非聚合类型:
#include <iostream>
class Pair // Pair is no longer an aggregate
{
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 }; // compile error: p is not a literal type
std::cout << p.greater() << '\n';
constexpr int g { p.greater() };
std::cout << g << '\n';
return 0;
}

这个示例与前一个几乎完全相同,区别在于Pair不再是聚合类型(因为它包含私有数据成员和构造函数)。
编译此程序时,会出现编译器错误,指出Pair不是“字面量类型”。这是什么情况?
在C++中,字面量类型literal type是指任何可能在常量表达式中创建对象的类型。换言之,除非类型符合字面量类型,否则对象不能成为 constexpr。而我们的非聚合 Pair 不符合条件。
术语说明:
字面量literal与字面量类型literal type是两个不同(但相关)的概念。字面量是插入源代码中的 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 } {} // now 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一样。
最佳实践:
若需使类在编译时可评估,请将成员函数和构造函数声明为 constexpr。
隐式定义的构造函数若符合条件则默认为 constexpr,显式默认构造函数必须显式声明为 constexpr。
提示:
constexpr 是类接口的组成部分,后期移除会破坏在常量上下文中调用该函数的调用方。
常量表达式Constexpr成员可能需要与非常量表达式non-constexpr/非常量non-const对象配合使用
在上例中,由于常量表达式变量 g 的初始化器必须是常量表达式,显然 p.greater() 必须是常量表达式,因此 p、Pair 构造函数和 greater() 都必须是常量表达式。
然而,若将 p.greater() 替换为常量表达式函数,情况就变得不太明显:
#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 int greater() const
{
return (m_x > m_y ? m_x : m_y);
}
};
constexpr int init()
{
Pair p { 5, 6 }; // requires constructor to be constexpr when evaluated at compile-time
return p.greater(); // requires greater() to be constexpr when evaluated at compile-time
}
int main()
{
constexpr int g { init() }; // init() evaluated in compile-time context
std::cout << g << '\n';
return 0;
}

请记住,constexpr 函数既可在运行时也可在编译时求值。当 constexpr 函数在编译时求值时,它只能调用能够在编译时求值的函数。对于类类型而言,这意味着只能调用 constexpr 成员函数。
由于 g 是 constexpr,init() 必须在编译时求值。在 init() 函数内部,我们将 p 定义为非 constexpr/非 const(因为可以这样做,而非必须如此)。尽管 p 未被定义为 constexpr,但它仍需在编译时创建,因此需要一个 constexpr Pair 构造函数。同样地,为使 p.greater() 在编译时求值,greater() 必须是 constexpr 成员函数。若 Pair 构造函数或 greater() 其中任一不是 constexpr,编译器将报错。
关键要点:
当 constexpr 函数在编译时上下文中求值时,仅能调用 constexpr 函数。
constexpr 成员函数可为 const 或非 const(C++14)
在 C++11 中,非静态的 constexpr 成员函数默认为 const(构造函数除外)。
然而,从 C++14 开始,constexpr 成员函数不再默认为 const。这意味着若需将 constexpr 函数设为 const 函数,必须显式标记其 const 属性。
constexpr 非 const 成员函数可修改数据成员(可选)
只要隐式对象不是 const,constexpr 非 const 成员函数即可修改类的数据成员。即使函数在编译时评估,此特性依然成立。
以下是一个刻意设计的示例:
#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 int greater() const // constexpr and const
{
return (m_x > m_y ? m_x : m_y);
}
constexpr void reset() // constexpr but non-const
{
m_x = m_y = 0; // non-const member function can change members
}
constexpr const int& getX() const { return m_x; }
};
// This function is constexpr
constexpr Pair zero()
{
Pair p { 1, 2 }; // p is non-const
p.reset(); // okay to call non-const member function on non-const object
return p;
}
int main()
{
Pair p1 { 3, 4 };
p1.reset(); // okay to call non-const member function on non-const object
std::cout << p1.getX() << '\n'; // prints 0
Pair p2 { zero() }; // zero() will be evaluated at runtime
p2.reset(); // okay to call non-const member function on non-const object
std::cout << p2.getX() << '\n'; // prints 0
constexpr Pair p3 { zero() }; // zero() will be evaluated at compile-time
// p3.reset(); // Compile error: can't call non-const member function on const object
std::cout << p3.getX() << '\n'; // prints 0
return 0;
}

在分析这个示例时请记住:
- 非 const 成员函数可以修改非 const 对象的成员。
- constexpr 成员函数既可在运行时上下文中调用,也可在编译时上下文中调用。
这两种情况独立运作。
对于 p1,由于其本身是非 const 对象,因此允许调用非 const 成员函数 p1.reset() 来修改 p1。reset() 是否为 constexpr 在此无关紧要,因为当前操作无需编译时评估。
p2 的情况类似。此时 p2 的初始化器是对 zero() 的函数调用。尽管 zero() 是 constexpr 函数,但在运行时上下文中调用时,其行为与普通函数无异。在 zero() 内部,我们实例化非 const 对象 p,调用其非 const 成员函数 p.reset(),然后返回 p。返回的 Pair 对象被用作 p2 的初始化器。此时 zero() 和 reset() 是否为 constexpr 并不重要,因为我们所做的操作都不需要编译时评估。
p3的情况则颇具趣味性。由于p3是constexpr,其初始化器必须为常量表达式。因此对zero()的调用必须在编译时完成。而在编译时上下文中,我们只能调用constexpr函数。在zero()内部,p是非const的(尽管在编译时评估,这仍是允许的)。然而在编译时上下文中,用于创建 p 的构造函数必须是 constexpr。与 p2 情况类似,我们允许对非 const 对象 p 调用非 const 成员函数 p.reset()。但由于处于编译时上下文,reset() 成员函数必须是 constexpr。该函数返回 p,用于初始化 p3。
作者注:
是的,我们使用非const对象初始化了constexpr对象。若此逻辑令你困惑,很可能是尚未完全区分const与constexpr的概念。constexpr 变量初始化并不强制要求使用 const 值。之所以看似如此,是因为我们通常用常量字面量(const)或其它 constexpr 变量(隐式 const)初始化 constexpr 变量,且 const 与 constexpr 名称相似。
实际要求是:constexpr 变量必须用常量表达式初始化。对于函数(及运算符),constexpr 不等同于 const,constexpr 函数(及运算符)可使用非 const 对象,甚至返回非 const 对象。
关键不在于 const,而在于编译器能否在编译时确定对象的值。对于 constexpr 函数而言,即使返回非 const 对象,编译器仍能实现这一点!
返回 const 引用(或指针)的 Constexpr 函数 可选
通常,您不会看到 constexpr 和 const 紧邻使用,但确实会发生这种情况的一种情况是,当您有一个返回 const 引用(或指向 const 的 (const) 指针)的 constexpr 成员函数时。
在上面的 Pair 类中,getX() 是一个 constexpr 成员函数,它返回一个 const 引用:
constexpr const int& getX() const { return m_x; }
这是很多常量!
constexpr 指示成员函数可以在编译时求值。 const int& 是函数的返回类型。最右边的 const 意味着成员函数本身是 const,因此可以在 const 对象上调用它。
顺便说一句……
返回指向 const 的 const 指针的成员函数可能如下所示:
constexpr const int* const getXPtr() const { return &m_x; }
是不是很漂亮?不?是,好吧。

浙公网安备 33010602011771号