关于C++:什么是rValk、LValk、XValuy、GLUVE和PROVALL?

What are rvalues, lvalues, xvalues, glvalues, and prvalues?

在C++ 03中,表达式是rValor或LValk。

在C++ 11中,表达式可以是:

  • 价值
  • 左值
  • X值
  • GLUVE
  • PR值
  • 两大类已成为五大类。

    • 这些新的表达式类别是什么?
    • 这些新类别如何与现有的右值和左值类别相关?
    • C++0X中的RValk和LValk类与C++ 03中的值相同吗?
    • 为什么需要这些新类别?wg21诸神只是想迷惑我们这些凡人吗?


    我想这份文件可以作为一个不那么简短的介绍:N3055

    整个大屠杀都是从移动语义学开始的。一旦我们有了可以移动而不可以复制的表达式,突然间容易掌握的规则就要求区分可以移动的表达式和方向。

    根据我的猜测,基于草案,R/L值的区别保持不变,只有在移动的情况下才会变得混乱。

    他们需要吗?如果我们想放弃新功能,可能不会。但为了实现更好的优化,我们可能应该接受它们。

    引用N3055:

    • 一个左值(所谓的,历史上的,因为lvalues可能出现在作业的左侧表达式)指定函数或物体。[示例:如果E是指针类型的表达式,然后是*E。是引用的左值表达式E的对象或功能。点。另一个例子是调用函数的结果返回类型是左值引用是一个左值
    • 一个xValm"到期"值)也指物体,通常在其末端附近终身(以便其资源例如,移动)。一个x值是某些类型的结果涉及右值的表达式参考文献。[例子:调用函数的结果返回类型是右值引用是一个xValue.
    • glvalue("广义"lvalue)是lvalue或一个xValk。
    • 一个右值(所谓,历史上,因为价值观出现在赋值表达式)是xvalue,临时物体或子对象,或不与对象关联。
    • 一prvalue("pure"rvvalue)是一个rvvalue这不是XValue。[例子:调用函数的结果返回类型不是引用是PR值

    所讨论的文档对于这个问题是一个很好的参考,因为它显示了引入新术语后标准发生的确切变化。


    What are these new categories of expressions?

    FCD(N3092)有一个很好的描述:

    — An lvalue (so called, historically, because lvalues could appear on the
    left-hand side of an assignment
    expression) designates a function or
    an object. [ Example: If E is an
    expression of pointer type, then
    *E is an lvalue expression referring to the object or function to which E
    points. As another example, the result
    of calling a function whose return
    type is an lvalue reference is an
    lvalue. —end example ]

    — An xvalue (an
    "eXpiring" value) also refers to an
    object, usually near the end of its
    lifetime (so that its resources may be
    moved, for example). An xvalue is the
    result of certain kinds of expressions
    involving rvalue references (8.3.2). [
    Example: The result of calling a
    function whose return type is an
    rvalue reference is an xvalue. —end
    example ]

    — A glvalue ("generalized"
    lvalue) is an lvalue or an xvalue.


    An rvalue (so called, historically,
    because rvalues could appear on the
    right-hand side of an assignment
    expressions) is an xvalue, a temporary
    object (12.2) or subobject thereof, or
    a value that is not associated with an
    object.

    — A prvalue ("pure" rvalue) is
    an rvalue that is not an xvalue. [
    Example: The result of calling a
    function whose return type is not a
    reference is a prvalue. The value of a
    literal such as 12, 7.3e5, or true is
    also a prvalue. —end example ]

    Every
    expression belongs to exactly one of
    the fundamental classifications in
    this taxonomy: lvalue, xvalue, or
    prvalue. This property of an
    expression is called its value
    category. [ Note: The discussion of
    each built-in operator in Clause 5
    indicates the category of the value it
    yields and the value categories of the
    operands it expects. For example, the
    built-in assignment operators expect
    that the left operand is an lvalue and
    that the right operand is a prvalue
    and yield an lvalue as the result.
    User-defined operators are functions,
    and the categories of values they
    expect and yield are determined by
    their parameter and return types. —end
    note

    不过,我建议您阅读整个3.10节的lvalues和rvalues。

    How do these new categories relate to the existing rvalue and lvalue categories?

    再一次:

    Taxonomy

    Are the rvalue and lvalue categories in C++0x the same as they are in C++03?

    随着移动语义学的引入,rvalues的语义学得到了发展。

    Why are these new categories needed?

    以便定义和支持移动构造/分配。


    我从你最后一个问题开始:

    Why are these new categories needed?

    C++标准包含许多处理表达式的值类别的规则。有些规则区分左值和右值。例如,当涉及过载分辨率时。其他规则区分glvalue和prvalue。例如,您可以拥有不完整或抽象类型的glvalue,但没有不完整或抽象类型的prvalue。在我们使用这个术语之前,实际上需要区分引用lvalue/rvalue的glvalue/prvalue的规则,它们要么是无意中错误的,要么包含大量对规则a la"的解释和例外……除非该rvalue是由于未命名的rvalue引用…"。所以,给glvalues和prvalues的概念取个名字似乎是个好主意。

    What are these new categories of expressions?
    How do these new categories relate to the existing rvalue and lvalue categories?

    我们仍然有与C++ 98兼容的术语LValk和RValk。我们只是将rvalues分为两个子组,xvalues和prvalues,我们将lvalues和xvalues称为glvalues。XValues是一种新的未命名右值引用的值类别。每个表达式都是这三个表达式之一:lvalue、xvalue、prvalue。维恩图如下:

    1
    2
    3
    4
    5
    6
    7
        ______ ______
       /      X      \
      /      / \      \
     |   l  | x |  pr  |
      \      \ /      /
       \______X______/
           gl    r

    函数示例:

    1
    2
    3
    int   prvalue();
    int&  lvalue();
    int&& xvalue();

    但也不要忘记,命名的rvalue引用是lvalue:

    1
    2
    3
    4
    void foo(int&& t) {
      // t is initialized with an rvalue expression
      // but is actually an lvalue expression itself
    }

    Why are these new categories needed? Are the WG21 gods just trying to confuse us mere mortals?

    我觉得其他答案(虽然很多答案都很好)并不能真正抓住这个特定问题的答案。是的,这些类别和类似的存在允许移动语义,但是复杂性的存在有一个原因。这是在C++ 11中移动东西的一个不违反的规则:

    只有在这样做毫无疑问是安全的时候,你才可以移动。

    这就是这些类别存在的原因:能够在安全的地方谈论价值,在不安全的地方谈论价值。

    在R值引用的最早版本中,移动很容易发生。太容易了。很容易,当用户真正无意时,有很多潜在的隐式移动对象。

    在这种情况下,移动某物是安全的:

  • 当它是它的临时对象或子对象时。(PR值)
  • 当用户明确表示要移动它时。
  • 如果你这样做:

    1
    2
    3
    4
    SomeType &&Func() { ... }

    SomeType &&val = Func();
    SomeType otherVal{val};

    这是做什么的?在旧版本的规范中,在输入5个值之前,这将引发一个移动。当然可以。您向构造函数传递了一个右值引用,因此它绑定到接受右值引用的构造函数。这是显而易见的。

    这个只有一个问题,你没有要求移动它。哦,你可能会说&&应该是个线索,但这并不能改变它违反规则的事实。val不是临时的,因为临时的没有名字。您可能已经延长了临时的生命周期,但这意味着它不是临时的;它和其他堆栈变量一样。

    如果不是临时的,而且你没有要求移动它,那么移动是错误的。

    显而易见的解决方案是使val成为左值。这意味着你不能离开它。好的,很好;它的名字,所以它是一个左值。

    一旦你这样做了,你就不能再说SomeType&&在任何地方都意味着同样的事情。现在,您已经区分了命名的右值引用和未命名的右值引用。好吧,名为rvalue的引用是lvalues;这就是我们上面的解决方案。那么,我们称之为未命名的右值引用(上面Func的返回值)是什么呢?

    它不是左值,因为您不能从左值移动。我们需要通过返回一个&&来移动;你还能明确地说移动什么东西吗?毕竟,这就是std::move的回报。它不是一个右值(旧样式),因为它可以在方程的左侧(实际上事情有点复杂,请参阅下面的问题和注释)。它既不是左值,也不是右值;它是一种新事物。

    我们所拥有的是一个值,您可以将其视为左值,但它是可以从中隐式移动的。我们称之为xvalue。

    请注意,xvalues是使我们获得其他两类值的原因:

    • prvalue实际上只是先前类型的rvvalue的新名称,即它们是不是xvalues的rvalue。

    • glvalues是xvalues和lvalues在一个组中的联合,因为它们共享许多共同的属性。

    所以说真的,所有这些都归结为xvalues,需要将移动限制在精确且仅限于特定的地方。这些位置由右值类别定义;prvalues是隐式移动,xvalues是显式移动(std::move返回xvalue)。


    IMHO,The best explanation about its meaning gave us stroustrup+take into account examples of Dániel Sándor and Mohan:

    Stroustrup:

    Now I was seriously worried. Clearly we were headed for an impasse or
    a mess or both. I spent the lunchtime doing an analysis to see which
    of the properties (of values) were independent. There were only two
    independent properties:

    • has identity – i.e. and address, a pointer, the user can determine whether two copies are identical, etc.
    • can be moved from – i.e. we are allowed to leave to source of a"copy" in some indeterminate, but valid state

    This led me to the conclusion that there are exactly three kinds of
    values (using the regex notational trick of using a capital letter to
    indicate a negative – I was in a hurry):

    • iM: has identity and cannot be moved from
    • im: has identity and can be moved from (e.g. the result of casting an lvalue to a rvalue reference)
    • Im: does not have identity and can be moved from.

      The fourth possibility, IM, (doesn’t have identity and cannot be moved) is not
      useful in C++ (or, I think) in any other language.

    In addition to these three fundamental classifications of values, we
    have two obvious generalizations that correspond to the two
    independent properties:

    • i: has identity
    • m: can be moved from

    This led me to put this diagram on the board:
    enter image description here

    Naming

    I observed that we had only limited freedom to name: The two points to
    the left (labeled iM and i) are what people with more or less
    formality have called lvalues and the two points on the right
    (labeled m and Im) are what people with more or less formality
    have called rvalues. This must be reflected in our naming. That is,
    the left"leg" of the W should have names related to lvalue and the
    right"leg" of the W should have names related to rvalue. I note
    that this whole discussion/problem arise from the introduction of
    rvalue references and move semantics. These notions simply don’t exist
    in Strachey’s world consisting of just rvalues and lvalues. Someone
    observed that the ideas that

    • Every value is either an lvalue or an rvalue
    • An lvalue is not an rvalue and an rvalue is not an lvalue

    are deeply embedded in our consciousness, very useful properties, and
    traces of this dichotomy can be found all over the draft standard. We
    all agreed that we ought to preserve those properties (and make them
    precise). This further constrained our naming choices. I observed that
    the standard library wording uses rvalue to mean m (the
    generalization), so that to preserve the expectation and text of the
    standard library the right-hand bottom point of the W should be named
    rvalue.

    This led to a focused discussion of naming. First, we needed to decide
    on lvalue. Should lvalue mean iM or the generalization i? Led
    by Doug Gregor, we listed the places in the core language wording
    where the word lvalue was qualified to mean the one or the other. A
    list was made and in most cases and in the most tricky/brittle text
    lvalue currently means iM. This is the classical meaning of lvalue
    because"in the old days" nothing was moved; move is a novel notion
    in C++0x. Also, naming the topleft point of the W lvalue gives us
    the property that every value is an lvalue or an rvalue, but not both.

    So, the top left point of the W is lvalue and the bottom right point
    is rvalue. What does that make the bottom left and top right points?
    The bottom left point is a generalization of the classical lvalue,
    allowing for move. So it is a generalized lvalue. We named it
    glvalue. You can quibble about the abbreviation, but (I think) not
    with the logic. We assumed that in serious use generalized lvalue
    would somehow be abbreviated anyway, so we had better do it
    immediately (or risk confusion). The top right point of the W is less
    general than the bottom right (now, as ever, called rvalue). That
    point represent the original pure notion of an object you can move
    from because it cannot be referred to again (except by a destructor).
    I liked the phrase specialized rvalue in contrast to generalized
    lvalue
    but pure rvalue abbreviated to prvalue won out (and
    probably rightly so). So, the left leg of the W is lvalue and
    glvalue and the right leg is prvalue and rvalue. Incidentally,
    every value is either a glvalue or a prvalue, but not both.

    This leaves the top middle of the W: im; that is, values that have
    identity and can be moved. We really don’t have anything that guides
    us to a good name for those esoteric beasts. They are important to
    people working with the (draft) standard text, but are unlikely to
    become a household name. We didn’t find any real constraints on the
    naming to guide us, so we picked ‘x’ for the center, the unknown, the
    strange, the xpert only, or even x-rated.

    Steve showing off the final product


    一.导言

    ISOC+11(Officially ISO/IEC 14882:2011)是最新版本的标准C+Programming Language。IT contains some new features,and concepts,for example:

    • RVALUE References
    • Value,Glvalue,Prvalue expression value categories
    • 移动语义

    如果我们想了解新的表达价值类别的概念,我们就必须知道有RVALUE和LVALUE参考。更好地了解RVALUES可以转换为非Const RVALUE References。

    1
    2
    int& r_i=7; // compile error
    int&& rr_i=7; // OK

    如果我们从第3337号工作草案(与ISOC++11标准最相似的版本)中选定了亚组标题为LVALUES和RVALUES的概念,我们可以获得一些价值类别概念的直觉。

    3.10 Lvalues and rvalues [basic.lval]

    1 Expressions are categorized according to the taxonomy in Figure 1.

    • An lvalue (so called, historically, because lvalues could appear on the left-hand side of an assignment expression) designates a function
      or an object. [ Example: If E is an expression of pointer type, then
      *E is an lvalue expression referring to the object or function to which E points. As another example, the result of calling a function
      whose return type is an lvalue reference is an lvalue. —end example ]
    • An xvalue (an"eXpiring" value) also refers to an object, usually near the end of its lifetime (so that its resources may be moved, for
      example). An xvalue is the result of certain kinds of expressions
      involving rvalue references (8.3.2). [ Example: The result of calling
      a function whose return type is an rvalue reference is an xvalue. —end
      example ]
    • A glvalue ("generalized" lvalue) is an lvalue or an xvalue.
    • An rvalue (so called, historically, because rvalues could appear on the right-hand side of an assignment expression) is an xvalue, a
      temporary object (12.2) or subobject thereof, or a value that is not
      associated with an object.
    • A prvalue ("pure" rvalue) is an rvalue that is not an xvalue. [ Example: The result of calling a function whose return type is not a
      reference is a prvalue. The value of a literal such as 12, 7.3e5, or
      true is also a prvalue. —end example ]

    Every expression belongs to exactly one of the fundamental
    classifications in this taxonomy: lvalue, xvalue, or prvalue. This
    property of an expression is called its value category.

    但我并不确定这一次的subsection is enough to understand the concepts clearly,because"usually"is not really general,"near the end of its lifetime"is not really concerte,"involving rvalue references"is not really clear,and"example:the result of calling a function whose return type is an rve reference is an xvalue."像蛇一样,咬着它的尾巴。

    初级价值类别

    每一个表达式都属于一个基本价值类别。这些价值类别是LVALUE、XVALUE和Prvalue类别。

    阿尔瓦卢埃

    The expression e belongs to the Lvalue category if and only if e refers to an entity that already has an identity(address,name or alias)that makes it accessible outside of E.

    ZZU1XValues

    表达属于xvalue类别if,and only if it is

    -The result of calling a function,whether implicitly or explicitly,whose return type is an rvalue reference to the type of object being returned,or

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int&& f(){
        return 3;
    }

    int main()
    {
        f(); // The expression f() belongs to the xvalue category, because f() return type is an rvalue reference to object type.

        return 0;
    }

    a cast to an rvalue reference to object type,or

    1
    2
    3
    4
    5
    6
    7
    int main()
    {
        static_cast<int&&>(7); // The expression static_cast<int&&>(7) belongs to the xvalue category, because it is a cast to an rvalue reference to object type.
        std::move(7); // std::move(7) is equivalent to static_cast<int&&>(7).

        return 0;
    }

    -A class member access expression designating a non-static data member of non-reference type in which the object expression is an xvalue,or

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct As
    {
        int i;
    };

    As&& f(){
        return As();
    }

    int main()
    {
        f().i; // The expression f().i belongs to the xvalue category, because As::i is a non-static data member of non-reference type, and the subexpression f() belongs to the xvlaue category.

        return 0;
    }

    -第一次操作是XValue的指针对成员的表达,第二次操作是数据成员的指针。

    Note that the effect of the rules above is that named rvalue references to objects are treated as lvalues and unnamed rvalue references to objects are treated as xvalues;rvalue references to functions are treated as lvalues whether named

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <functional>

    struct As
    {
        int i;
    };

    As&& f(){
        return As();
    }

    int main()
    {
        f(); // The expression f() belongs to the xvalue category, because it refers to an unnamed rvalue reference to object.
        As&& rr_a=As();
        rr_a; // The expression rr_a belongs to the lvalue category, because it refers to a named rvalue reference to object.
        std::ref(f); // The expression std::ref(f) belongs to the lvalue category, because it refers to an rvalue reference to function.

        return 0;
    }

    Prvalues

    表达式属于Prvalue category if and only if e belongs neither to the Lvalue nor to the Xvalue category.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct As
    {
        void f(){
            this; // The expression this is a prvalue expression. Note, that the expression this is not a variable.
        }
    };

    As f(){
        return As();
    }

    int main()
    {
        f(); // The expression f() belongs to the prvalue category, because it belongs neither to the lvalue nor to the xvalue category.

        return 0;
    }

    混合价值类别

    还有两个重要的混合价值类别。这些价值类别是RVALUE和GLVALUE类别。

    Rvalues

    该表达式属于RVALUE类别if,只有属于XVALUE类别,或属于Prvalue类别。

    Note that this definition means that the expression e belongs to the RVALUE category if and only if e refers to an entity that has not had any identity that makes it accessible outside of e yet.

    Glvalues

    表达式E属于GLVALUCTEGORY IF,只有属于LVALUE类别或XVALUCTEGORY。

    实用规则

    Scott Meyer发布了一条非常有用的Thumb规则,以区分RVALUES和LVALUES。

    BLCK1/


    C++ 03的类别过于受限,无法正确地将R值引用引入到表达式属性中。

    通过引入它们,可以说一个未命名的右值引用的计算结果是右值,这样重载解析将更喜欢右值引用绑定,这将使它选择移动构造函数而不是复制构造函数。但研究发现,这会引起周围的问题,例如动态类型和限定条件。

    要显示这一点,请考虑

    1
    2
    3
    4
    5
    int const&& f();

    int main() {
      int &&i = f(); // disgusting!
    }

    在XEXPROCED草案中,这是允许的,因为在C++ 03中,非类类型的值从不符合CV限定。但我们打算在右值引用案例中应用const,因为这里我们确实引用对象(=memory!)从非类rvalues中删除const主要是因为周围没有对象。

    动态类型的问题具有相似的性质。在C++ 03中,类类型的rRead具有已知的动态类型——它是该表达式的静态类型。因为要换一种方式使用它,需要引用或取消引用,这些引用或取消引用的值为左值。对于未命名的右值引用,情况并非如此,但它们可以显示多态行为。所以要解决这个问题,

    • 未命名的右值引用变为XValues。它们可以是限定的,并且可能具有不同的动态类型。它们确实像预期的那样,在重载期间更喜欢使用右值引用,并且不会绑定到非常量左值引用。

    • 以前的值(文本,由强制转换为非引用类型创建的对象)现在变成了prvalue。它们在重载期间与xvalue具有相同的首选项。

    • 以前的左值仍然是左值。

    两个分组是为了捕获那些可以限定并且可以具有不同动态类型(glvalues)的分组,以及那些重载倾向于使用右值引用绑定(rvalues)的分组。


    我一直在努力,直到我跨越了CPREFERENCE.com explanation of the value categories.

    事实上,这很简单,但我发现它常常以难以记忆的方式解释。这是非常详细的时间表。我会把这页的一些部分:

    Primary categories

    The primary value categories correspond to two properties of expressions:

    • has identity: it's possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly);

    • can be moved from: move constructor, move assignment operator, or another function overload that implements move semantics can bind to the expression.

    Expressions that:

    • have identity and cannot be moved from are called lvalue expressions;
    • have identity and can be moved from are called xvalue expressions;
    • do not have identity and can be moved from are called prvalue expressions;
    • do not have identity and cannot be moved from are not used.

    lvalue

    An lvalue ("left value") expression is an expression that has identity and cannot be moved from.

    rvalue (until C++11), prvalue (since C++11)

    A prvalue ("pure rvalue") expression is an expression that does not have identity and can be moved from.

    xvalue

    An xvalue ("expiring value") expression is an expression that has identity and can be moved from.

    glvalue

    A glvalue ("generalized lvalue") expression is an expression that is either an lvalue or an xvalue. It has identity. It may or may not be moved from.

    rvalue (since C++11)

    An rvalue ("right value") expression is an expression that is either a prvalue or an xvalue. It can be moved from. It may or may not have identity.


    How do these new categories relate to the existing rvalue and lvalue categories?

    C++ 03值仍然是C++ 11值,而C++ 03的值在C++ 11中被称为PROVALL。


    对上述优秀答案的一个补充,在我读了斯特劳斯特鲁普并认为我理解了中值/左值的区别之后,我还是感到困惑。当你看到

    int&& a = 3

    int&&作为一种类型来读,并得出结论,a是一个右值,这是非常诱人的。不是这样的:

    1
    2
    3
    int&& a = 3;
    int&& c = a; //error: cannot bind 'int' lvalue to 'int&&'
    int& b = a; //compiles

    a有一个名称,实际上是一个左值。不要把&&看作a类型的一部分;它只是告诉你a可以绑定到什么。

    这对于构造函数中的T&&类型参数尤其重要。如果你写

    Foo::Foo(T&& _t) : t{_t} {}

    您将把_t复制到t中。你需要

    如果你想搬家的话。当我把move排除在外时,我的编译器会警告我吗?


    正如前面的答案详尽地涵盖了价值类别背后的理论,我想补充一件事:你可以真正玩它并测试它。

    对于值类别的一些实际实验,可以使用decltype说明符。它的行为明确区分了三个主要值类别(xvalue、lvalue和prvalue)。

    使用预处理器可以节省一些输入…

    主要类别:

    1
    2
    3
    #define IS_XVALUE(X) std::is_rvalue_reference<decltype((X))>::value
    #define IS_LVALUE(X) std::is_lvalue_reference<decltype((X))>::value
    #define IS_PRVALUE(X) !std::is_reference<decltype((X))>::value

    混合类别:

    1
    2
    #define IS_GLVALUE(X) IS_LVALUE(X) || IS_XVALUE(X)
    #define IS_RVALUE(X) IS_PRVALUE(X) || IS_XVALUE(X)

    现在,我们可以(几乎)复制CPP中有关价值类别的所有示例。

    这里有一些C++ 17的例子(对于简洁的StasyAsHyt):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    void doesNothing(){}
    struct S
    {
        int x{0};
    };
    int x = 1;
    int y = 2;
    S s;

    static_assert(IS_LVALUE(x));
    static_assert(IS_LVALUE(x+=y));
    static_assert(IS_LVALUE("Hello world!"));
    static_assert(IS_LVALUE(++x));

    static_assert(IS_PRVALUE(1));
    static_assert(IS_PRVALUE(x++));
    static_assert(IS_PRVALUE(static_cast<double>(x)));
    static_assert(IS_PRVALUE(std::string{}));
    static_assert(IS_PRVALUE(throw std::exception()));
    static_assert(IS_PRVALUE(doesNothing()));

    static_assert(IS_XVALUE(std::move(s)));
    // The next one doesn't work in gcc 8.2 but in gcc(trunk). Clang 7.0.0 and msvc 19.16 are doing fine.
    static_assert(IS_XVALUE(S().x));

    一旦你找到了主要的分类,混合的分类就有点无聊了。

    有关更多示例(和实验),请查看编译器资源管理器上的以下链接。不过,别费心看大会了。我添加了很多编译器,只是为了确保它能在所有常见的编译器中工作。