【问题标题】:Why do we need the two definitions: integral constant expression and converted constant expression?为什么我们需要两个定义:整型常量表达式和转换常量表达式?
【发布时间】:2016-03-25 10:31:06
【问题描述】:

C++14 中的§5.19/3 定义了一个整型常量表达式和一个转换后的常量表达式:

整数常量表达式是整数或 无作用域枚举类型,隐式转换为纯右值,其中 转换后的表达式是核心常量表达式。 [注:这样 表达式可以用作数组边界(8.3.4、5.3.4),作为位域 长度(9.6),如果基础类型是枚举器初始化器 不固定(7.2),并作为对齐(7.6.2)。 —尾注] A 已转换 T 类型的常量表达式是一个表达式,隐含 转换为T 类型的纯右值,其中转换后的表达式为 核心常量表达式和隐式转换序列 仅包含用户定义的转换,左值到右值的转换 (4.1)、积分促销 (4.5) 和积分转换 (4.7) 其他 而不是缩小转换范围 (8.5.4)。 [注:这样的表达可能是 用于new 表达式 (5.3.4),如 case 表达式 (6.4.2),如 如果基础类型是固定的(7.2),则枚举器初始化器,如 数组边界(8.3.4),并作为整数或枚举非类型模板 论据(14.3)。 ——尾注]

也许我遗漏了什么,但我的第一印象是每个整型常量表达式都是一个转换后的常量表达式

编辑

而且我也认为这一段有错误:

代替:

A converted constant expression of type T is an expression, implicitly converted to a prvalue of type T, ...

应该是:

A converted constant expression of type T is an expression, implicitly converted to a prvalue of an integral type, ...

并且这个改变允许编译以下代码:

#include <iostream>
struct A { operator int() { return 5; } } a;

int main() {
    int b[a]{ 0, 1, 2, 3, 4 };
    std::cout << b[4] << '\n';
}

其中int b[a]{ 0, 1, 2, 3, 4}; 声明中的aA 类型的转换常量表达式,隐式转换为整数类型(int) 的纯右值,其中转换后的表达式@987654332 @是核心常量表达式,隐式转换序列只包含用户定义的转换。

【问题讨论】:

  • 相反不是每个converted constant expression都是integral constant expression
  • “转换后的 T 类型常量表达式是一个表达式,隐式转换为整数类型的纯右值,”这明显是错误的。
  • 请注意,标准始终使用“std::size_t 的转换常量表达式”或“模板参数类型的转换常量表达式”,它从未说过“转换后的常量表达式,句号。”

标签: c++ language-lawyer c++14 constexpr


【解决方案1】:

这两个定义都是必需的,因为有些事情你可以用一个做,但不能用另一个做。不,不是每个整型常量表达式都是真正的转换常量表达式。对于明显的示例,已转换的常量表达式 禁止缩小转换范围,而整型常量表达式 则不会。

因此我不能这样做:

enum x : char { a = 1024 };

如果枚举的初始值设定项允许整型常量表达式,而不是转换后的常量表达式,那么这正是允许的。

作为维恩图,我将情况画成这样:

因此,两者之间有相当多的重叠(可能比此图所暗示的更多),但每个都至少允许一些其他不允许的事情。我已经给出了每个方向的一个项目的示例,但没有尝试详尽地列出差异。

尽管如此,我并不完全相信用户定义的转换被禁止用于整型常量表达式(并且快速测试表明我目前使用的编译器允许它们)。这将给出我最初写这个答案的情况,更像是这样:

【讨论】:

  • 为什么转换后的常量表达式比整型常量表达式更严格?转换后的常量表达式适用于具有用户定义转换的任何类型 T,显然不适用于整型常量表达式,后者仅限于整型或枚举类型。
  • @Ayrosa:我认为缺乏缩小转换比缺乏用户定义的转换是一个“更大”的限制,但最终你是对的,那个部分的答案可能具有误导性;我已将其替换为(我认为)更准确地描述情况的维恩图。
  • @JerryCoffin 但是为什么整数常量表达式不允许用户定义的转换?
  • @EugeneZavidovsky:至少在我阅读这篇文章时,integral constant expression 的定义基本上是说它必须具有以下形式:integral expression -> allowed conversion -> core constant expression。如果这是正确的,则用户定义的转换不能应用,因为它只能应用于用户定义的类型。转换后的常量表达式对转换顺序和结果施加了限制,所以它更像:whatever -> allowed conversion -> core constant expression
  • @EugeneZavidovsky:鉴于这将失败(但如果您将 operator int() 公开,至少 g++ 接受它)。我不完全确定它是否应该被允许。
【解决方案2】:

注意:此答案基于目前最新的标准草案,称为 N4567。指出了它与C++11/14标准的一些区别。

整数常量表达式转换常量表达式在类类型方面是不同的。在C++98/03中,当这里不能使用类类型时(因为当时还没有constexpr转换函数),确实没有类型为T的转换常量表达式 /em>。

对于一个整数常量表达式,目标类型是未知的。但是对于T 类型的转换后的常量表达式,已知目标类型为T,而T 不一定是整数或无范围枚举类型1.

