我想知道为什么conexpr函数(尤其是std::size)在一些非const上下文中不能工作,而只有类型很重要。
让我们来看看两个array_size的实现。
template <typename T, size_t N>
char (&array_size_helper(const T (&)[N]))[N];
#define array_size(a) sizeof(array_size_helper(a))
std::size
(GCC-8的实施)constexpr size_t size(const _Tp (&/*__array*/)[_Nm]) noexcept { return _Nm; }
第二个版本很好很完美,只是和第一个版本的工作方式不一样。由于第一个宏与 sizeof
它只关心类型,而 constexpr
函数是很复杂的事情。
考虑一个例子。
struct A
{
int a[10];
};
template <typename T, size_t N>
char (&array_size_helper(const T (&)[N]))[N];
# define array_size(a) sizeof(array_size_helper(a))
int main()
{
A a;
A* new_A = reinterpret_cast<A*>(&a);
static_assert(array_size(a.a) == 10) // OK;
static_assert(array_size(new_A->a) == 10); //OK
static_assert(std::size(a.a) == 10); //OK
static_assert(std::size(new_A->a) == 10); //error: the value of ‘new_A’ is not usable in a constant expression
}
为什么会这样?为什么 std::size
除了类型,还在乎什么?是不是应该重新实现?
我写了一个 整篇博文 关于这个。不知道 std::size
不应该被重新实现。
这两种实现之间有一个重要的区别:在使用 array_size()
,所有的东西都是在一个未评估的上下文中。只有类型是重要的,而不是任何特定的值。array_size()
适用于任何 C 数组类型,而不适用其他类型。
std::size()
另一方面,它适用于所有的范围。但它 有 来评估它的参数。而当我们在做恒定的评估时,我们有一套严格的规则必须遵循。其中一条就是未定义的行为是不规范的--编译器必须跟踪每一次这样的访问。所以当你读过一个指针或引用时,编译器必须验证这个读是有效的。这是很奇怪的表情 std::size(a.a)
工作但 std::size(new_A->a)
没有,但要考虑这两种情况下必须发生的不同操作。
std::size(a.a)
,我们永远不会有看看 a
. 成员访问只是一些偏移量。我们将一个引用捆绑到这个偏移量上(参数为 std::size
),但实施 size()
其实从来没有读过这个提法。所以即使 a
本身并不 可读 在一个常量表达式中,我们实际上并没有对它进行任何读取--所以这只是工作。std::size(new_A->a)
的值,我们首先要做的是读取 new_A
以便执行该dereference。但是 new_A
不是一个常量,所以我们不能在常量评估时读取它的值,所以我们已经完成了。这并不重要,我们甚至不需要我们读到的值,在这种情况下我们只关心它的类型。这是目前的一个基本限制--在静态大小的范围内,你需要一个类型特征(或宏)来获取它们作为常量表达式的大小,而动态大小的范围,你需要依靠 std::size()
.