所以,为了编译一个整型常量表达式,编译器首先需要确定目标类型是什么。如果表达式具有整数或非范围枚举类型,那么显然目标类型就是表达式的类型。否则,如果表达式具有字面量类类型(我们称这种类型为E),则使用以下过程2

编译器检查E3 中的所有非显式转换函数。假设这些函数的结果类型形成了一个集合S。如果S 恰好包含一个整数或无范围枚举类型(引用修饰符被剥离并且constvolatile 限定符被忽略:const volatile int&amp; 在此过程中被视为int),那么目标类型就是类型。否则判断失败。

(需要注意的是,在上述过程中,没有检查转换函数模板。)

因此,例如,如果一个类类型有两个转换函数,一个是constexpr operator int,另一个是constexpr operator long,那么这个类型就不能用在一个整型常量表达式中(目标类型无法确定)。但是,这种类型可以用在 int 类型的转换常量表达式long 类型的转换常量表达式中。

确定目标类型D后,应用重载决议寻找最合适的转换函数或函数模板,然后调用选择的转换函数(必须是constexpr)产生一个类型的值D。 — 这部分或多或少与 D 类型的转换常量表达式相同。

在以下示例中,Var{} 是一个有效的整型常量表达式,但它是一个无效的转换后的std::size_t 类型的常量表达式(该示例的灵感来自this question)。

class Var
{
public:

    constexpr operator int ()
    { return 42; }

    template <typename T>
    constexpr operator T () = delete;
};

enum {
    x = Var{} // the initializer of `x` is expected to be an 
              // integral constant expression
              // x has value 42
};

int t[ Var{} ]; // the array bound is expected to be a
                // converted constant expression of type std::size_t
                // this declaration is ill-formed

参考文献

N4567 5.20 [expr.const]p7

如果在需要整型常量表达式的上下文中使用文字类类型的表达式, 然后该表达式在上下文中被隐式转换(第 4 条)为整数或无范围枚举 类型和选择的转换函数应为constexpr

N4567 4[转化]p5

某些语言结构需要转换为具有适合结构的一组指定类型之一的值。出现在这种上下文中的类类型 E 的表达式 e 被称为上下文隐式转换为指定类型 T,并且当且仅当 e 可以被隐式转换为类型 T,其确定如下:E 搜索返回类型为 cv T 或引用 cv 的非显式转换函数em> T 使得 T 被上下文允许。应该有一个这样的T

注意事项

  1. 在 C++11/14 中,转换后的常量表达式只能是整数或枚举类型。 N4268 改变了这一点。
  2. 在 C++11 中,没有这样的过程,而是要求“类类型应具有单个非显式转换函数到整数或枚举类型,并且该转换函数应为 constexpr。 " N3323 将其更改为当前措辞。
  3. C++14 标准中不存在“非显式”一词。由CWG 1981添加。

【讨论】:

  • 但是为什么转换函数模板在这种情况下不参与重载决议呢?你知道,有 13.3.1\7。
  • @EugeneZavidovsky 他们确实参与了重载解决 AFAICS。他们只是不参与确定目的地类型的过程。实际上,在重载解析中仍然可以选择转换函数模板。
  • 哦...和我上面的对话有关。但是你知道吗?我想,我误解了 5.20\3 中的那些话:“隐式转换为纯右值”。它们暗示了 glvalue 到prvalue 的转换,而不是任何其他(用户定义的)转换,因为否则 7.2\5.1, 5.3, 7 将没有意义。所以 JerryCoffin 在他的回答中是正确的……它看起来像另一个编译器错误。 不应该有任何决定目的地类型的过程,你已经建议了!应始终指定目标类型。
  • @EugeneZavidovsky 见 5.20\7。
  • 哦...所以因为 5.20\7; 4\5 那么可能没有编译器错误。但是请注意,如果我理解正确的话,无论如何都必须指定目的地类型。在那个例子中应该是std::size_t,不是吗?
【解决方案3】:

在讨论了 Jerry Coffin 和 cpplearner 提供的答案后,让我提议重写那些该死的规则,如下所示:

[expr.const] 5.20\3(修改)

整型常量表达式是整型或无范围枚举类型的表达式,它被隐式转换为相同类型的纯右值核心常量表达式,这样隐式转换序列只包含一个左值到-rvalue 转换。

[expr.const] 5.20\4(修改)

T 类型的转换后的常量表达式是任意表达式 类型,即隐式转换为 T 类型的常量表达式 这样隐式转换序列只包含:

  • 用户定义的转化,
  • 左值到右值的转换,
  • 数组到指针的转换,
  • 函数到指针的转换,
  • 资格转换,
  • 综合促销,
  • 除缩小转化之外的整体转化,
  • 来自std::nullptr_t的空指针转换,
  • 来自std::nullptr_t的空成员指针转换,和
  • 函数指针转换,

以及引用绑定(如果有)直接绑定的位置。 [注:这样 表达式可以用在new 表达式中,作为 case 表达式,如 如果基础类型是固定的,则枚举器初始化器,如数组 边界,并作为非类型模板参数。 ——尾注]

现在区别很明显了,嗯?另外需要提醒的是,按照5.20\7; 4\5 在某些情况下,可以使用文字类类型的表达式代替整型常量表达式。

【讨论】:

    猜你喜欢
    • 2016-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多